summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt23
-rw-r--r--docs/api/events.rst8
-rw-r--r--docs/api/interfaces.rst6
-rw-r--r--docs/narr/router.rst2
-rw-r--r--docs/narr/startup.rst4
-rw-r--r--repoze/bfg/configuration.py8
-rw-r--r--repoze/bfg/events.py114
-rw-r--r--repoze/bfg/interfaces.py46
-rw-r--r--repoze/bfg/router.py12
-rw-r--r--repoze/bfg/tests/test_configuration.py7
-rw-r--r--repoze/bfg/tests/test_events.py62
-rw-r--r--repoze/bfg/tests/test_router.py23
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):