""" 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") environ = req start_response = args[0] req = self.RequestClass(environ) req.response = req.ResponseClass() try: args = self.args if self.middleware_wraps: args = (self.middleware_wraps,) + args resp = self.call_func(req, *args, **self.kwargs) 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: if self.middleware_wraps: args = (self.middleware_wraps,) + args return self.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 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) 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, **config): kw = self.kw.copy() kw.update(config) return self.wrapper_class.middleware(self.middleware, app, **kw)