Michael Merickel
2016-04-13 b1527e793bc101327050370c17e1be698f7192ff
Merge pull request #2469 from Pylons/feature/BeforeTraversal

Feature: BeforeTraversal
9 files modified
121 ■■■■ changed files
CHANGES.txt 5 ●●●●● patch | view | raw | blame | history
docs/api/events.rst 2 ●●●●● patch | view | raw | blame | history
docs/api/interfaces.rst 3 ●●●●● patch | view | raw | blame | history
docs/narr/router.rst 25 ●●●●● patch | view | raw | blame | history
pyramid/events.py 24 ●●●●● patch | view | raw | blame | history
pyramid/interfaces.py 8 ●●●●● patch | view | raw | blame | history
pyramid/router.py 15 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_events.py 35 ●●●● patch | view | raw | blame | history
pyramid/tests/test_router.py 4 ●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -1,6 +1,11 @@
unreleased
==========
- A new event and interface (BeforeTraversal) has been introduced that will
  notify listeners before traversal starts in the router. See
  https://github.com/Pylons/pyramid/pull/2469 and
  https://github.com/Pylons/pyramid/pull/1876
- Python 2.6 is no longer supported by Pyramid. See
  https://github.com/Pylons/pyramid/issues/2368
docs/api/events.rst
@@ -21,6 +21,8 @@
.. autoclass:: ContextFound
.. autoclass:: BeforeTraversal
.. autoclass:: NewResponse
.. autoclass:: BeforeRender
docs/api/interfaces.rst
@@ -17,6 +17,9 @@
  .. autointerface:: IContextFound
     :members:
  .. autointerface:: IBeforeTraversal
     :members:
  .. autointerface:: INewResponse
     :members:
docs/narr/router.rst
@@ -41,19 +41,24 @@
   user-defined :term:`route` matches the current WSGI environment.  The
   :term:`router` passes the request as an argument to the mapper.
#. If any route matches, the route mapper adds attributes to the request:
   ``matchdict`` and ``matched_route`` attributes are added to the request
   object.  The former contains a dictionary representing the matched dynamic
   elements of the request's ``PATH_INFO`` value, and the latter contains the
#. If any route matches, the route mapper adds the attributes ``matchdict``
   and ``matched_route`` to the request object. The former contains a
   dictionary representing the matched dynamic elements of the request's
   ``PATH_INFO`` value, and the latter contains the
   :class:`~pyramid.interfaces.IRoute` object representing the route which
   matched.  The root object associated with the route found is also generated:
   if the :term:`route configuration` which matched has an associated
   ``factory`` argument, this factory is used to generate the root object,
   otherwise a default :term:`root factory` is used.
   matched.
#. If a route match was *not* found, and a ``root_factory`` argument was passed
#. A :class:`~pyramid.events.BeforeTraversal` :term:`event` is sent to any
   subscribers.
#. Continuing, if any route matches, the root object associated with the found
   route is generated. If the :term:`route configuration` which matched has an
   associated ``factory`` argument, then this factory is used to generate the
   root object; otherwise a default :term:`root factory` is used.
   However, if no route matches, and if a ``root_factory`` argument was passed
   to the :term:`Configurator` constructor, that callable is used to generate
   the root object.  If the ``root_factory`` argument passed to the
   the root object. If the ``root_factory`` argument passed to the
   Configurator constructor was ``None``, a default root factory is used to
   generate a root object.
pyramid/events.py
@@ -11,6 +11,7 @@
    INewResponse,
    IApplicationCreated,
    IBeforeRender,
    IBeforeTraversal,
    )
class subscriber(object):
@@ -129,6 +130,26 @@
        self.request = request
        self.response = response
@implementer(IBeforeTraversal)
class BeforeTraversal(object):
    """
    An instance of this class is emitted as an :term:`event` after the
    :app:`Pyramid` :term:`router` has attempted to find a :term:`route` object
    but before any traversal or view code is executed. The instance has an
    attribute, ``request``, which is the request object generated by
    :app:`Pyramid`.
    Notably, the request object **may** have an attribute named
    ``matched_route``, which is the matched route if found. If no route
    matched, this attribute is not available.
    This class implements the :class:`pyramid.interfaces.IBeforeTraversal`
    interface.
    """
    def __init__(self, request):
        self.request = request
@implementer(IContextFound)
class ContextFound(object):
    """ An instance of this class is emitted as an :term:`event` after
@@ -156,7 +177,7 @@
AfterTraversal = ContextFound # b/c as of 1.0
@implementer(IApplicationCreated)
class ApplicationCreated(object):
class ApplicationCreated(object):
    """ An instance of this class is emitted as an :term:`event` when
    the :meth:`pyramid.config.Configurator.make_wsgi_app` is
    called.  The instance has an attribute, ``app``, which is an
@@ -242,5 +263,4 @@
    def __init__(self, system, rendering_val=None):
        dict.__init__(self, system)
        self.rendering_val = rendering_val
pyramid/interfaces.py
@@ -25,6 +25,14 @@
IAfterTraversal = IContextFound
class IBeforeTraversal(Interface):
    """
    An event type that is emitted after :app:`Pyramid` attempted to find a
    route but before it calls any traversal or view code. See the documentation
    attached to :class:`pyramid.events.Routefound` for more information.
    """
    request = Attribute('The request object')
class INewRequest(Interface):
    """ An event type that is emitted whenever :app:`Pyramid`
    begins to process a new request.  See the documentation attached
pyramid/router.py
@@ -20,6 +20,7 @@
    ContextFound,
    NewRequest,
    NewResponse,
    BeforeTraversal,
    )
from pyramid.httpexceptions import HTTPNotFound
@@ -114,10 +115,19 @@
                root_factory = route.factory or self.root_factory
        # Notify anyone listening that we are about to start traversal
        #
        # Notify before creating root_factory in case we want to do something
        # special on a route we may have matched. See
        # https://github.com/Pylons/pyramid/pull/1876 for ideas of what is
        # possible.
        has_listeners and notify(BeforeTraversal(request))
        # Create the root factory
        root = root_factory(request)
        attrs['root'] = root
        # find a context
        # We are about to traverse and find a context
        traverser = adapters.queryAdapter(root, ITraverser)
        if traverser is None:
            traverser = ResourceTreeTraverser(root)
@@ -133,6 +143,9 @@
            )
        attrs.update(tdict)
        # Notify anyone listening that we have a context and traversal is
        # complete
        has_listeners and notify(ContextFound(request))
        # find a view callable
pyramid/tests/test_events.py
@@ -14,7 +14,7 @@
        from zope.interface.verify import verifyClass
        klass = self._getTargetClass()
        verifyClass(INewRequest, klass)
    def test_instance_conforms_to_INewRequest(self):
        from pyramid.interfaces import INewRequest
        from zope.interface.verify import verifyObject
@@ -40,7 +40,7 @@
        from zope.interface.verify import verifyClass
        klass = self._getTargetClass()
        verifyClass(INewResponse, klass)
    def test_instance_conforms_to_INewResponse(self):
        from pyramid.interfaces import INewResponse
        from zope.interface.verify import verifyObject
@@ -103,7 +103,7 @@
        from zope.interface.verify import verifyClass
        from pyramid.interfaces import IContextFound
        verifyClass(IContextFound, self._getTargetClass())
    def test_instance_conforms_to_IContextFound(self):
        from zope.interface.verify import verifyObject
        from pyramid.interfaces import IContextFound
@@ -118,11 +118,32 @@
        from zope.interface.verify import verifyClass
        from pyramid.interfaces import IAfterTraversal
        verifyClass(IAfterTraversal, self._getTargetClass())
    def test_instance_conforms_to_IAfterTraversal(self):
        from zope.interface.verify import verifyObject
        from pyramid.interfaces import IAfterTraversal
        verifyObject(IAfterTraversal, self._makeOne())
class BeforeTraversalEventTests(unittest.TestCase):
    def _getTargetClass(self):
        from pyramid.events import BeforeTraversal
        return BeforeTraversal
    def _makeOne(self, request=None):
        if request is None:
            request = DummyRequest()
        return self._getTargetClass()(request)
    def test_class_conforms_to_IBeforeTraversal(self):
        from zope.interface.verify import verifyClass
        from pyramid.interfaces import IBeforeTraversal
        verifyClass(IBeforeTraversal, self._getTargetClass())
    def test_instance_conforms_to_IBeforeTraversal(self):
        from zope.interface.verify import verifyObject
        from pyramid.interfaces import IBeforeTraversal
        verifyObject(IBeforeTraversal, self._makeOne())
class TestSubscriber(unittest.TestCase):
    def setUp(self):
@@ -221,7 +242,7 @@
        result = event.setdefault('a', 1)
        self.assertEqual(result, 1)
        self.assertEqual(event, {'a':1})
    def test_setdefault_success(self):
        event = self._makeOne({})
        event['a'] = 1
@@ -282,7 +303,7 @@
class DummyRegistry(object):
    pass
class DummyVenusian(object):
    def __init__(self):
        self.attached = []
@@ -292,7 +313,7 @@
class Dummy:
    pass
class DummyRequest:
    pass
pyramid/tests/test_router.py
@@ -591,6 +591,7 @@
    def test_call_eventsends(self):
        from pyramid.interfaces import INewRequest
        from pyramid.interfaces import INewResponse
        from pyramid.interfaces import IBeforeTraversal
        from pyramid.interfaces import IContextFound
        from pyramid.interfaces import IViewClassifier
        context = DummyContext()
@@ -601,6 +602,7 @@
        environ = self._makeEnviron()
        self._registerView(view, '', IViewClassifier, None, None)
        request_events = self._registerEventListener(INewRequest)
        beforetraversal_events = self._registerEventListener(IBeforeTraversal)
        context_found_events = self._registerEventListener(IContextFound)
        response_events = self._registerEventListener(INewResponse)
        router = self._makeOne()
@@ -608,6 +610,8 @@
        result = router(environ, start_response)
        self.assertEqual(len(request_events), 1)
        self.assertEqual(request_events[0].request.environ, environ)
        self.assertEqual(len(beforetraversal_events), 1)
        self.assertEqual(beforetraversal_events[0].request.environ, environ)
        self.assertEqual(len(context_found_events), 1)
        self.assertEqual(context_found_events[0].request.environ, environ)
        self.assertEqual(context_found_events[0].request.context, context)