diff options
Diffstat (limited to 'repoze')
| -rw-r--r-- | repoze/bfg/request.py | 55 | ||||
| -rw-r--r-- | repoze/bfg/router.py | 6 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_request.py | 22 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_router.py | 56 |
4 files changed, 137 insertions, 2 deletions
diff --git a/repoze/bfg/request.py b/repoze/bfg/request.py index 6318faf5a..6e5417d9a 100644 --- a/repoze/bfg/request.py +++ b/repoze/bfg/request.py @@ -39,6 +39,7 @@ class Request(WebobRequest): """ implements(IRequest) response_callbacks = () + finished_callbacks = () default_charset = 'utf-8' def add_response_callback(self, callback): @@ -84,6 +85,60 @@ class Request(WebobRequest): callback(self, response) self.response_callbacks = () + def add_finished_callback(self, callback): + """ + Add a callback to the set of callbacks to be called + unconditionally by the :term:`router` at the very end of + request processing. + + ``callback`` is a callable which accepts a single positional + parameter: ``request``. For example: + + .. code-block:: python + :linenos: + + import transaction + + def commit_callback(request): + '''commit or abort the transaction associated with request''' + if hasattr(request, 'exception'): + transaction.abort() + else: + transaction.commit() + request.add_finished_callback(commit_callback) + + Finished callbacks are called in the order they're added ( + first- to most-recently- added). Finished callbacks (unlike + response callbacks) are *always* called, even if an exception + happens in application code that prevents a response from + being generated. + + The set of finished callbacks associated with a request are + called *very late* in the processing of that request; they are + essentially the last thing called by the :term:`router`. They + are called after response processing has already occurred in a + top-level ``finally:`` block within the router request + processing code. As a result, mutations performed to the + ``request`` provided to a finished callback will have no + meaningful effect, because response processing will have + already occurred, and the request's scope will expire almost + immediately after all finished callbacks have been processed. + + Errors raised by finished callbacks are not handled specially. + They will be propagated to the caller of the :mod:`repoze.bfg` + router application. """ + + callbacks = self.finished_callbacks + if not callbacks: + callbacks = [] + callbacks.append(callback) + self.finished_callbacks = callbacks + + def _process_finished_callbacks(self): + for callback in self.finished_callbacks: + callback(self) + self.finished_callbacks = () + # override default WebOb "environ['adhoc_attr']" mutation behavior __getattr__ = object.__getattribute__ __setattr__ = object.__setattr__ diff --git a/repoze/bfg/router.py b/repoze/bfg/router.py index 4a818d097..6532beec4 100644 --- a/repoze/bfg/router.py +++ b/repoze/bfg/router.py @@ -164,5 +164,9 @@ class Router(object): return app_iter finally: - manager.pop() + try: + if request is not None and request.finished_callbacks: + request._process_finished_callbacks() + finally: + manager.pop() diff --git a/repoze/bfg/tests/test_request.py b/repoze/bfg/tests/test_request.py index 0248e10be..d8a3c09fb 100644 --- a/repoze/bfg/tests/test_request.py +++ b/repoze/bfg/tests/test_request.py @@ -184,6 +184,28 @@ class TestRequest(unittest.TestCase): self.assertEqual(response.called2, True) self.assertEqual(inst.response_callbacks, ()) + def test_add_finished_callback(self): + inst = self._makeOne({}) + self.assertEqual(inst.finished_callbacks, ()) + def callback(request): + """ """ + inst.add_finished_callback(callback) + self.assertEqual(inst.finished_callbacks, [callback]) + inst.add_finished_callback(callback) + self.assertEqual(inst.finished_callbacks, [callback, callback]) + + def test__process_finished_callbacks(self): + inst = self._makeOne({}) + def callback1(request): + request.called1 = True + def callback2(request): + request.called2 = True + inst.finished_callbacks = [callback1, callback2] + inst._process_finished_callbacks() + self.assertEqual(inst.called1, True) + self.assertEqual(inst.called2, True) + self.assertEqual(inst.finished_callbacks, ()) + class Test_route_request_iface(unittest.TestCase): def _callFUT(self, name): from repoze.bfg.request import route_request_iface diff --git a/repoze/bfg/tests/test_router.py b/repoze/bfg/tests/test_router.py index fbe11968a..199602a96 100644 --- a/repoze/bfg/tests/test_router.py +++ b/repoze/bfg/tests/test_router.py @@ -383,7 +383,6 @@ class TestRouter(unittest.TestCase): directlyProvides(context, IContext) self._registerTraverserFactory(context, subpath=['']) response = DummyResponse('200 OK') - response.headerlist = [('a', 1)] def view(context, request): def callback(request, response): response.called_back = True @@ -396,6 +395,61 @@ class TestRouter(unittest.TestCase): router(environ, start_response) self.assertEqual(response.called_back, True) + def test_call_request_has_finished_callbacks_when_view_succeeds(self): + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IViewClassifier + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context, subpath=['']) + response = DummyResponse('200 OK') + def view(context, request): + def callback(request): + request.environ['called_back'] = True + request.finished_callbacks = [callback] + return response + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + start_response = DummyStartResponse() + router(environ, start_response) + self.assertEqual(environ['called_back'], True) + + def test_call_request_has_finished_callbacks_when_view_raises(self): + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IViewClassifier + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context, subpath=['']) + def view(context, request): + def callback(request): + request.environ['called_back'] = True + request.finished_callbacks = [callback] + raise NotImplementedError + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + start_response = DummyStartResponse() + exc_raised(NotImplementedError, router, environ, start_response) + self.assertEqual(environ['called_back'], True) + + def test_call_request_factory_raises(self): + # making sure finally doesnt barf when a request cannot be created + environ = self._makeEnviron() + router = self._makeOne() + def dummy_request_factory(environ): + raise NotImplementedError + router.request_factory = dummy_request_factory + start_response = DummyStartResponse() + exc_raised(NotImplementedError, router, environ, start_response) + def test_call_eventsends(self): from repoze.bfg.interfaces import INewRequest from repoze.bfg.interfaces import INewResponse |
