summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt5
-rw-r--r--TODO.txt3
-rw-r--r--docs/api/config.rst2
-rw-r--r--docs/narr/hooks.rst14
-rw-r--r--docs/narr/introspector.rst15
-rw-r--r--pyramid/config/factories.py74
-rw-r--r--pyramid/tests/test_config/test_factories.py47
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