diff options
| -rw-r--r-- | CHANGES.txt | 23 | ||||
| -rw-r--r-- | docs/api/events.rst | 8 | ||||
| -rw-r--r-- | docs/api/interfaces.rst | 6 | ||||
| -rw-r--r-- | docs/narr/router.rst | 2 | ||||
| -rw-r--r-- | docs/narr/startup.rst | 4 | ||||
| -rw-r--r-- | repoze/bfg/configuration.py | 8 | ||||
| -rw-r--r-- | repoze/bfg/events.py | 114 | ||||
| -rw-r--r-- | repoze/bfg/interfaces.py | 46 | ||||
| -rw-r--r-- | repoze/bfg/router.py | 12 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_configuration.py | 7 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_events.py | 62 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_router.py | 23 |
12 files changed, 249 insertions, 66 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 4b716414f..6ea2893c4 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -24,6 +24,29 @@ Features - A new ZCML directive was added: ``default_permission``. +- The BFG router now emits an additional event unconditionally at the + end of request processing: + ``repoze.bfg.interfaces.IFinishedRequest``. This event is meant to + be used when it is necessary to perform unconditional cleanup after + request processing. See the ``repoze.bfg.events.FinishedRequest`` + class documentation for more information. + +Deprecations +------------ + +- The ``repoze.bfg.interfaces.IWSGIApplicationCreatedEvent`` event + interface was renamed to + ``repoze.bfg.interfaces.IApplicationCreated``. Likewise, the + ``repoze.bfg.events.WSGIApplicationCreatedEvent`` class was renamed + to ``repoze.bfg.events.ApplicationCreated``. The older aliases will + continue to work indefinitely. + +- The ``repoze.bfg.interfaces.IAfterTraversal`` event interface was + renamed to ``repoze.bfg.interfaces.IContextFound``. Likewise, the + ``repoze.bfg.events.AfterTraveral`` class was renamed to + ``repoze.bfg.events.ContextFound``. The older aliases will continue + to work indefinitely. + Documentation ------------- diff --git a/docs/api/events.rst b/docs/api/events.rst index cad199be3..53fdeda86 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -10,14 +10,20 @@ Functions .. autofunction:: subscriber +.. _event_types: + Event Types ~~~~~~~~~~~ +.. autoclass:: ApplicationCreated + .. autoclass:: NewRequest +.. autoclass:: ContextFound + .. autoclass:: NewResponse -.. autoclass:: WSGIApplicationCreatedEvent +.. autoclass:: FinishedRequest See :ref:`events_chapter` for more information about how to register code which subscribes to these events. diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index bda636940..974ab2ae9 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -8,13 +8,13 @@ Event-Related Interfaces ++++++++++++++++++++++++ - .. autointerface:: IAfterTraversal + .. autointerface:: IApplicationCreated .. autointerface:: INewRequest - .. autointerface:: INewResponse + .. autointerface:: IContextFound - .. autointerface:: IWSGIApplicationCreatedEvent + .. autointerface:: INewResponse Other Interfaces ++++++++++++++++ diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 025d70157..af8f057b0 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -67,7 +67,7 @@ processing? they can be accessed via e.g. ``request.context`` within :term:`view` code. -#. A :class:`repoze.bfg.interfaces.IAfterTraversal` :term:`event` is +#. A :class:`repoze.bfg.interfaces.IContextFound` :term:`event` is sent to any subscribers. #. :mod:`repoze.bfg` looks up a :term:`view` callable using the diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst index c3850fb4e..5b4365229 100644 --- a/docs/narr/startup.rst +++ b/docs/narr/startup.rst @@ -127,8 +127,8 @@ press ``return`` after running ``paster serve MyProject.ini``. by the configurator previously populated by ZCML. The router is a WSGI application. -#. A :class:`repoze.bfg.interfaces.WSGIApplicationCreatedEvent` event - is emitted (see :ref:`events_chapter` for more information about +#. A :class:`repoze.bfg.interfaces.IApplicationCreated` event is + emitted (see :ref:`events_chapter` for more information about events). #. Assuming there were no errors, the ``app`` function in diff --git a/repoze/bfg/configuration.py b/repoze/bfg/configuration.py index f8fdcca07..e4ac824ef 100644 --- a/repoze/bfg/configuration.py +++ b/repoze/bfg/configuration.py @@ -50,7 +50,7 @@ from repoze.bfg.renderers import RendererHelper from repoze.bfg.authorization import ACLAuthorizationPolicy from repoze.bfg.compat import all from repoze.bfg.compat import md5 -from repoze.bfg.events import WSGIApplicationCreatedEvent +from repoze.bfg.events import ApplicationCreated from repoze.bfg.exceptions import Forbidden from repoze.bfg.exceptions import NotFound from repoze.bfg.exceptions import PredicateMismatch @@ -594,16 +594,16 @@ class Configurator(object): def make_wsgi_app(self): """ Returns a :mod:`repoze.bfg` WSGI application representing the current configuration state and sends a - :class:`repoze.bfg.interfaces.IWSGIApplicationCreatedEvent` + :class:`repoze.bfg.interfaces.IApplicationCreated` event to all listeners.""" from repoze.bfg.router import Router # avoid circdep app = Router(self.registry) # We push the registry on to the stack here in case any code # that depends on the registry threadlocal APIs used in - # listeners subscribed to the WSGIApplicationCreatedEvent. + # listeners subscribed to the IApplicationCreated event. self.manager.push({'registry':self.registry, 'request':None}) try: - self.registry.notify(WSGIApplicationCreatedEvent(app)) + self.registry.notify(ApplicationCreated(app)) finally: self.manager.pop() return app diff --git a/repoze/bfg/events.py b/repoze/bfg/events.py index dd4f2eacc..301c28953 100644 --- a/repoze/bfg/events.py +++ b/repoze/bfg/events.py @@ -2,10 +2,11 @@ import venusian from zope.interface import implements -from repoze.bfg.interfaces import IAfterTraversal +from repoze.bfg.interfaces import IContextFound from repoze.bfg.interfaces import INewRequest from repoze.bfg.interfaces import INewResponse -from repoze.bfg.interfaces import IWSGIApplicationCreatedEvent +from repoze.bfg.interfaces import IApplicationCreated +from repoze.bfg.interfaces import IFinishedRequest class subscriber(object): """ Decorator activated via a :term:`scan` which treats the @@ -72,8 +73,8 @@ class subscriber(object): class NewRequest(object): """ An instance of this class is emitted as an :term:`event` whenever :mod:`repoze.bfg` begins to process a new request. The - instance has an attribute, ``request``, which is a :term:`request` - object. This class implements the + even instance has an attribute, ``request``, which is a + :term:`request` object. This event class implements the :class:`repoze.bfg.interfaces.INewRequest` interface.""" implements(INewRequest) def __init__(self, request): @@ -81,12 +82,23 @@ class NewRequest(object): class NewResponse(object): """ An instance of this class is emitted as an :term:`event` - whenever any :mod:`repoze.bfg` view returns a :term:`response`. + whenever any :mod:`repoze.bfg` :term:`view` or :term:`exception + view` returns a :term:`response`. The instance has two attributes:``request``, which is the request which caused the response, and ``response``, which is the response object returned by a view or renderer. + If the ``response`` was generated by an :term:`exception view`, + the request will have an attribute named ``exception``, which is + the exception object which caused the exception view to be + executed. If the response was generated by a 'normal' view, the + request will not have this attribute. + + This event will not be generated if a response cannot be created + due to an exception that is not caught by an exception view (no + response is created under this circumstace). + This class implements the :class:`repoze.bfg.interfaces.INewResponse` interface. @@ -104,30 +116,94 @@ class NewResponse(object): self.request = request self.response = response -class AfterTraversal(object): - implements(IAfterTraversal) +class ContextFound(object): + implements(IContextFound) """ An instance of this class is emitted as an :term:`event` after - the :mod:`repoze.bfg` :term:`router` performs traversal but before - any view code is executed. The instance has an attribute, - ``request``, which is the request object generated by - :mod:`repoze.bfg`. Notably, the request object will have an - attribute named ``context``, which is the context that will be - provided to the view which will eventually be called, as well as - other attributes defined by the traverser. This class implements - the :class:`repoze.bfg.interfaces.IAfterTraversal` interface.""" + the :mod:`repoze.bfg` :term:`router` finds a :term:`context` + object (after it performs traversal) but before any view code is + executed. The instance has an attribute, ``request``, which is + the request object generated by :mod:`repoze.bfg`. + + Notably, the request object will have an attribute named + ``context``, which is the context that will be provided to the + view which will eventually be called, as well as other attributes + attached by context-finding code. + + This class implements the + :class:`repoze.bfg.interfaces.IContextFound` interface. + + .. note:: As of :mod:`repoze.bfg` 1.3, for backwards compatibility + purposes, this event may also be imported as + :class:`repoze.bfg.events.AfterTraversal`. + """ def __init__(self, request): self.request = request + +AfterTraversal = ContextFound # b/c as of 1.3 -class WSGIApplicationCreatedEvent(object): +class ApplicationCreated(object): """ An instance of this class is emitted as an :term:`event` when the :meth:`repoze.bfg.configuration.Configurator.make_wsgi_app` is called. The instance has an attribute, ``app``, which is an instance of the :term:`router` that will handle WSGI requests. This class implements the - :class:`repoze.bfg.interfaces.IWSGIApplicationCreatedEvent` - interface.""" - implements(IWSGIApplicationCreatedEvent) + :class:`repoze.bfg.interfaces.IApplicationCreated` interface. + + .. note:: For backwards compatibility purposes, this class can + also be imported as + :class:`repoze.bfg.events.WSGIApplicationCreatedEvent`. This + was the name of the event class before :mod:`repoze.bfg` 1.3. + + """ + implements(IApplicationCreated) def __init__(self, app): self.app = app self.object = app +WSGIApplicationCreatedEvent = ApplicationCreated # b/c (as of 1.3) + +class FinishedRequest(object): + """ + This :term:`event` is sent after all request processing is + finished. + + An event of this type is emitted unconditionally at the end of + request processing, even when an unhandled exception occurs. This + is in contrast to the :class:`repoze.bfg.interfaces.INewResponse` + event, which cannot be emitted when, due to an unhandled + exception, a response object cannot not be created . The + :class:`repoze.bfg.events.FinishedRequest` event will even be sent + when a request cannot not be created due to an error in request + factory code: in such a case, the ``request`` attribute of the + event will be ``None``. + + Mutating the attached ``request`` object in a subscriber to this + event will have no effect, because, when this event is emitted, + there is no further request or response processing to be done. It + is purely an informational event, which can be hooked to do + 'finally:'-style tear-down at the end of each request. + + Instances of this event have an attribute, ``request``, which is + the :term:`request` object (or, in extremely rare cases might be + ``None``, when a request object cannot be created due to a bug in + a request factory) . + + Because this event happens unconditionally, the set of attributes + possessed by an attached ``request`` object are indeterminate. At + very least, if the request is not ``None``, it will have a + ``registry`` attribute. However, if an exception was thrown + before this event is broadcast, it may not have other + :mod:`repoze.bfg` -specific attributes such as ``subpath``, + ``root`, ``traversed``, etc. + + Exceptions raised by subscribers of this event are unhandled. + + This class implements the + :class:`repoze.bfg.interfaces.IFinishedRequest` interface. + + .. note:: This event type is new as of :mod:`repoze.bfg` 1.3. + """ + implements(IFinishedRequest) + def __init__(self, request): + self.request = request + diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py index 44fe84c18..8730a5294 100644 --- a/repoze/bfg/interfaces.py +++ b/repoze/bfg/interfaces.py @@ -3,28 +3,60 @@ from zope.interface import Interface # public API interfaces -class IAfterTraversal(Interface): - """ An event type that is emitted after :mod:`repoze.bfg` - completes traversal but before it calls any view code.""" +class IContextFound(Interface): + """ An event type that is emitted after :mod:`repoze.bfg` finds a + :term:`context` object but before it calls any view code. See the + documentation attached to :class:`repoze.bfg.events.ContextFound` + for more information. + + .. note:: For backwards compatibility with versions of + :mod:`repoze.bfg` before 1.3, this event interface can also be + imported as :class:`repoze.bfg.interfaces.IAfterTraversal`. + """ request = Attribute('The request object') +IAfterTraversal = IContextFound + class INewRequest(Interface): """ An event type that is emitted whenever :mod:`repoze.bfg` - begins to process a new request""" + begins to process a new request. See the documentation attached + to :class:`repoze.bfg.events.NewRequest` for more information.""" request = Attribute('The request object') class INewResponse(Interface): """ An event type that is emitted whenever any :mod:`repoze.bfg` - view returns a response.""" + view returns a response. See the + documentation attached to :class:`repoze.bfg.events.NewResponse` + for more information.""" request = Attribute('The request object') response = Attribute('The response object') -class IWSGIApplicationCreatedEvent(Interface): +class IApplicationCreated(Interface): """ Event issued when the :meth:`repoze.bfg.configuration.Configurator.make_wsgi_app` method - is called.""" + is called. See the documentation attached to + :class:`repoze.bfg.events.ApplicationCreated` for more + information. + + .. note:: For backwards compatibility with :mod:`repoze.bfg` + versions before 1.3, this interface can also be imported as + :class:`repoze.bfg.interfaces.IWSGIApplicationCreatedEvent. + """ app = Attribute(u"Published application") +class IFinishedRequest(Interface): + """ + This :term:`event` is sent after all request processing is + finished. See the + documentation attached to :class:`repoze.bfg.events.FinishedRequest` + for more information. + + .. note:: This event type is new as of :mod:`repoze.bfg` 1.3. + """ + request = Attribute('The request object') + +IWSGIApplicationCreatedEvent = IApplicationCreated # b /c + class IResponse(Interface): # not an API status = Attribute('WSGI status code of response') headerlist = Attribute('List of response headers') diff --git a/repoze/bfg/router.py b/repoze/bfg/router.py index f976dfea2..fb5120d2b 100644 --- a/repoze/bfg/router.py +++ b/repoze/bfg/router.py @@ -15,9 +15,10 @@ from repoze.bfg.interfaces import IView from repoze.bfg.interfaces import IViewClassifier from repoze.bfg.configuration import make_app # b/c import -from repoze.bfg.events import AfterTraversal +from repoze.bfg.events import ContextFound from repoze.bfg.events import NewRequest from repoze.bfg.events import NewResponse +from repoze.bfg.events import FinishedRequest from repoze.bfg.exceptions import NotFound from repoze.bfg.request import Request from repoze.bfg.threadlocal import manager @@ -59,6 +60,7 @@ class Router(object): manager = self.threadlocal_manager threadlocals = {'registry':registry, 'request':None} manager.push(threadlocals) + request = None try: # create the request @@ -68,7 +70,6 @@ class Router(object): attrs = request.__dict__ attrs['registry'] = registry has_listeners and registry.notify(NewRequest(request)) - request_iface = IRequest try: @@ -101,7 +102,7 @@ class Router(object): tdict['traversed'], tdict['virtual_root'], tdict['virtual_root_path']) attrs.update(tdict) - has_listeners and registry.notify(AfterTraversal(request)) + has_listeners and registry.notify(ContextFound(request)) context_iface = providedBy(context) view_callable = adapters.lookup( (IViewClassifier, request_iface, context_iface), @@ -164,5 +165,8 @@ class Router(object): return app_iter finally: - manager.pop() + try: + has_listeners and registry.notify(FinishedRequest(request)) + finally: + manager.pop() diff --git a/repoze/bfg/tests/test_configuration.py b/repoze/bfg/tests/test_configuration.py index 943e6b832..b095c5c4c 100644 --- a/repoze/bfg/tests/test_configuration.py +++ b/repoze/bfg/tests/test_configuration.py @@ -563,11 +563,10 @@ class ConfiguratorTests(unittest.TestCase): def test_make_wsgi_app(self): from repoze.bfg.router import Router - from repoze.bfg.interfaces import IWSGIApplicationCreatedEvent + from repoze.bfg.interfaces import IApplicationCreated manager = DummyThreadLocalManager() config = self._makeOne() - subscriber = self._registerEventListener(config, - IWSGIApplicationCreatedEvent) + subscriber = self._registerEventListener(config, IApplicationCreated) config.manager = manager app = config.make_wsgi_app() self.assertEqual(app.__class__, Router) @@ -575,7 +574,7 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(manager.pushed['request'], None) self.failUnless(manager.popped) self.assertEqual(len(subscriber), 1) - self.failUnless(IWSGIApplicationCreatedEvent.providedBy(subscriber[0])) + self.failUnless(IApplicationCreated.providedBy(subscriber[0])) def test_load_zcml_default(self): import repoze.bfg.tests.fixtureapp diff --git a/repoze/bfg/tests/test_events.py b/repoze/bfg/tests/test_events.py index c122f4744..d097ca0f4 100644 --- a/repoze/bfg/tests/test_events.py +++ b/repoze/bfg/tests/test_events.py @@ -55,45 +55,69 @@ class NewResponseEventTests(unittest.TestCase): self.assertEqual(inst.request, request) self.assertEqual(inst.response, response) -class WSGIAppEventTests(unittest.TestCase): - def test_object_implements(self): +class ApplicationCreatedEventTests(unittest.TestCase): + def test_alias_object_implements(self): from repoze.bfg.events import WSGIApplicationCreatedEvent event = WSGIApplicationCreatedEvent(object()) from repoze.bfg.interfaces import IWSGIApplicationCreatedEvent + from repoze.bfg.interfaces import IApplicationCreated from zope.interface.verify import verifyObject verifyObject(IWSGIApplicationCreatedEvent, event) + verifyObject(IApplicationCreated, event) - def test_class_implements(self): + def test_alias_class_implements(self): from repoze.bfg.events import WSGIApplicationCreatedEvent from repoze.bfg.interfaces import IWSGIApplicationCreatedEvent + from repoze.bfg.interfaces import IApplicationCreated from zope.interface.verify import verifyClass verifyClass(IWSGIApplicationCreatedEvent, WSGIApplicationCreatedEvent) + verifyClass(IApplicationCreated, WSGIApplicationCreatedEvent) -class AfterTraversalEventTests(unittest.TestCase): - def _getTargetClass(self): - from repoze.bfg.events import AfterTraversal - return AfterTraversal - - def _makeOne(self, request): - return self._getTargetClass()(request) + def test_object_implements(self): + from repoze.bfg.events import ApplicationCreated + event = ApplicationCreated(object()) + from repoze.bfg.interfaces import IApplicationCreated + from zope.interface.verify import verifyObject + verifyObject(IApplicationCreated, event) def test_class_implements(self): - from repoze.bfg.interfaces import IAfterTraversal + from repoze.bfg.events import ApplicationCreated + from repoze.bfg.interfaces import IApplicationCreated from zope.interface.verify import verifyClass - klass = self._getTargetClass() - verifyClass(IAfterTraversal, klass) - - def test_instance_implements(self): + verifyClass(IApplicationCreated, ApplicationCreated) + +class ContextFoundEventTests(unittest.TestCase): + def test_alias_class_implements(self): + from zope.interface.verify import verifyClass + from repoze.bfg.events import AfterTraversal from repoze.bfg.interfaces import IAfterTraversal + from repoze.bfg.interfaces import IContextFound + verifyClass(IAfterTraversal, AfterTraversal) + verifyClass(IContextFound, AfterTraversal) + + def test_alias_instance_implements(self): from zope.interface.verify import verifyObject + from repoze.bfg.events import AfterTraversal + from repoze.bfg.interfaces import IAfterTraversal + from repoze.bfg.interfaces import IContextFound request = DummyRequest() - inst = self._makeOne(request) + inst = AfterTraversal(request) verifyObject(IAfterTraversal, inst) + verifyObject(IContextFound, inst) - def test_ctor(self): + def test_class_implements(self): + from zope.interface.verify import verifyClass + from repoze.bfg.events import ContextFound + from repoze.bfg.interfaces import IContextFound + verifyClass(IContextFound, ContextFound) + + def test_instance_implements(self): + from zope.interface.verify import verifyObject + from repoze.bfg.events import ContextFound + from repoze.bfg.interfaces import IContextFound request = DummyRequest() - inst = self._makeOne(request) - self.assertEqual(inst.request, request) + inst = ContextFound(request) + verifyObject(IContextFound, inst) class TestSubscriber(unittest.TestCase): def setUp(self): diff --git a/repoze/bfg/tests/test_router.py b/repoze/bfg/tests/test_router.py index c243d739e..fafe88d4a 100644 --- a/repoze/bfg/tests/test_router.py +++ b/repoze/bfg/tests/test_router.py @@ -126,6 +126,21 @@ class TestRouter(unittest.TestCase): router = self._makeOne() self.assertEqual(router.request_factory, DummyRequestFactory) + def test_call_request_factory_raises_finished_request_catches(self): + from repoze.bfg.interfaces import IFinishedRequest + finished_events = self._registerEventListener(IFinishedRequest) + environ = self._makeEnviron() + logger = self._registerLogger() + 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) + self.assertEqual(len(logger.messages), 0) + self.assertEqual(len(finished_events), 1) + self.assertEqual(finished_events[0].request, None) + def test_call_traverser_default(self): from repoze.bfg.exceptions import NotFound environ = self._makeEnviron() @@ -399,7 +414,8 @@ class TestRouter(unittest.TestCase): def test_call_eventsends(self): from repoze.bfg.interfaces import INewRequest from repoze.bfg.interfaces import INewResponse - from repoze.bfg.interfaces import IAfterTraversal + from repoze.bfg.interfaces import IContextFound + from repoze.bfg.interfaces import IFinishedRequest from repoze.bfg.interfaces import IViewClassifier context = DummyContext() self._registerTraverserFactory(context) @@ -409,8 +425,9 @@ class TestRouter(unittest.TestCase): environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, None, None) request_events = self._registerEventListener(INewRequest) - aftertraversal_events = self._registerEventListener(IAfterTraversal) + aftertraversal_events = self._registerEventListener(IContextFound) response_events = self._registerEventListener(INewResponse) + finished_events = self._registerEventListener(IFinishedRequest) router = self._makeOne() start_response = DummyStartResponse() result = router(environ, start_response) @@ -420,6 +437,8 @@ class TestRouter(unittest.TestCase): self.assertEqual(aftertraversal_events[0].request.environ, environ) self.assertEqual(len(response_events), 1) self.assertEqual(response_events[0].response, response) + self.assertEqual(len(finished_events), 1) + self.assertEqual(finished_events[0].request.environ, environ) self.assertEqual(result, response.app_iter) def test_call_pushes_and_pops_threadlocal_manager(self): |
