diff options
| -rw-r--r-- | docs/errata.rst | 1 | ||||
| -rw-r--r-- | docs/narr/hybrid.rst | 494 | ||||
| -rw-r--r-- | docs/narr/traversal.rst | 5 | ||||
| -rw-r--r-- | docs/narr/urldispatch.rst | 2 |
4 files changed, 338 insertions, 164 deletions
diff --git a/docs/errata.rst b/docs/errata.rst index b27bfc684..89e1b909a 100644 --- a/docs/errata.rst +++ b/docs/errata.rst @@ -64,6 +64,7 @@ Should read: def __call__(self): return Response('OK') + pp. 98 ------ diff --git a/docs/narr/hybrid.rst b/docs/narr/hybrid.rst index 2beb3aed9..fc51feb1c 100644 --- a/docs/narr/hybrid.rst +++ b/docs/narr/hybrid.rst @@ -3,39 +3,32 @@ Combining Traversal and URL Dispatch ==================================== -:mod:`repoze.bfg` makes an honest attempt to unify the largely -incompatible concepts of :term:`traversal` and :term:`url dispatch`. - When you write most :mod:`repoze.bfg` applications, you'll be using -either one or the other subsystem, but not both, to perform -:term:`context finding`. However, to solve specific problems, it's -useful to use *both* traversal *and* URL dispatch within the same -application. :mod:`repoze.bfg` makes this possible via *hybrid* -applications. +one or the other of two available :term:`context finding` subsystems: +traversal or URL dispatch. However, to solve a limited set of +problems, it's useful to use *both* traversal and URL dispatch +together within the same application. :mod:`repoze.bfg` makes this +possible via *hybrid* applications. .. warning:: - Creating an application that uses hybrid-mode features of - :mod:`repoze.bfg` exposes non-trivial corner cases. Reasoning - about a "hybrid" URL dispatch + traversal model can be difficult - because the combination of the two concepts seems to fall outside - the sweet spot of `the magical number seven plus or minus 2 - <http://en.wikipedia.org/wiki/The_Magical_Number_Seven,_Plus_or_Minus_Two>`_. - To reason successfully about using URL dispatch and traversal - together, you need to understand 1) URL pattern matching, 2) root - factories and 3) the traversal algorithm, and the interactions - between all of them. Therefore, we don't recommend creating an - application that relies on hybrid behavior unless you must. - -The Schism ----------- - -:mod:`repoze.bfg`, especially when used according to the tutorials in -its documentation is sort of a "dual-mode" framework. The tutorials -explain how to create an application in terms of using either -:term:`url dispatch` *or* :term:`traversal`. But not both. It's -useful to examine that pattern in order to understand the schism -between the two. + Reasoning about the behavior of a "hybrid" URL dispatch + traversal + application can be challenging. To successfully reason about using + URL dispatch and traversal together, you need to understand URL + pattern matching, root factories, and the :term:`traversal` + algorithm, and the potential interactions between them. Therefore, + we don't recommend creating an application that relies on hybrid + behavior unless you must. + +A Review of Non-Hybrid Applications +----------------------------------- + +When used according to the tutorials in its documentation +:mod:`repoze.bfg` is a "dual-mode" framework: the tutorials explain +how to create an application in terms of using either :term:`url +dispatch` *or* :term:`traversal`. This chapter details how you might +combine these two dispatch mechanisms, but we'll review how they work +in isolation before trying to combine them. URL Dispatch Only ~~~~~~~~~~~~~~~~~ @@ -44,6 +37,7 @@ An application that uses :term:`url dispatch` exclusively to map URLs to code will often have declarations like this within :term:`ZCML`: .. code-block:: xml + :linenos: <route path=":foo/:bar" @@ -57,42 +51,24 @@ to code will often have declarations like this within :term:`ZCML`: view=".views.bazbuz" /> -In other words, each :term:`route` typically corresponds to a single -view callable, and when that route is matched during a request, the -view callable attached to it is invoked. - -"Under the hood", these ``<route>`` declarations register a view for -each route. This view is registered for the following context/request -type/name triad: - -- the context :term:`interface` ``None``, implying any context. - -- Two :term:`request type` interfaces are attached to the request: the - :class:`repoze.bfg.interfaces.IRequest` interface and a - dynamically-constructed route-statement-specific :term:`interface`. - -- the empty string as the :term:`view name`, implying the default - view. - -This usually ensures that the named view will only be called when the -route it's attached to actually matches. +Each :term:`route` typically corresponds to a single view callable, +and when that route is matched during a request, the view callable +named by the ``view`` attribute is invoked. Typically, an application that uses only URL dispatch won't perform -any view configuration in ZCML and won't have any calls to +any configuration in ZCML that includes a ``<view>`` declaration and +won't have any calls to :meth:`repoze.bfg.configuration.Configurator.add_view` in its startup code. Traversal Only ~~~~~~~~~~~~~~ -An application that uses :term:`traversal` exclusively to map URLs to -code just won't have any ZCML ``<route>`` declarations nor will it -make any calls to the -:meth:`repoze.bfg.configuration.Configurator.add_route` method. -Instead, its view configuration will imply declarations that look like -this: +An application that uses only traversal will have view configuration +declarations that look like this: .. code-block:: xml + :linenos: <view name="foobar" @@ -104,61 +80,163 @@ this: view=".views.bazbuz" /> -"Under the hood", the above view statements register a view using the -following context/request/name :term:`triad`: - -- the :term:`context` interface ``None`` +When the above configuration is applied to an application, the +``.views.foobar`` view callable above will be called when the URL +``/foobar`` is visited. Likewise, the view ``.views.bazbuz`` will be +called when the URL ``/bazbuz`` is visited. -- the :class:`repoze.bfg.interfaces.IRequest` :term:`request type` - interface - -- a :term:`view name` matching the ``name=`` argument. - -The ``.views.foobar`` view callable above will be called when the URL -``/a/b/c/foobar`` or ``/foobar``, etc, assuming that no view is named -``a``, ``b``, or ``c`` during traversal. - -.. index:: - single: hybrid mode application +An application that uses :term:`traversal` exclusively to map URLs to +code usually won't have any ZCML ``<route>`` declarations nor will it +make any calls to the +:meth:`repoze.bfg.configuration.Configurator.add_route` method. Hybrid Applications ------------------- -Clearly either traversal or url dispatch can be used to create a -:mod:`repoze.bfg` application. However, it is possible to combine the -competing concepts of traversal and url dispatch to resolve URLs to -code within the same application. - -To "turn on" hybrid mode, use a :term:`route configuration` that -includes a ``path`` argument that contains a special dynamic part: -either ``*traverse`` or ``*subpath``. +Either traversal or url dispatch alone can be used to create a +:mod:`repoze.bfg` application. However, it is also possible to +combine the concepts of traversal and url dispatch when building an +application: the result is a hybrid application. In a hybrid +application, traversal is performed *after* a particular route has +matched. + +A hybrid application is a lot more like a "pure" traversal-based +application than it is like a "pure" URL-dispatch based application. +But unlike in a "pure" traversal-based application, in a hybrid +application, :term:`traversal` is performed during a request after a +route has already matched. This means that the URL pattern that +represents the ``path`` argument of a route must match the +``PATH_INFO`` of a request, and after the route path has matched, most +of the "normal" rules of traversal with respect to :term:`context +finding` and :term:`view lookup` apply. + +There are only four real differences between a purely traversal-based +application and a hybrid application: + +- In a purely traversal based application, no routes are defined; in a + hybrid application, at least one route will be defined. + +- In a purely traversal based application, the root object used is + global implied by the :term:`root factory` provided at startup + time; in a hybrid application, the :term:`root` object at which + traversal begins may be varied on a per-route basis. + +- In a purely traversal-based application, the ``PATH_INFO`` of the + underlying :term:`WSGI` environment is used wholesale as a traversal + path; in a hybrid application, the traversal path is not the entire + ``PATH_INFO`` string, but a portion of the URL determined by a + matching pattern in the matched route configuration's path. + +- In a purely traversal based applications, view configurations which + do not mention a ``route_name`` argument are considered during + :term:`view lookup`; in a hybrid application, when a route is + matched, only view configurations which mention that route's name as + a ``route_name`` are considered during :term:`view lookup`. + +More generally, a hybrid application *is* a traversal-based +application except: + +- the traversal *root* is chosen based on the route configuration of + the route that matched instead of from the ``root_factory`` supplied + during application startup configuration. + +- the traversal *path* is chosen based on the route configuration of + the route that matched rather than from the ``PATH_INFO`` of a + request. + +- the set of views that may be chosen during :term:`view lookup` when + a route matches are limited to those which specifically name a + ``route_name`` in their configuration that is the same as the + matched route's ``name``. + +To create a hybrid mode application, use a :term:`route configuration` +that implies a particular :term:`root factory` and which also includes +a ``path`` argument that contains a special dynamic part: either +``*traverse`` or ``*subpath``. + +The Root Object for a Route Match +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A hybrid application implies that traversal is performed during a +request after a route has matched. Traversal, by definition, must +always begin at a root object. Therefore it's important to know +*which* root object will be traversed after a route has matched. + +Figuring out which :term:`root` object results from a particular route +match is straightforward. When a route is matched: + +- If the route's configuration has a ``root_factory`` argument which + points to a :term:`root factory` callable, that callable will be + called to generate a :term:`root` object. + +- If the route's configuration does not have a ``root_factory`` + argument, the *global* :term:`root factory` will be called to + generate a :term:`root` object. The global root factory is the + callable implied by the ``root_factory`` argument passed to + :class:`repoze.bfg.configuration.Configurator` at application + startup time. + +- If a ``root_factory`` argument is not provided to the + :class:`repoze.bfg.configuration.Configurator` at startup time, a + *default* root factory is used. The default root factory is used to + generate a root object. + +.. note:: + + Root factories related to a route were explained previously in + within :ref:`route_factories`. Both the global root factory and + default root factory were explained previously within + :ref:`the_object_graph`. Using ``*traverse`` In a Route Path ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To create a hybrid application, combine traversal and URL dispatch by -using route configuration that contains the special token -``*traverse`` in the route *path*. For example: +A hybrid application most often implies the inclusion of a route +configuration that contains the special token ``*traverse`` at the end +of a route's path: .. code-block:: xml + :linenos: <route path=":foo/:bar/*traverse" name="home" - view=".views.home" /> -When the this route is matched, :mod:`repoze.bfg` will attempt to use -:term:`traversal` against the context implied by the :term:`root -factory` of this route. - -The above example isn't very useful unless you've defined a custom -:term:`root factory` by passing it to constructor of a -:class:`repoze.bfg.configuration.Configurator` because the *default* -root factory cannot be traversed (it has no useful ``__getitem__`` -method). But let's imagine that your root factory looks like so: +A ``*traverse`` token at the end of the path in a route's +configuration implies a "stararg" *capture* value. When it is used, +it will match the remainder of the path segments of the URL. This +remainder becomes the path used to perform traversal. + +.. note:: + + The ``*stararg`` route path pattern syntax is explained in more + detail within :ref:`route_path_pattern_syntax`. + +Note that unlike the examples provided within +:ref:`urldispatch_chapter`, the ``<route>`` configuration named +previously does not name a ``view`` attribute. This is because a +hybrid mode application relies on :term:`traversal` to do +:term:`context finding` and :term:`view lookup` instead of invariably +invoking a specific view callable named directly within the matched +route's configuration. + +Because the path of the above route ends with ``*traverse``, when this +route configuration is matched during a request, :mod:`repoze.bfg` +will attempt to use :term:`traversal` against the :term:`root` object +implied by the :term:`root factory` implied by the route's +configuration. Once :term:`traversal` has found a :term:`context`, +:term:`view lookup` will be invoked in almost exactly the same way it +would have been invoked in a "pure" traversal-based application. + +The *default* :term:`root factory` cannot be traversed: it has no +useful ``__getitem__`` method. So we'll need to associate this route +configuration with a non-default root factory in order to create a +useful hybrid application. To that end, let's imagine that we've +created a root factory looks like so in a module named ``routes.py``: .. code-block:: python + :linenos: class Traversable(object): def __init__(self, subobjects): @@ -168,66 +246,174 @@ method). But let's imagine that your root factory looks like so: return self.subobjects[name] root = Traversable( - {'a':Traversable({'b':Traversable({'c':Traversable({})})})}) + {'a':Traversable({'b':Traversable({'c':Traversable({})})})} + ) def root_factory(request): return root -We've defined a bogus graph here that can be traversed, and a -``root_factory`` function that returns the root of the graph that we -can pass to our :class:`repoze.bfg.configuration.Configurator`. - -Because the ``Traversable`` object we've defined has a ``__getitem__`` -method that does something nominally useful, using traversal against -the root implied by a route statement becomes a reasonable thing to -do. - -Under the circumstance implied by ``:foo/:bar/*traverse``, traversal -is performed *after* the route matches. If the root factory -associated with that route returns a traversable object, the "capture -value" implied by the ``*traverse`` element in the path pattern will -be used to traverse the graph, starting from the root object returned -from the root factory. - -For example, if the URL requested by a user was -``http://example.com/one/two/a/b/c``, and the above route was matched, -the traversal path used against the root would be ``a/b/c``. -:mod:`repoze.bfg` will attempt to traverse a graph through the edges -``a``, ``b``, and ``c``. In our above example, that would imply that -the *context* of the view would be the ``Traversable`` object we've -named ``c`` in our bogus graph, using the ``.views.home`` view as the -view callable. - -We can also define extra views that match a route: +Above, we've defined a (bogus) graph here that can be traversed, and a +``root_factory`` function that can be used as part of a particular +route configuration statement: .. code-block:: xml + :linenos: <route path=":foo/:bar/*traverse" name="home" - view=".views.home" + root_factory=".routes.root_factory" + /> + +The ``root_factory`` above points at the function we've defined. It +will return an instance of the ``Traversable`` class as a root object +whenever this route is matched. Because the ``Traversable`` object +we've defined has a ``__getitem__`` method that does something +nominally useful, and because traversal uses ``__getitem__`` to walk +the nodes that make up an object graph, using traversal against the +root object implied by our route statement becomes a reasonable thing +to do. + +.. note:: + + We could have also used our ``root_factory`` callable as the + ``root_factory`` argument of the + :class:`repoze.bfg.configuration.Configurator` constructor instead + of associating it with a particular route inside the route's + configuration. Every hybrid route configuration that is matched but + which does *not* name a ``root_factory``` attribute will use the use + global ``root_factory`` function to generate a root object. + +When the route configuration named ``home`` above is matched during a +request, the matchdict generated will be based on its path: +``:foo/:bar/*traverse``. The "capture value" implied by the +``*traverse`` element in the path pattern will be used to traverse the +graph in order to find a context, starting from the root object +returned from the root factory. In the above example, the +:term:`root` object found will be the instance named ``root`` in +``routes.py``. + +If the URL that matched a route with the path ``:foo/:bar/*traverse``, +is ``http://example.com/one/two/a/b/c``, the traversal path used +against the root object will be ``a/b/c``. As a result, +:mod:`repoze.bfg` will attempt to traverse through the edges ``a``, +``b``, and ``c``, beginning at the root object. + +In our above example, this particular set of traversal steps will mean +that the :term:`context` of the view would be the ``Traversable`` +object we've named ``c`` in our bogus graph and the :term:`view name` +resulting from traversal will be the empty string; if you need a +refresher about why this outcome is presumed, see +:ref:`traversal_algorithm`. + +At this point, a suitable view callable will be found and invoked +using :term:`view lookup` as described in :ref:`view_configuration`, +but with a caveat: in order for view lookup to work, we need to define +a view configuration that will match when :term:`view lookup` is +invoked after a route matches: + +.. code-block:: xml + :linenos: + + <route + path=":foo/:bar/*traverse" + name="home" + root_factory=".routes.root_factory" /> <view route_name="home" - name="another" - view=".views.another" + view=".views.myview" /> -Views that spell a route name are meant to associate a particular view -declaration with a route, using the route's name, in order to indicate -that the view should *only be invoked when the route matches*. +Note that the above ``view`` declaration includes a ``route_name`` +argument. Views that include a ``route_name`` argument are meant to +associate a particular view declaration with a route, using the +route's name, in order to indicate that the view should *only be +invoked when the route matches*. View configurations may have a ``route_name`` attribute which refers to the value of the ``<route>`` declaration's ``name`` attribute. In -the above example, the route name is ``home``. +the above example, the route name is ``home``, referring to the name +of the route defined above it. + +The above ``.views.myview`` view will be invoked when: + +- the route named "home" is matched + +- the :term:`view name` resulting from traversal is the empty string. + +- the :term:`context` is any object. + +It is also possible to declare alternate views that may be invoked +when a hybrid route is matched: + +.. code-block:: xml + :linenos: + + <route + path=":foo/:bar/*traverse" + name="home" + root_factory=".routes.root_factory" + /> + + <view + route_name="home" + view=".views.myview" + /> + + <view + route_name="home" + name="another" + view=".views.another_view" + /> + +The ``view`` declaration for ``.views.another_view`` above names a +different view and, more importantly, a different :term:`view name`. +The above ``.views.another_view`` view will be invoked when: + +- the route named "home" is matched + +- the :term:`view name` resulting from traversal is ``another``. + +- the :term:`context` is any object. + +For instance, if the URL ``http://example.com/one/two/a/another`` is +provided to an application that uses the previously mentioned object +graph, the ``.views.another`` view callable will be called instead of +the ``.views.myview`` view callable because the :term:`view name` will +be ``another`` instead of the empty string. -The ``<view>`` declaration above names a different view and (more -importantly) a different :term:`view name`. It's :term:`view name` -will be looked for during traversal. So if our URL is -"http://example.com/one/two/a/another", the ``.views.another`` view -callable will be called instead of the *default* view callable (the -one implied by the route with the name ``home``). +More complicated matching can be composed. All arguments to *route* +configuration statements and *view* configuration statements are +supported in hybrid applications (such as :term:`predicate` +arguments). + +Making Global Views Match ++++++++++++++++++++++++++ + +By default, view configurations that don't mention a ``route_name`` +will be not found by view lookup when a route that mentions a +``*traverse`` in its path matches. You can make these match forcibly +by adding the ``use_global_views`` flag to the route definition. For +example, the ``views.bazbuz`` view below will be found if the route +named ``abc`` below is matched and the ``PATH_INFO`` is +``/abc/bazbuz``, even though the view configuration statement does not +have the ``route_name="abc"`` attribute. + +.. code-block:: xml + :linenos: + + <route + path="/abc/*traverse" + name="abc" + use_global_views="True" + /> + + <view + name="bazbuz" + view=".views.bazbuz" + /> .. index:: single: route subpath @@ -252,6 +438,7 @@ list implied by the capture value of ``*subpath``. You'll see this pattern most commonly in route declarations that look like this: .. code-block:: xml + :linenos: <route path="/static/*subpath" @@ -263,30 +450,6 @@ Where ``.views.static_view`` is an instance of :class:`repoze.bfg.view.static`. This effectively tells the static helper to traverse everything in the subpath as a filename. -Making Global Views Match -~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, view configurations that don't mention a ``route_name`` -will be found by view lookup when a route matches. You can make these -match forcibly by adding the ``use_global_views`` flag to the route -definition. For example, the ``views.bazbuz`` view below will be -found if the route named ``abc`` below is matched and the -``PATH_INFO`` is ``/abc/bazbuz``, even though the view configuration -statement does not have the ``route_name="abc"`` attribute. - -.. code-block:: xml - - <route - path="/abc/*traverse" - name="abc" - use_global_views="True" - /> - - <view - name="bazbuz" - view=".views.bazbuz" - /> - Corner Cases ------------ @@ -303,6 +466,7 @@ For example, this pair of route/view ZCML declarations will generate a "conflict" error at startup time. .. code-block:: xml + :linenos: <route path=":foo/:bar/*traverse" @@ -321,6 +485,7 @@ above is an *implicit* default view when that route matches. example, this ``<route>`` statement: .. code-block:: xml + :linenos: <route path=":foo/:bar/*traverse" @@ -331,6 +496,7 @@ example, this ``<route>`` statement: Can also be spelled like so: .. code-block:: xml + :linenos: <route path=":foo/:bar/*traverse" @@ -351,6 +517,7 @@ Binding Extra Views Against a Route Configuration that Doesn't Have a ``*travers Here's another corner case that just makes no sense. .. code-block:: xml + :linenos: <route path="/abc" @@ -364,16 +531,17 @@ Here's another corner case that just makes no sense. route_name="abc" /> -The above ``<view>`` declaration is completely useless, because the -view name will never be matched when the route it references matches. -Only the view associated with the route itself (``.views.abc``) will -ever be invoked when the route matches, because the default view is -always invoked when a route matches and when no post-match traversal -is performed. To make the below ``<view>`` declaration non-useless, -you must the special ``*traverse`` token to the route's "path." For -example: +The above ``<view>`` declaration is useless, because it will never be +matched when the route it references has matched. Only the view +associated with the route itself (``.views.abc``) will ever be invoked +when the route matches, because the default view is always invoked +when a route matches and when no post-match traversal is performed. + +To make the above ``<view>`` declaration non-useless, the special +``*traverse`` token must end the route's path. For example: .. code-block:: xml + :linenos: <route path="/abc/*traverse" diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index eb53f94a7..126678f07 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -146,6 +146,8 @@ within the :ref:`views_chapter` chapter. single: traversal graph single: model graph +.. _the_object_graph: + The Object Graph ---------------- @@ -180,7 +182,8 @@ named ``config``: Using the ``root_factory`` argument to a :class:`repoze.bfg.configuration.Configurator` constructor tells your :mod:`repoze.bfg` application to call this root factory to generate a -root object whenever a request enters the application. +root object whenever a request enters the application. This root +factory is also known as the global root factory. A root factory is passed a :term:`request` object and it is expected to return an object which represents the root of the object graph. diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index d766eba09..81254d3a8 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -357,6 +357,8 @@ will never be evaluated. .. index:: single: route factory +.. _route_factories: + Route Factories ~~~~~~~~~~~~~~~ |
