summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIan Bicking <ianb@colorstudy.com>2009-08-07 21:41:03 +0000
committerIan Bicking <ianb@colorstudy.com>2009-08-07 21:41:03 +0000
commit98a4883c24b590ff13e7e175c0bce88da4d11d8d (patch)
treeb75fb825a7a362b203cf05a4976635da8b805f4b
parentb6425dfe4535831d9189919ffb42462de9dc1ca2 (diff)
downloadwebob-ianb-decorator-experiment.tar.gz
Substantial refactor of webob.dec.wsgifyianb-decorator-experiment
-rw-r--r--tests/test_dec.txt133
-rw-r--r--webob/dec.py310
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()
-