• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2Decorators to wrap functions to make them WSGI applications.
3
4The main decorator :class:`wsgify` turns a function into a WSGI
5application (while also allowing normal calling of the method with an
6instantiated request).
7"""
8
9from webob.compat import (
10    bytes_,
11    text_type,
12    )
13
14from webob.request import Request
15from webob.exc import HTTPException
16
17__all__ = ['wsgify']
18
19class 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
262class _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
282class _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