summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2011-07-23 19:57:51 -0400
committerChris McDonough <chrism@plope.com>2011-07-23 19:57:51 -0400
commitda797cbacecfa990f7225b7c3e198cb2e6b3391e (patch)
tree0bc5d640acb8cbf4b84d13aeb3b5726c9c839a80
parent566a276a37fad8073206b46ec77b27498a126f02 (diff)
parentb723792bfc43dc3d4446837c48d78c9258697e6d (diff)
downloadpyramid-da797cbacecfa990f7225b7c3e198cb2e6b3391e.tar.gz
pyramid-da797cbacecfa990f7225b7c3e198cb2e6b3391e.tar.bz2
pyramid-da797cbacecfa990f7225b7c3e198cb2e6b3391e.zip
Merge branch 'wrapviews'
-rw-r--r--CHANGES.txt44
-rw-r--r--docs/api/request.rst2
-rw-r--r--pyramid/request.py62
-rw-r--r--pyramid/router.py9
-rw-r--r--pyramid/tests/test_request.py30
-rw-r--r--pyramid/tests/test_router.py91
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()