diff options
| author | Ian Bicking <ianb@colorstudy.com> | 2009-08-07 21:41:03 +0000 |
|---|---|---|
| committer | Ian Bicking <ianb@colorstudy.com> | 2009-08-07 21:41:03 +0000 |
| commit | 98a4883c24b590ff13e7e175c0bce88da4d11d8d (patch) | |
| tree | b75fb825a7a362b203cf05a4976635da8b805f4b | |
| parent | b6425dfe4535831d9189919ffb42462de9dc1ca2 (diff) | |
| download | webob-ianb-decorator-experiment.tar.gz | |
Substantial refactor of webob.dec.wsgifyianb-decorator-experiment
| -rw-r--r-- | tests/test_dec.txt | 133 | ||||
| -rw-r--r-- | webob/dec.py | 310 |
2 files changed, 89 insertions, 354 deletions
diff --git a/tests/test_dec.txt b/tests/test_dec.txt index 69ef988..40a0fc8 100644 --- a/tests/test_dec.txt +++ b/tests/test_dec.txt @@ -12,12 +12,12 @@ A test of the decorator module:: ... req = Request.blank(req) ... resp = req.get_response(app) ... print resp - >>> testit(test_app.wsgi_app, 'a url') + >>> testit(test_app, '/a url') 200 OK Content-Type: text/html; charset=UTF-8 - Content-Length: 44 + Content-Length: 45 <BLANKLINE> - hey, this is a test: http://localhosta%20url + hey, this is a test: http://localhost/a%20url >>> test_app wsgify(test_app) @@ -27,13 +27,13 @@ Now some middleware testing:: ... def set_urlvar(req, app, **vars): ... req.urlvars.update(vars) ... return app(req) - >>> @wsgify(add_urlvars=True) - ... def show_vars(req, **vars): - ... return 'These are the vars: %r' % (sorted(vars.items())) - >>> show_vars2 = set_urlvar(show_vars.wsgi_app, a=1, b=2) + >>> @wsgify + ... def show_vars(req): + ... return 'These are the vars: %r' % (sorted(req.urlvars.items())) + >>> show_vars2 = set_urlvar(show_vars, a=1, b=2) >>> show_vars2 - wsgify.middleware(set_urlvar)(wsgify(webob.dec.method), a=1, b=2) - >>> testit(show_vars2.wsgi_app, '/path') + wsgify.middleware(set_urlvar)(wsgify(show_vars), a=1, b=2) + >>> testit(show_vars2, '/path') 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 40 @@ -70,105 +70,11 @@ Some examples from Sergey:: <BLANKLINE> http -Now a controller-like class:: - - >>> def getapp(obj): - ... if hasattr(obj, 'wsgi_app'): - ... return obj.wsgi_app - ... if hasattr(obj, '__call__') and hasattr(obj.__call__, 'wsgi_app'): - ... return obj.__call__.wsgi_app - ... return obj - >>> class instantiate_wsgi_app(object): - ... def __get__(self, obj, type=None): - ... if obj is None: - ... def wsgi_app(environ, start_response): - ... instance = type() - ... if hasattr(instance.__call__, 'wsgi_app'): - ... instance = instance.__call__.wsgi_app - ... return instance(environ, start_response) - ... return wsgi_app - ... return getapp(obj) - >>> class Controller(object): - ... def __before__(self): - ... pass - ... def __after__(self, resp): - ... return resp - ... @wsgify - ... def __call__(self, req): - ... self.__before__() - ... action = req.urlvars.get('action', 'index') - ... method = getattr(self, action, self.not_found) - ... resp = method(req) - ... resp = self.__after__(resp) - ... return resp - ... @wsgify - ... def not_found(self, req): - ... raise exc.HTTPNotFound() - ... wsgi_app = instantiate_wsgi_app() - >>> Controller.not_found - wsgify(Controller.not_found) - >>> Controller().not_found # doctest: +ELLIPSIS - wsgify(<Controller object at ...>.not_found) - >>> class dispatch_method(object): - ... def __init__(self, method_name, obj=None, not_found_attr='not_found'): - ... self.method_name = method_name - ... self.obj = obj - ... self.not_found_attr = not_found_attr - ... def __get__(self, obj, type=None): - ... if obj is not None: - ... return self.__class__(self.method_name, obj) - ... else: - ... return self.__class__(self.method_name, type) - ... @wsgify - ... def __call__(self, req): - ... method = getattr(self.obj, '%s_%s' % (self.method_name, req.method), None) - ... if method is None: - ... method = getattr(self.obj, self.not_found_attr, exc.HTTPNotFound()) - ... return method(req) - >>> class MyController(Controller): - ... def index(self, req): - ... return Response('some menu of items') - ... form = dispatch_method('form') - ... def form_GET(self, req): - ... return 'here is a form' - ... def form_POST(self, req): - ... raise exc.HTTPFound(location=req.application_url).exception - >>> import re - >>> class Router(object): - ... def __init__(self, *items): - ... self.items = list(items) - ... def add(self, route, controller): - ... self.items.append((route, controller)) - ... @wsgify - ... def __call__(self, req): - ... for route, controller in self.items: - ... match = re.match(route, req.path_info) - ... if match: - ... req.urlvars.update(match.groupdict()) - ... return getapp(controller) - ... raise exc.HTTPNotFound().exception - >>> router = Router() - >>> router.add('^/$', MyController) - >>> router.add('^/(?P<action>[^/]+)$', MyController) - >>> print Request.blank('/').get_response(getapp(router)).body - some menu of items - >>> print Request.blank('/form').get_response(getapp(router)).body - here is a form - >>> print Request.blank('/form', method='POST').get_response(getapp(router)) - 302 Found - Content-Type: text/html; charset=UTF-8 - location: http://localhost - Content-Length: 96 - <BLANKLINE> - 302 Found - <BLANKLINE> - The resource was found at http://localhost; you should be redirected automatically. - -Now a status checking middleware:: +A status checking middleware:: >>> @wsgify.middleware ... def catch(req, app, catchers): - ... resp = app(req) + ... resp = req.get_response(app) ... return catchers.get(resp.status_int, resp) >>> @wsgify ... def simple(req): @@ -195,20 +101,3 @@ Now a status checking middleware:: Content-Length: 7 <BLANKLINE> nothing - -Or, here's the instantiating descriptor:: - - >>> class MyClass(object): - ... def __init__(self, req): - ... self.req = req - ... wsgi_app = wsgify.instantiator() - ... def __call__(self): - ... return 'Hi %s' % self.req.path_info - >>> MyClass.wsgi_app - <wsgify.instantiator() bound to MyClass> - >>> print Request.blank('/hey').get_response(MyClass.wsgi_app) - 200 OK - Content-Type: text/html; charset=UTF-8 - Content-Length: 7 - <BLANKLINE> - Hi /hey diff --git a/webob/dec.py b/webob/dec.py index 891a418..6a465a4 100644 --- a/webob/dec.py +++ b/webob/dec.py @@ -14,231 +14,113 @@ from types import ClassType __all__ = ['wsgify', 'wsgiwrap'] class wsgify(object): - - # The class to use for requests: RequestClass = webob.Request - # Bind these attributes to the request object when the request - # comes in: - request_attrs = {} - # If true, then **req.urlvars are added to the signature - add_urlvars = False - # Positional arguments to add to the function call: - add_call_args = () - # Keyword arguments to add to the function call: - add_keyword_args = {} - # When this is middleware that wraps an application: - middleware_wraps = None - # When this is used as an instantiator: - _instantiate_class = None - - def __init__(self, func=None, **kw): - """Decorate `func` as a WSGI application - - If `func` is None, then this is a lazy binding of ``**kw`` and - the resulting object is a decorator for the func, like:: - - @wsgify(add_urlvars=True) - def app(req, *args, **kw): - return Response(...) - Note that the resulting object *is not itself a WSGI application*. - Instead it has an attribute ``.wsgi_app`` which is a WSGI - application. - """ + def __init__(self, func=None, RequestClass=None, + args=(), kwargs=None, middleware_wraps=None): self.func = func - if func is not None: - for attr in ('__name__', 'func_name', 'func_doc', '__doc__'): - if hasattr(func, attr): - setattr(self, attr, getattr(func, attr)) - self._instance_args = kw - for name, value in kw.iteritems(): - if not hasattr(self, name): - raise TypeError("Unexpected argument: %s" % name) - setattr(self, name, value) + 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): if self.func is None: args = [] else: args = [_func_name(self.func)] - args.extend(['%s=%r' % (name, value) for name, value in sorted(self._instance_args.items()) - if name != 'middleware_wraps' - and (name != 'add_keyword_args' or self.middleware_wraps is None) - and (name != '_instantiate_class')]) + if self.RequestClass is not self.__class__.RequestClass: + args.append('RequestClass=%r' % self.RequestClass) + if self.args: + args.append('args=%r' % self.args) my_name = self.__class__.__name__ if self.middleware_wraps is not None: my_name = '%s.middleware' % my_name - if self._instantiate_class is not None: - my_name = '%s.instantiator' % my_name - r = '%s(%s)' % ( - my_name, ', '.join(args)) - if self.middleware_wraps: + else: + if self.kwargs: + args.append('kwargs=%r' % self.kwargs) + r = '%s(%s)' % (my_name, ', '.join(args)) + if self.middleware_wraps is not None: args = [repr(self.middleware_wraps)] - if self.add_keyword_args: - args.extend(['%s=%r' % (name, value) for name, value in sorted(self.add_keyword_args.items())]) + if self.kwargs: + args.extend(['%s=%r' % (name, value) + for name, value in sorted(self.kwargs.items())]) r += '(%s)' % ', '.join(args) - if self._instantiate_class is not None and self._instantiate_class is not True: - r = '<%s bound to %s>' % (r, _func_name(self._instantiate_class)) return r - # To match @decorator: - @property - def undecorated(self): - return self.func - - def __call__(self, req, *args, **kw): - if self._instantiate_class is not None: - return self.wsgi_app(req, args[0]) - if self.func is None: - func = req - if not func or args or kw: - raise TypeError('wsgiapp is unbound; you must call it with a function') - return self.__class__(func, **self._instance_args) - if type(req) is dict: - # A WSGI call - if len(args) != 1 or kw: - raise TypeError( - "Improper WSGI call signature: %r(%s)" - % (self, _format_args((req,)+args, kw))) - return self.wsgi_app(req, args[0]) - return self.call(req, *args, **kw) - - def call(self, req, *args, **kw): - """Call the function like normal, normalizing the response if - asked for - """ - if self._instantiate_class is None: - args = (req,)+args - func = self.func - else: - if self._instantiate_class is True: - raise TypeError('%r called with no bound class (did you add it to a new-style class?' - % self) - func = self._instantiate_class(req) - resp = func(*args, **kw) - resp = self.normalize_response(req, resp) - return resp - def __get__(self, obj, type=None): if hasattr(self.func, '__get__'): - return self.__class__(self.func.__get__(obj, type), **self._instance_args) + return self.clone(self.func.__get__(obj, type)) else: return self - def wsgi_app(self, environ, start_response): - """The WSGI calling signature for this wrapped function""" - req = self.RequestClass(environ, **self.request_attrs) - req.response = req.ResponseClass() - req.response.request = req - # Hacky, but I think it improves the traceback: (?) - if self.handle_exception: - handle_exception = Exception # Catches all (well, most) exceptions - else: - handle_exception = None # Catches no exceptions - try: - resp = self.apply(req) - except webob.exc.HTTPException, resp: - pass # the exception is the new response - except handle_exception, e: - resp = self.handle_exception(req, e) + def __call__(self, req, *args, **kw): + 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() + req.response.request = req + try: + args = self.args + if self.middleware_wraps: + args = (self.middleware_wraps,) + args + resp = self.func(req, *args, **self.kwargs) + except webob.exc.HTTPException, resp: + pass if resp is None: - raise - resp = self.normalize_response(req, resp) - if hasattr(resp, 'wsgi_app'): - # Allows chaining return values - resp = resp.wsgi_app - return resp(environ, start_response) - - def normalize_response(self, req, resp): - if resp is None: - ## FIXME: I'm not sure this is a good idea? - resp = req.response - elif isinstance(resp, basestring): - body = resp - resp = req.response - resp.write(body) - if resp is not req.response: - resp = req.response.merge_cookies(resp) - return resp - - # Optional exception handler: - handle_exception = None - - def apply(self, req): - """For use by subclasses to override the calling signature""" - args = (req,) - if self.middleware_wraps: - args += (self.middleware_wraps,) - if self.add_call_args: - args += tuple(self.add_call_args) - if self.add_urlvars: - args = args + tuple(req.urlargs) - kw = req.urlvars + ## FIXME: I'm not sure what this should be? + resp = req.response + elif isinstance(resp, basestring): + 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: - kw = {} - if self.add_keyword_args: - kw.update(self.add_keyword_args) - return self.call(*args, **kw) + return self.func(req, *args, **kw) - @classmethod - def reverse(cls, wsgi_app): - """Takes a WSGI application and gives it a calling signature - similar to a wrapped function (``resp = func(req)``)""" - if hasattr(wsgi_app, 'wsgi_app'): - wsgi_app = wsgi_app.wsgi_app - def method(req): - return req.get_response(wsgi_app) - return cls(method) + def clone(self, func=None, **kw): + 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): - """Wrap a function as middleware, to create either a new - application (if you pass in `app`) or middleware factory (a - function that takes an `app`). - - Use this like:: - - @wsgify.middleware - def set_user(req, app, username): - req.remote_user = username - return app(req) - - app = set_user(app, username='bob') - """ if middle_func is None: return _UnboundMiddleware(cls, app, kw) if app is None: return _MiddlewareFactory(cls, middle_func, kw) - if hasattr(app, 'wsgi_app'): - app = app.wsgi_app - if 'reverse_args' in kw: - reverse_args = kw.pop('reverse_args') - else: - reverse_args = {} - app = cls.reverse(app, **reverse_args) - return cls(middle_func, middleware_wraps=app, **kw) - - @classmethod - def instantiator(cls, **kw): - """Give a class a descriptor that will instantiate the class - with a request object, then call the instance with no - arguments. - - Use this like:: - - class MyClass(object): - def __init__(self, req): - self.req = req - def __call__(self): - return Response('whatever') - wsgi_app = wsgify.instantiator() - - Then ``MyClass.wsgi_app`` will be an application that will - instantiate the class *for every request*. You can use settings - like ``add_urlvars`` with ``wsgify.instantiate``, and these will - effect how ``__call__`` is invoked. - """ - return _Instantiator(cls, kw) + return cls(middle_func, middleware_wraps=app, kwargs=kw) class _UnboundMiddleware(object): def __init__(self, wrapper_class, app, kw): @@ -254,7 +136,7 @@ class _UnboundMiddleware(object): self.wrapper_class.__name__, _format_args(args, self.kw)) def __call__(self, func, app=None): - if app is not None: + if app is None: app = self.app return self.wrapper_class.middleware(func, app=app, **self.kw) @@ -269,19 +151,9 @@ class _MiddlewareFactory(object): _format_args((self.middleware,), self.kw)) def __call__(self, app, **config): kw = self.kw.copy() - kw['add_keyword_args'] = config + kw.update(config) return self.wrapper_class.middleware(self.middleware, app, **kw) -class _Instantiator(object): - def __init__(self, wsgify_class, wsgify_kw): - self.wsgify_class = wsgify_class - self.wsgify_kw = wsgify_kw - def __get__(self, obj, type=None): - if obj is not None: - return self.wsgify_class(obj, **self.wsgify_kw) - else: - return self.wsgify_class(_instantiate_class=type, **self.wsgify_kw) - def _func_name(func): """Returns the string name of a function, or method, as best it can""" if isinstance(func, (type, ClassType)): @@ -327,29 +199,3 @@ def _format_args(args=(), kw=None, leading_comma=False, obj=None, names=None, de result = ', ' + result return result -class classapp(object): - def __init__(self, klass, construction_method=None, call_method='__call__'): - """This turns a class into a WSGI application.""" - self.klass = klass - self.construction_method = construction_method - self.call_method = call_method - def __repr__(self): - args = {} - if self.construction_method is not None: - args['construction_method'] = self.construction_method - if self.call_method != '__call__': - args['call_method'] = self.call_method - return 'classapp(%s%s)' % (_func_name(self.klass), - _format_args(obj=self, names='construction_method call_method', - defaults=dict(construction_method=None, call_method='__call__'), - leading_comma=True)) - @wsgify - def __call__(self, req): - if self.construction_method is None: - instantiator = self.klass - else: - instantiator = getattr(self.klass, self.construction_method) - instance = instantiator(req) - method = getattr(instance, self.call_method) - return method() - |
