From 748aad47f90136b151be13f477ed6af1caed0493 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 19:09:08 -0500 Subject: - Add ``pyramid.config.Configurator.set_traverser`` API method. See the Hooks narrative documentation section entitled "Changing the Traverser" for more information. This is not a new feature, it just provides an API for adding a traverser without needing to use the ZCA API. --- CHANGES.txt | 5 ++ TODO.txt | 3 +- docs/api/config.rst | 2 + docs/narr/hooks.rst | 14 +++--- docs/narr/introspector.rst | 15 ++++++ pyramid/config/factories.py | 74 +++++++++++++++++++++++++++++ pyramid/tests/test_config/test_factories.py | 47 ++++++++++++++++++ 7 files changed, 150 insertions(+), 10 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index a69313a8a..0bb2c164b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -23,6 +23,11 @@ Features something like "AttributeError: 'NoneType' object has no attribute 'rfind'". +- Add ``pyramid.config.Configurator.set_traverser`` API method. See the + Hooks narrative documentation section entitled "Changing the Traverser" for + more information. This is not a new feature, it just provides an API for + adding a traverser without needing to use the ZCA API. + Documentation ------------- diff --git a/TODO.txt b/TODO.txt index 75dd2e0c4..dfce2e2fb 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,8 +4,7 @@ Pyramid TODOs Nice-to-Have ------------ -- Add set_traverser configurator method and set_resource_url_generator - method. +- Add set_resource_url_generator method. - Put includes in development.ini on separate lines and fix project.rst to tell people to comment out only the debugtoolbar include when they want to diff --git a/docs/api/config.rst b/docs/api/config.rst index d16930cc0..3c5ee563a 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -94,6 +94,8 @@ .. automethod:: set_notfound_view + .. automethod:: set_traverser + .. automethod:: set_renderer_globals_factory(factory) .. attribute:: introspectable diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 350b5734d..076f9fa5c 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -406,11 +406,10 @@ via configuration. .. code-block:: python :linenos: - from pyramid.interfaces import ITraverser - from zope.interface import Interface + from pyramid.config import Configurator from myapp.traversal import Traverser - - config.registry.registerAdapter(Traverser, (Interface,), ITraverser) + config = Configurator() + config.set_traverser(Traverser) In the example above, ``myapp.traversal.Traverser`` is assumed to be a class that implements the following interface: @@ -456,12 +455,11 @@ used. Otherwise, the default traverser would be used. For example: .. code-block:: python :linenos: - from pyramid.interfaces import ITraverser - from zope.interface import Interface from myapp.traversal import Traverser from myapp.resources import MyRoot - - config.registry.registerAdapter(Traverser, (MyRoot,), ITraverser) + from pyramid.config import Configurator + config = Configurator() + config.set_traverser(Traverser, MyRoot) If the above stanza was added to a Pyramid ``__init__.py`` file's ``main`` function, :app:`Pyramid` would use the ``myapp.traversal.Traverser`` only diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index 11d779854..08cc430f6 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -529,6 +529,21 @@ introspectables in categories not described here. A normalized version of the ``spec`` argument provided to ``add_static_view``. +``traversers`` + + Each introspectable in the ``traversers`` category represents a call to + :meth:`pyramid.config.Configurator.add_traverser`; each will have the + following data. + + ``iface`` + + The (resolved) interface or class object that represents the return value + of a root factory that this traverser will be used for. + + ``factory`` + + The (resolved) traverser class. + Introspection in the Toolbar ---------------------------- diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index eb4442e98..7c0ea054d 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -1,3 +1,5 @@ +from zope.interface import Interface + from pyramid.config.util import action_method from pyramid.interfaces import ( @@ -7,6 +9,7 @@ from pyramid.interfaces import ( IRequestProperties, IRootFactory, ISessionFactory, + ITraverser, ) from pyramid.traversal import DefaultRootFactory @@ -140,6 +143,77 @@ class FactoriesConfiguratorMixin(object): self.action(('request properties', name), register, introspectables=(intr,)) + def set_traverser(self, factory, iface=None): + """ + The superdefault :term:`traversal` algorithm that :app:`Pyramid` uses + is explained in :ref:`traversal_algorithm`. Though it is rarely + necessary, this default algorithm can be swapped out selectively for + a different traversal pattern via configuration. The section + entitled :ref:`changing_the_traverser` details how to create a + traverser class. + + For example, to override the superdefault traverser used by Pyramid, + you might do something like this: + + .. code-block:: python + + from myapp.traversal import MyCustomTraverser + config.set_traverser(MyCustomTraverser) + + This would cause the Pyramid superdefault traverser to never be used; + intead all traversal would be done using your ``MyCustomTraverser`` + class, no matter which object was returned by the :term:`root + factory` of this application. Note that we passed no arguments to + the ``iface`` keyword parameter. The default value of ``iface``, + ``None`` represents that the registered traverser should be used when + no other more specific traverser is available for the object returned + by the root factory. + + However, more than one traversal algorithm can be active at the same + time. The traverser used can depend on the result of the :term:`root + factory`. For instance, if your root factory returns more than one + type of object conditionally, you could claim that an alternate + traverser adapter should be used agsinst one particular class or + interface returned by that root factory. When the root factory + returned an object that implemented that class or interface, a custom + traverser would be used. Otherwise, the default traverser would be + used. The ``iface`` argument represents the class of the object that + the root factory might return or an :term:`interface` that the object + might implement. + + To use a particular traverser only when the root factory returns a + particular class: + + .. code-block:: python + + config.set_traverser(MyCustomTraverser, MyRootClass) + + When more than one traverser is active, the "most specific" traverser + will be used (the one that matches the class or interface of the + value returned by the root factory most closely). + + Note that either ``factory`` or ``iface`` can be a :term:`dotted + Python name` or a Python object. + + See :ref:`changing_the_traverser` for more information. + """ + iface = self.maybe_dotted(iface) + factory = self.maybe_dotted(factory) + def register(iface=iface): + if iface is None: + iface = Interface + self.registry.registerAdapter(factory, (iface,), ITraverser) + discriminator = ('traverser', iface) + intr = self.introspectable( + 'traversers', + discriminator, + 'traverser for %r' % iface, + 'traverser', + ) + intr['factory'] = factory + intr['iface'] = iface + self.action(('traverser', iface), register, introspectables=(intr,)) + def _set_request_properties(event): request = event.request plist = request.registry.queryUtility(IRequestProperties) diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index d1a01568f..51c60896e 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -129,6 +129,46 @@ class TestFactoriesMixin(unittest.TestCase): self.assertEqual(callables, [('foo', foo, False), ('bar', foo, True)]) + def test_set_traverser_dotted_names(self): + from pyramid.interfaces import ITraverser + config = self._makeOne(autocommit=True) + config.set_traverser( + 'pyramid.tests.test_config.test_factories.DummyTraverser', + 'pyramid.tests.test_config.test_factories.DummyIface') + iface = DummyIface() + traverser = config.registry.getAdapter(iface, ITraverser) + self.assertEqual(traverser.__class__, DummyTraverser) + self.assertEqual(traverser.root, iface) + + def test_set_traverser_default_iface_means_Interface(self): + from pyramid.interfaces import ITraverser + config = self._makeOne(autocommit=True) + config.set_traverser(DummyTraverser) + traverser = config.registry.getAdapter(None, ITraverser) + self.assertEqual(traverser.__class__, DummyTraverser) + + def test_set_traverser_nondefault_iface(self): + from pyramid.interfaces import ITraverser + config = self._makeOne(autocommit=True) + config.set_traverser(DummyTraverser, DummyIface) + iface = DummyIface() + traverser = config.registry.getAdapter(iface, ITraverser) + self.assertEqual(traverser.__class__, DummyTraverser) + self.assertEqual(traverser.root, iface) + + def test_set_traverser_introspectables(self): + config = self._makeOne() + config.set_traverser(DummyTraverser, DummyIface) + actions = config.action_state.actions + self.assertEqual(len(actions), 1) + intrs = actions[0]['introspectables'] + self.assertEqual(len(intrs), 1) + intr = intrs[0] + self.assertEqual(intr.type_name, 'traverser') + self.assertEqual(intr.discriminator, ('traverser', DummyIface)) + self.assertEqual(intr.category_name, 'traversers') + self.assertEqual(intr.title, 'traverser for %r' % DummyIface) + class DummyRequest(object): callables = None @@ -139,3 +179,10 @@ class DummyRequest(object): if self.callables is None: self.callables = [] self.callables.append((name, callable, reify)) + +class DummyTraverser(object): + def __init__(self, root): + self.root = root + +class DummyIface(object): + pass -- cgit v1.2.3