diff options
| author | Chris McDonough <chrism@plope.com> | 2011-07-23 19:57:51 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-07-23 19:57:51 -0400 |
| commit | da797cbacecfa990f7225b7c3e198cb2e6b3391e (patch) | |
| tree | 0bc5d640acb8cbf4b84d13aeb3b5726c9c839a80 | |
| parent | 566a276a37fad8073206b46ec77b27498a126f02 (diff) | |
| parent | b723792bfc43dc3d4446837c48d78c9258697e6d (diff) | |
| download | pyramid-da797cbacecfa990f7225b7c3e198cb2e6b3391e.tar.gz pyramid-da797cbacecfa990f7225b7c3e198cb2e6b3391e.tar.bz2 pyramid-da797cbacecfa990f7225b7c3e198cb2e6b3391e.zip | |
Merge branch 'wrapviews'
| -rw-r--r-- | CHANGES.txt | 44 | ||||
| -rw-r--r-- | docs/api/request.rst | 2 | ||||
| -rw-r--r-- | pyramid/request.py | 62 | ||||
| -rw-r--r-- | pyramid/router.py | 9 | ||||
| -rw-r--r-- | pyramid/tests/test_request.py | 30 | ||||
| -rw-r--r-- | pyramid/tests/test_router.py | 91 |
6 files changed, 238 insertions, 0 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 57ab76e46..666b89b96 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,6 +8,50 @@ Features ``rendering_val``. This can be used to introspect the value returned by a view in a BeforeRender subscriber. +- New method: ``pyramid.request.Request.add_view_mapper``. A view wrapper is + used to wrap the found view callable before it is called by Pyramid's + router. This is a feature usually only used by framework extensions, to + provide, for example, view timing support. + + A view wrapper factory must be a callable which accepts three arguments: + ``view_callable``, ``request``, and ``exc``. It must return a view + callable. The view callable returned by the factory must implement the + ``context, request`` view callable calling convention. For example:: + + import time + + def wrapper_factory(view_callable, request, exc): + def wrapper(context, request): + start = time.time() + result = view_callable(context, request) + end = time.time() + request.view_timing = end - start + return result + return wrapper + + The ``view_callable`` argument to the factory will be the view callable + found by Pyramid via view lookup. The ``request`` argument to the factory + will be the current request. The ``exc`` argument to the factory will be + an Exception object if the found view is an exception view; it will be + ``None`` otherwise. + + View wrappers only last for the duration of a single request. You can add + such a factory for every request by using the + ``pyramid.events.NewRequest`` subscriber:: + + from pyramid.events import subscriber, NewRequest + + @subscriber(NewRequest) + def newrequest(event): + event.request.add_view_wrapper(wrapper_factory) + + If more than one view wrapper is registered during a single request, + a 'later' view wrapper factory will be called with the result of its + directly former view wrapper factory as its ``view_callable`` + argument; this chain will be returned to Pyramid as a single view + callable. + + 1.1 (2011-07-22) ================ diff --git a/docs/api/request.rst b/docs/api/request.rst index 404825d1b..58532bbd1 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -154,6 +154,8 @@ .. automethod:: add_finished_callback + .. automethod:: add_view_wrapper + .. automethod:: route_url .. automethod:: route_path diff --git a/pyramid/request.py b/pyramid/request.py index 8df204681..f84365dc5 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -203,6 +203,7 @@ class Request(BaseRequest, DeprecatedRequestMethods): implements(IRequest) response_callbacks = () finished_callbacks = () + view_wrappers = () exception = None matchdict = None matched_route = None @@ -212,6 +213,67 @@ class Request(BaseRequest, DeprecatedRequestMethods): """ Template context (for Pylons apps) """ return TemplateContext() + def add_view_wrapper(self, wrapper): + """ + Add a view wrapper factory. A view wrapper is used to wrap the found + view callable before it is called by Pyramid's router. This is a + feature usually only used by framework extensions, to provide, for + example, view timing support. + + A view wrapper factory must be a callable which accepts three + arguments: ``view_callable``, ``request``, and ``exc``. It must + return a view callable. The view callable returned by the factory + must implement the ``context, request`` view callable calling + convention. For example: + + .. code-block:: python + + import time + + def wrapper_factory(view_callable, request, exc): + def wrapper(context, request): + start = time.time() + result = view_callable(context, request) + end = time.time() + request.view_timing = end - start + return result + return wrapper + + The ``view_callable`` argument to the factory will be the view + callable found by Pyramid via :term:`view lookup`. The ``request`` + argument to the factory will be the current request. The ``exc`` + argument to the factory will be an Exception object if the found view + is a :term:`exception view`; it will be ``None`` otherwise. + + View wrappers only last for the duration of a single request. You + can add such a factory for every request by using the + :class:`pyramid.events.NewRequest` subscriber: + + .. code-block:: python + + from pyramid.events import subscriber, NewRequest + + @subscriber(NewRequest) + def newrequest(event): + event.request.add_view_wrapper(wrapper_factory) + + If more than one view wrapper is registered during a single request, + a 'later' view wrapper factory will be called with the result of its + directly former view wrapper factory as its ``view_callable`` + argument; this chain will be returned to Pyramid as a single view + callable. + """ + wrappers = self.view_wrappers + if not wrappers: + wrappers = [] + wrappers.append(wrapper) + self.view_wrappers = wrappers + + def _wrap_view(self, view, exc=None): + for wrapper in self.view_wrappers: + view = wrapper(view, self, exc) + return view + def add_response_callback(self, callback): """ Add a callback to the set of callbacks to be called by the diff --git a/pyramid/router.py b/pyramid/router.py index 0a92b5cd5..0294d8d75 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -158,6 +158,11 @@ class Router(object): msg = request.path_info raise HTTPNotFound(msg) else: + # if there were any view wrappers for the current + # request, use them to wrap the view + if request.view_wrappers: + view_callable = request._wrap_view(view_callable) + response = view_callable(context, request) # handle exceptions raised during root finding and view-exec @@ -178,6 +183,10 @@ class Router(object): if view_callable is None: raise + if request.view_wrappers: + view_callable = request._wrap_view(view_callable, + exc=why) + response = view_callable(why, request) has_listeners and notify(NewResponse(request, response)) diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 066aa9207..3c24c8f58 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -145,6 +145,36 @@ class TestRequest(unittest.TestCase): self.assertEqual(inst.called2, True) self.assertEqual(inst.finished_callbacks, []) + def test_add_view_wrapper(self): + inst = self._makeOne({}) + wrapper = object() + inst.add_view_wrapper(wrapper) + self.assertEqual(inst.view_wrappers, [wrapper]) + + def test_add_view_wrapper_wrappers_exist(self): + inst = self._makeOne({}) + inst.view_wrappers = [123] + wrapper = object() + inst.add_view_wrapper(wrapper) + self.assertEqual(inst.view_wrappers, [123, wrapper]) + + def test__wrap_view_no_wrappers(self): + inst = self._makeOne({}) + wrapped = inst._wrap_view(lambda *arg: 'OK') + self.assertEqual(wrapped(), 'OK') + + def test__wrap_view_with_wrappers_no_exc(self): + inst = self._makeOne({}) + def view(*arg): return 'OK' + def view_wrapper(_view, request, exc): + self.assertEqual(_view, view) + self.assertEqual(request, inst) + self.assertEqual(exc, '123') + return _view + inst.view_wrappers = [view_wrapper] + wrapped = inst._wrap_view(view, exc='123') + self.assertEqual(wrapped(), 'OK') + def test_resource_url(self): self._registerContextURL() inst = self._makeOne({}) diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 55eed50f5..1a70ca5a3 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -498,6 +498,97 @@ class TestRouter(unittest.TestCase): exc_raised(NotImplementedError, router, environ, start_response) self.assertEqual(environ['called_back'], True) + def test_call_request_has_view_wrappers(self): + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import INewRequest + wrappers = [] + class ViewWrapper(object): + def __call__(self, view, request, exc): + self.view = view + self.exc = exc + wrappers.append(self) + return self.wrap + def wrap(self, context, request): + return self.view(context, request) + wrapper1 = ViewWrapper() + wrapper2 = ViewWrapper() + def newrequest(event): + event.request.view_wrappers = [wrapper1, wrapper2] + self.registry.registerHandler(newrequest, (INewRequest,)) + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context, subpath=['']) + response = DummyResponse('200 OK') + response.app_iter = ['OK'] + def view(context, request): + return response + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + start_response = DummyStartResponse() + itera = router(environ, start_response) + wrapper1, wrapper2 = wrappers + self.assertEqual(wrapper1.view, view) + self.assertEqual(wrapper2.view, wrapper1.wrap) + self.assertEqual(wrapper1.exc, None) + self.assertEqual(wrapper2.exc, None) + self.assertEqual(itera, ['OK']) + + def test_call_request_has_view_wrappers_in_exception(self): + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import INewRequest + from pyramid.interfaces import IExceptionViewClassifier + wrappers = [] + class ViewWrapper(object): + def __init__(self): + self.views = [] + self.exc = [] + def __call__(self, view, request, exc): + self.views.append(view) + self.exc.append(exc) + wrappers.append(self) + return self.wrap + def wrap(self, context, request): + return self.views[-1](context, request) + wrapper1 = ViewWrapper() + wrapper2 = ViewWrapper() + def newrequest(event): + event.request.view_wrappers = [wrapper1, wrapper2] + self.registry.registerHandler(newrequest, (INewRequest,)) + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context, subpath=['']) + error = NotImplementedError() + def view(context, request): + raise error + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + exception_response = DummyResponse('404 Not Found') + exception_response.app_iter = ['Not Found'] + exception_view = DummyView(exception_response) + environ = self._makeEnviron() + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, NotImplementedError) + router = self._makeOne() + start_response = DummyStartResponse() + itera = router(environ, start_response) + wrapper1, wrapper2, wrapper3, wrapper4 = wrappers + self.assertEqual(wrapper1.views, [view, exception_view]) + self.assertEqual(wrapper2.views, [wrapper1.wrap, wrapper1.wrap]) + self.assertEqual(wrapper1.exc, [None, error]) + self.assertEqual(wrapper2.exc, [None, error]) + self.assertEqual(itera, ['Not Found']) + def test_call_request_factory_raises(self): # making sure finally doesnt barf when a request cannot be created environ = self._makeEnviron() |
