summaryrefslogtreecommitdiff
path: root/repoze
diff options
context:
space:
mode:
Diffstat (limited to 'repoze')
-rw-r--r--repoze/bfg/request.py55
-rw-r--r--repoze/bfg/router.py6
-rw-r--r--repoze/bfg/tests/test_request.py22
-rw-r--r--repoze/bfg/tests/test_router.py56
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