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