312 lines
10 KiB
Python
312 lines
10 KiB
Python
"""
|
|
Decorators to wrap functions to make them WSGI applications.
|
|
|
|
The main decorator :class:`wsgify` turns a function into a WSGI
|
|
application (while also allowing normal calling of the method with an
|
|
instantiated request).
|
|
"""
|
|
|
|
from webob.compat import (
|
|
bytes_,
|
|
text_type,
|
|
)
|
|
|
|
from webob.request import Request
|
|
from webob.exc import HTTPException
|
|
|
|
__all__ = ['wsgify']
|
|
|
|
class wsgify(object):
|
|
"""Turns a request-taking, response-returning function into a WSGI
|
|
app
|
|
|
|
You can use this like::
|
|
|
|
@wsgify
|
|
def myfunc(req):
|
|
return webob.Response('hey there')
|
|
|
|
With that ``myfunc`` will be a WSGI application, callable like
|
|
``app_iter = myfunc(environ, start_response)``. You can also call
|
|
it like normal, e.g., ``resp = myfunc(req)``. (You can also wrap
|
|
methods, like ``def myfunc(self, req)``.)
|
|
|
|
If you raise exceptions from :mod:`webob.exc` they will be turned
|
|
into WSGI responses.
|
|
|
|
There are also several parameters you can use to customize the
|
|
decorator. Most notably, you can use a :class:`webob.Request`
|
|
subclass, like::
|
|
|
|
class MyRequest(webob.Request):
|
|
@property
|
|
def is_local(self):
|
|
return self.remote_addr == '127.0.0.1'
|
|
@wsgify(RequestClass=MyRequest)
|
|
def myfunc(req):
|
|
if req.is_local:
|
|
return Response('hi!')
|
|
else:
|
|
raise webob.exc.HTTPForbidden
|
|
|
|
Another customization you can add is to add `args` (positional
|
|
arguments) or `kwargs` (of course, keyword arguments). While
|
|
generally not that useful, you can use this to create multiple
|
|
WSGI apps from one function, like::
|
|
|
|
import simplejson
|
|
def serve_json(req, json_obj):
|
|
return Response(json.dumps(json_obj),
|
|
content_type='application/json')
|
|
|
|
serve_ob1 = wsgify(serve_json, args=(ob1,))
|
|
serve_ob2 = wsgify(serve_json, args=(ob2,))
|
|
|
|
You can return several things from a function:
|
|
|
|
* A :class:`webob.Response` object (or subclass)
|
|
* *Any* WSGI application
|
|
* None, and then ``req.response`` will be used (a pre-instantiated
|
|
Response object)
|
|
* A string, which will be written to ``req.response`` and then that
|
|
response will be used.
|
|
* Raise an exception from :mod:`webob.exc`
|
|
|
|
Also see :func:`wsgify.middleware` for a way to make middleware.
|
|
|
|
You can also subclass this decorator; the most useful things to do
|
|
in a subclass would be to change `RequestClass` or override
|
|
`call_func` (e.g., to add ``req.urlvars`` as keyword arguments to
|
|
the function).
|
|
"""
|
|
|
|
RequestClass = Request
|
|
|
|
def __init__(self, func=None, RequestClass=None,
|
|
args=(), kwargs=None, middleware_wraps=None):
|
|
self.func = func
|
|
if (RequestClass is not None
|
|
and RequestClass is not self.RequestClass):
|
|
self.RequestClass = RequestClass
|
|
self.args = tuple(args)
|
|
if kwargs is None:
|
|
kwargs = {}
|
|
self.kwargs = kwargs
|
|
self.middleware_wraps = middleware_wraps
|
|
|
|
def __repr__(self):
|
|
return '<%s at %s wrapping %r>' % (self.__class__.__name__,
|
|
id(self), self.func)
|
|
|
|
def __get__(self, obj, type=None):
|
|
# This handles wrapping methods
|
|
if hasattr(self.func, '__get__'):
|
|
return self.clone(self.func.__get__(obj, type))
|
|
else:
|
|
return self
|
|
|
|
def __call__(self, req, *args, **kw):
|
|
"""Call this as a WSGI application or with a request"""
|
|
func = self.func
|
|
if func is None:
|
|
if args or kw:
|
|
raise TypeError(
|
|
"Unbound %s can only be called with the function it "
|
|
"will wrap" % self.__class__.__name__)
|
|
func = req
|
|
return self.clone(func)
|
|
if isinstance(req, dict):
|
|
if len(args) != 1 or kw:
|
|
raise TypeError(
|
|
"Calling %r as a WSGI app with the wrong signature" %
|
|
self.func)
|
|
environ = req
|
|
start_response = args[0]
|
|
req = self.RequestClass(environ)
|
|
req.response = req.ResponseClass()
|
|
try:
|
|
args, kw = self._prepare_args(None, None)
|
|
resp = self.call_func(req, *args, **kw)
|
|
except HTTPException as exc:
|
|
resp = exc
|
|
if resp is None:
|
|
## FIXME: I'm not sure what this should be?
|
|
resp = req.response
|
|
if isinstance(resp, text_type):
|
|
resp = bytes_(resp, req.charset)
|
|
if isinstance(resp, bytes):
|
|
body = resp
|
|
resp = req.response
|
|
resp.write(body)
|
|
if resp is not req.response:
|
|
resp = req.response.merge_cookies(resp)
|
|
return resp(environ, start_response)
|
|
else:
|
|
args, kw = self._prepare_args(args, kw)
|
|
return self.call_func(req, *args, **kw)
|
|
|
|
def get(self, url, **kw):
|
|
"""Run a GET request on this application, returning a Response.
|
|
|
|
This creates a request object using the given URL, and any
|
|
other keyword arguments are set on the request object (e.g.,
|
|
``last_modified=datetime.now()``).
|
|
|
|
::
|
|
|
|
resp = myapp.get('/article?id=10')
|
|
"""
|
|
kw.setdefault('method', 'GET')
|
|
req = self.RequestClass.blank(url, **kw)
|
|
return self(req)
|
|
|
|
def post(self, url, POST=None, **kw):
|
|
"""Run a POST request on this application, returning a Response.
|
|
|
|
The second argument (`POST`) can be the request body (a
|
|
string), or a dictionary or list of two-tuples, that give the
|
|
POST body.
|
|
|
|
::
|
|
|
|
resp = myapp.post('/article/new',
|
|
dict(title='My Day',
|
|
content='I ate a sandwich'))
|
|
"""
|
|
kw.setdefault('method', 'POST')
|
|
req = self.RequestClass.blank(url, POST=POST, **kw)
|
|
return self(req)
|
|
|
|
def request(self, url, **kw):
|
|
"""Run a request on this application, returning a Response.
|
|
|
|
This can be used for DELETE, PUT, etc requests. E.g.::
|
|
|
|
resp = myapp.request('/article/1', method='PUT', body='New article')
|
|
"""
|
|
req = self.RequestClass.blank(url, **kw)
|
|
return self(req)
|
|
|
|
def call_func(self, req, *args, **kwargs):
|
|
"""Call the wrapped function; override this in a subclass to
|
|
change how the function is called."""
|
|
return self.func(req, *args, **kwargs)
|
|
|
|
def clone(self, func=None, **kw):
|
|
"""Creates a copy/clone of this object, but with some
|
|
parameters rebound
|
|
"""
|
|
kwargs = {}
|
|
if func is not None:
|
|
kwargs['func'] = func
|
|
if self.RequestClass is not self.__class__.RequestClass:
|
|
kwargs['RequestClass'] = self.RequestClass
|
|
if self.args:
|
|
kwargs['args'] = self.args
|
|
if self.kwargs:
|
|
kwargs['kwargs'] = self.kwargs
|
|
kwargs.update(kw)
|
|
return self.__class__(**kwargs)
|
|
|
|
# To match @decorator:
|
|
@property
|
|
def undecorated(self):
|
|
return self.func
|
|
|
|
@classmethod
|
|
def middleware(cls, middle_func=None, app=None, **kw):
|
|
"""Creates middleware
|
|
|
|
Use this like::
|
|
|
|
@wsgify.middleware
|
|
def restrict_ip(req, app, ips):
|
|
if req.remote_addr not in ips:
|
|
raise webob.exc.HTTPForbidden('Bad IP: %s' % req.remote_addr)
|
|
return app
|
|
|
|
@wsgify
|
|
def app(req):
|
|
return 'hi'
|
|
|
|
wrapped = restrict_ip(app, ips=['127.0.0.1'])
|
|
|
|
Or as a decorator::
|
|
|
|
@restrict_ip(ips=['127.0.0.1'])
|
|
@wsgify
|
|
def wrapped_app(req):
|
|
return 'hi'
|
|
|
|
Or if you want to write output-rewriting middleware::
|
|
|
|
@wsgify.middleware
|
|
def all_caps(req, app):
|
|
resp = req.get_response(app)
|
|
resp.body = resp.body.upper()
|
|
return resp
|
|
|
|
wrapped = all_caps(app)
|
|
|
|
Note that you must call ``req.get_response(app)`` to get a WebOb
|
|
response object. If you are not modifying the output, you can just
|
|
return the app.
|
|
|
|
As you can see, this method doesn't actually create an application, but
|
|
creates "middleware" that can be bound to an application, along with
|
|
"configuration" (that is, any other keyword arguments you pass when
|
|
binding the application).
|
|
|
|
"""
|
|
if middle_func is None:
|
|
return _UnboundMiddleware(cls, app, kw)
|
|
if app is None:
|
|
return _MiddlewareFactory(cls, middle_func, kw)
|
|
return cls(middle_func, middleware_wraps=app, kwargs=kw)
|
|
|
|
def _prepare_args(self, args, kwargs):
|
|
args = args or self.args
|
|
kwargs = kwargs or self.kwargs
|
|
if self.middleware_wraps:
|
|
args = (self.middleware_wraps,) + args
|
|
return args, kwargs
|
|
|
|
class _UnboundMiddleware(object):
|
|
"""A `wsgify.middleware` invocation that has not yet wrapped a
|
|
middleware function; the intermediate object when you do
|
|
something like ``@wsgify.middleware(RequestClass=Foo)``
|
|
"""
|
|
|
|
def __init__(self, wrapper_class, app, kw):
|
|
self.wrapper_class = wrapper_class
|
|
self.app = app
|
|
self.kw = kw
|
|
|
|
def __repr__(self):
|
|
return '<%s at %s wrapping %r>' % (self.__class__.__name__,
|
|
id(self), self.app)
|
|
|
|
def __call__(self, func, app=None):
|
|
if app is None:
|
|
app = self.app
|
|
return self.wrapper_class.middleware(func, app=app, **self.kw)
|
|
|
|
class _MiddlewareFactory(object):
|
|
"""A middleware that has not yet been bound to an application or
|
|
configured.
|
|
"""
|
|
|
|
def __init__(self, wrapper_class, middleware, kw):
|
|
self.wrapper_class = wrapper_class
|
|
self.middleware = middleware
|
|
self.kw = kw
|
|
|
|
def __repr__(self):
|
|
return '<%s at %s wrapping %r>' % (self.__class__.__name__, id(self),
|
|
self.middleware)
|
|
|
|
def __call__(self, app=None, **config):
|
|
kw = self.kw.copy()
|
|
kw.update(config)
|
|
return self.wrapper_class.middleware(self.middleware, app, **kw)
|