Home | History | Annotate | Download | only in webob
      1 """
      2 Decorators to wrap functions to make them WSGI applications.
      3 
      4 The main decorator :class:`wsgify` turns a function into a WSGI
      5 application (while also allowing normal calling of the method with an
      6 instantiated request).
      7 """
      8 
      9 from webob.compat import (
     10     bytes_,
     11     text_type,
     12     )
     13 
     14 from webob.request import Request
     15 from webob.exc import HTTPException
     16 
     17 __all__ = ['wsgify']
     18 
     19 class wsgify(object):
     20     """Turns a request-taking, response-returning function into a WSGI
     21     app
     22 
     23     You can use this like::
     24 
     25         @wsgify
     26         def myfunc(req):
     27             return webob.Response('hey there')
     28 
     29     With that ``myfunc`` will be a WSGI application, callable like
     30     ``app_iter = myfunc(environ, start_response)``.  You can also call
     31     it like normal, e.g., ``resp = myfunc(req)``.  (You can also wrap
     32     methods, like ``def myfunc(self, req)``.)
     33 
     34     If you raise exceptions from :mod:`webob.exc` they will be turned
     35     into WSGI responses.
     36 
     37     There are also several parameters you can use to customize the
     38     decorator.  Most notably, you can use a :class:`webob.Request`
     39     subclass, like::
     40 
     41         class MyRequest(webob.Request):
     42             @property
     43             def is_local(self):
     44                 return self.remote_addr == '127.0.0.1'
     45         @wsgify(RequestClass=MyRequest)
     46         def myfunc(req):
     47             if req.is_local:
     48                 return Response('hi!')
     49             else:
     50                 raise webob.exc.HTTPForbidden
     51 
     52     Another customization you can add is to add `args` (positional
     53     arguments) or `kwargs` (of course, keyword arguments).  While
     54     generally not that useful, you can use this to create multiple
     55     WSGI apps from one function, like::
     56 
     57         import simplejson
     58         def serve_json(req, json_obj):
     59             return Response(json.dumps(json_obj),
     60                             content_type='application/json')
     61 
     62         serve_ob1 = wsgify(serve_json, args=(ob1,))
     63         serve_ob2 = wsgify(serve_json, args=(ob2,))
     64 
     65     You can return several things from a function:
     66 
     67     * A :class:`webob.Response` object (or subclass)
     68     * *Any* WSGI application
     69     * None, and then ``req.response`` will be used (a pre-instantiated
     70       Response object)
     71     * A string, which will be written to ``req.response`` and then that
     72       response will be used.
     73     * Raise an exception from :mod:`webob.exc`
     74 
     75     Also see :func:`wsgify.middleware` for a way to make middleware.
     76 
     77     You can also subclass this decorator; the most useful things to do
     78     in a subclass would be to change `RequestClass` or override
     79     `call_func` (e.g., to add ``req.urlvars`` as keyword arguments to
     80     the function).
     81     """
     82 
     83     RequestClass = Request
     84 
     85     def __init__(self, func=None, RequestClass=None,
     86                  args=(), kwargs=None, middleware_wraps=None):
     87         self.func = func
     88         if (RequestClass is not None
     89             and RequestClass is not self.RequestClass):
     90             self.RequestClass = RequestClass
     91         self.args = tuple(args)
     92         if kwargs is None:
     93             kwargs = {}
     94         self.kwargs = kwargs
     95         self.middleware_wraps = middleware_wraps
     96 
     97     def __repr__(self):
     98         return '<%s at %s wrapping %r>' % (self.__class__.__name__,
     99                                            id(self), self.func)
    100 
    101     def __get__(self, obj, type=None):
    102         # This handles wrapping methods
    103         if hasattr(self.func, '__get__'):
    104             return self.clone(self.func.__get__(obj, type))
    105         else:
    106             return self
    107 
    108     def __call__(self, req, *args, **kw):
    109         """Call this as a WSGI application or with a request"""
    110         func = self.func
    111         if func is None:
    112             if args or kw:
    113                 raise TypeError(
    114                     "Unbound %s can only be called with the function it "
    115                     "will wrap" % self.__class__.__name__)
    116             func = req
    117             return self.clone(func)
    118         if isinstance(req, dict):
    119             if len(args) != 1 or kw:
    120                 raise TypeError(
    121                     "Calling %r as a WSGI app with the wrong signature")
    122             environ = req
    123             start_response = args[0]
    124             req = self.RequestClass(environ)
    125             req.response = req.ResponseClass()
    126             try:
    127                 args = self.args
    128                 if self.middleware_wraps:
    129                     args = (self.middleware_wraps,) + args
    130                 resp = self.call_func(req, *args, **self.kwargs)
    131             except HTTPException as exc:
    132                 resp = exc
    133             if resp is None:
    134                 ## FIXME: I'm not sure what this should be?
    135                 resp = req.response
    136             if isinstance(resp, text_type):
    137                 resp = bytes_(resp, req.charset)
    138             if isinstance(resp, bytes):
    139                 body = resp
    140                 resp = req.response
    141                 resp.write(body)
    142             if resp is not req.response:
    143                 resp = req.response.merge_cookies(resp)
    144             return resp(environ, start_response)
    145         else:
    146             if self.middleware_wraps:
    147                 args = (self.middleware_wraps,) + args
    148             return self.func(req, *args, **kw)
    149 
    150     def get(self, url, **kw):
    151         """Run a GET request on this application, returning a Response.
    152 
    153         This creates a request object using the given URL, and any
    154         other keyword arguments are set on the request object (e.g.,
    155         ``last_modified=datetime.now()``).
    156 
    157         ::
    158 
    159             resp = myapp.get('/article?id=10')
    160         """
    161         kw.setdefault('method', 'GET')
    162         req = self.RequestClass.blank(url, **kw)
    163         return self(req)
    164 
    165     def post(self, url, POST=None, **kw):
    166         """Run a POST request on this application, returning a Response.
    167 
    168         The second argument (`POST`) can be the request body (a
    169         string), or a dictionary or list of two-tuples, that give the
    170         POST body.
    171 
    172         ::
    173 
    174             resp = myapp.post('/article/new',
    175                               dict(title='My Day',
    176                                    content='I ate a sandwich'))
    177         """
    178         kw.setdefault('method', 'POST')
    179         req = self.RequestClass.blank(url, POST=POST, **kw)
    180         return self(req)
    181 
    182     def request(self, url, **kw):
    183         """Run a request on this application, returning a Response.
    184 
    185         This can be used for DELETE, PUT, etc requests.  E.g.::
    186 
    187             resp = myapp.request('/article/1', method='PUT', body='New article')
    188         """
    189         req = self.RequestClass.blank(url, **kw)
    190         return self(req)
    191 
    192     def call_func(self, req, *args, **kwargs):
    193         """Call the wrapped function; override this in a subclass to
    194         change how the function is called."""
    195         return self.func(req, *args, **kwargs)
    196 
    197     def clone(self, func=None, **kw):
    198         """Creates a copy/clone of this object, but with some
    199         parameters rebound
    200         """
    201         kwargs = {}
    202         if func is not None:
    203             kwargs['func'] = func
    204         if self.RequestClass is not self.__class__.RequestClass:
    205             kwargs['RequestClass'] = self.RequestClass
    206         if self.args:
    207             kwargs['args'] = self.args
    208         if self.kwargs:
    209             kwargs['kwargs'] = self.kwargs
    210         kwargs.update(kw)
    211         return self.__class__(**kwargs)
    212 
    213     # To match @decorator:
    214     @property
    215     def undecorated(self):
    216         return self.func
    217 
    218     @classmethod
    219     def middleware(cls, middle_func=None, app=None, **kw):
    220         """Creates middleware
    221 
    222         Use this like::
    223 
    224             @wsgify.middleware
    225             def restrict_ip(req, app, ips):
    226                 if req.remote_addr not in ips:
    227                     raise webob.exc.HTTPForbidden('Bad IP: %s' % req.remote_addr)
    228                 return app
    229 
    230             @wsgify
    231             def app(req):
    232                 return 'hi'
    233 
    234             wrapped = restrict_ip(app, ips=['127.0.0.1'])
    235 
    236         Or if you want to write output-rewriting middleware::
    237 
    238             @wsgify.middleware
    239             def all_caps(req, app):
    240                 resp = req.get_response(app)
    241                 resp.body = resp.body.upper()
    242                 return resp
    243 
    244             wrapped = all_caps(app)
    245 
    246         Note that you must call ``req.get_response(app)`` to get a WebOb
    247         response object.  If you are not modifying the output, you can just
    248         return the app.
    249 
    250         As you can see, this method doesn't actually create an application, but
    251         creates "middleware" that can be bound to an application, along with
    252         "configuration" (that is, any other keyword arguments you pass when
    253         binding the application).
    254 
    255         """
    256         if middle_func is None:
    257             return _UnboundMiddleware(cls, app, kw)
    258         if app is None:
    259             return _MiddlewareFactory(cls, middle_func, kw)
    260         return cls(middle_func, middleware_wraps=app, kwargs=kw)
    261 
    262 class _UnboundMiddleware(object):
    263     """A `wsgify.middleware` invocation that has not yet wrapped a
    264     middleware function; the intermediate object when you do
    265     something like ``@wsgify.middleware(RequestClass=Foo)``
    266     """
    267 
    268     def __init__(self, wrapper_class, app, kw):
    269         self.wrapper_class = wrapper_class
    270         self.app = app
    271         self.kw = kw
    272 
    273     def __repr__(self):
    274         return '<%s at %s wrapping %r>' % (self.__class__.__name__,
    275                                            id(self), self.app)
    276 
    277     def __call__(self, func, app=None):
    278         if app is None:
    279             app = self.app
    280         return self.wrapper_class.middleware(func, app=app, **self.kw)
    281 
    282 class _MiddlewareFactory(object):
    283     """A middleware that has not yet been bound to an application or
    284     configured.
    285     """
    286 
    287     def __init__(self, wrapper_class, middleware, kw):
    288         self.wrapper_class = wrapper_class
    289         self.middleware = middleware
    290         self.kw = kw
    291 
    292     def __repr__(self):
    293         return '<%s at %s wrapping %r>' % (self.__class__.__name__, id(self),
    294                                            self.middleware)
    295 
    296     def __call__(self, app, **config):
    297         kw = self.kw.copy()
    298         kw.update(config)
    299         return self.wrapper_class.middleware(self.middleware, app, **kw)
    300