Commits

Anonymous committed d03c499

Substantial refactor of webob.dec.wsgify

Comments (0)

Files changed (2)

tests/test_dec.txt

     ...         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)
 
     ... 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
     <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):
     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
 __all__ = ['wsgify', 'wsgiwrap']
 
 class wsgify(object):
+    RequestClass = webob.Request
 
-    # 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
 
+    def __get__(self, obj, type=None):
+        if hasattr(self.func, '__get__'):
+            return self.clone(self.func.__get__(obj, type))
+        else:
+            return self
+
+    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:
+                ## 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:
+            return self.func(req, *args, **kw)
+
+    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
 
-    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)
-        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)
-            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
-        else:
-            kw = {}
-        if self.add_keyword_args:
-            kw.update(self.add_keyword_args)
-        return self.call(*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)
-
     @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):
             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)
 
             _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)):
         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()
-