diff options
| author | Chris McDonough <chrism@plope.com> | 2010-12-09 04:04:39 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2010-12-09 04:04:39 -0500 |
| commit | e8edd5ff7157c9a11b6478702c1e45bb46f11344 (patch) | |
| tree | 98ba7a2d4235fcbb86b67c187f84cc39fd6d0add | |
| parent | f360d9e6d9689dfe92a950e97e7e19c414655997 (diff) | |
| download | pyramid-e8edd5ff7157c9a11b6478702c1e45bb46f11344.tar.gz pyramid-e8edd5ff7157c9a11b6478702c1e45bb46f11344.tar.bz2 pyramid-e8edd5ff7157c9a11b6478702c1e45bb46f11344.zip | |
- Add a ``handler`` ZCML directive. This directive does the same thing as
``pyramid.configuration.add_handler``.
| -rw-r--r-- | CHANGES.txt | 3 | ||||
| -rw-r--r-- | TODO.txt | 5 | ||||
| -rw-r--r-- | docs/narr/declarative.rst | 51 | ||||
| -rw-r--r-- | docs/narr/handlers.rst | 6 | ||||
| -rw-r--r-- | docs/zcml.rst | 1 | ||||
| -rw-r--r-- | docs/zcml/handler.rst | 158 | ||||
| -rw-r--r-- | pyramid/includes/meta.zcml | 6 | ||||
| -rw-r--r-- | pyramid/tests/test_zcml.py | 80 | ||||
| -rw-r--r-- | pyramid/zcml.py | 79 |
9 files changed, 374 insertions, 15 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 2a968daf9..2adec52c6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,9 @@ Next release Features -------- +- Add a ``handler`` ZCML directive. This directive does the same thing as + ``pyramid.configuration.add_handler``. + - A new module named ``pyramid.config`` was added. It subsumes the duties of the older ``pyramid.configuration`` module. @@ -4,11 +4,6 @@ Pyramid TODOs Must-Have (before 1.0) ---------------------- -- Add a ``handler`` ZCML directive. This implies some slightly dicey - refactoring of the configurator to allow it to generate ZCML - "discriminators" for views and routes, that could be implemented in terms - of "twophase configuration" in "should have" below. - - Provide a .flash API on session object. - Use ``@register_view`` instead of ``@view_config`` and change view docs to diff --git a/docs/narr/declarative.rst b/docs/narr/declarative.rst index 31d6fc575..99c5a75ac 100644 --- a/docs/narr/declarative.rst +++ b/docs/narr/declarative.rst @@ -644,12 +644,11 @@ See :ref:`view_directive` for complete ZCML directive documentation. Configuring a Route via ZCML ---------------------------- -Instead of using the imperative -:meth:`pyramid.config.Configurator.add_route` method to add a new -route, you can alternately use :term:`ZCML`. :ref:`route_directive` -statements in a :term:`ZCML` file used by your application is a sign that -you're using :term:`URL dispatch`. For example, the following :term:`ZCML -declaration` causes a route to be added to the application. +Instead of using the imperative :meth:`pyramid.config.Configurator.add_route` +method to add a new route, you can alternately use :term:`ZCML`. +:ref:`route_directive` statements in a :term:`ZCML` file. For example, the +following :term:`ZCML declaration` causes a route to be added to the +application. .. code-block:: xml :linenos: @@ -679,6 +678,46 @@ is the order that they appear relative to each other in the ZCML file. See :ref:`route_directive` for full ``route`` ZCML directive documentation. +.. _zcml_handler_configuration: + +Configuring a Handler via ZCML +------------------------------ + +Instead of using the imperative +:meth:`pyramid.config.Configurator.add_handler` method to add a new +route, you can alternately use :term:`ZCML`. :ref:`handler_directive` +statements in a :term:`ZCML` file used by your application is a sign that +you're using :term:`URL dispatch`. For example, the following :term:`ZCML +declaration` causes a route to be added to the application. + +.. code-block:: xml + :linenos: + + <handler + route_name="myroute" + pattern="/prefix/{action}" + handler=".handlers.MyHandler" + /> + +.. note:: + + Values prefixed with a period (``.``) within the values of ZCML attributes + such as the ``handler`` attribute of a ``handler`` directive mean + "relative to the Python package directory in which this :term:`ZCML` file + is stored". So if the above ``handler`` declaration was made inside a + ``configure.zcml`` file that lived in the ``hello`` package, you could + replace the relative ``.views.MyHandler`` with the absolute + ``hello.views.MyHandler`` Either the relative or absolute form is + functionally equivalent. It's often useful to use the relative form, in + case your package's name changes. It's also shorter to type. + +The order that the routes attached to handlers are evaluated when declarative +configuration is used is the order that they appear relative to each other in +the ZCML file. + +See :ref:`handler_directive` for full ``handler`` ZCML directive +documentation. + .. index:: triple: view; zcml; static resource diff --git a/docs/narr/handlers.rst b/docs/narr/handlers.rst index 49dd73f7f..9a5267b2a 100644 --- a/docs/narr/handlers.rst +++ b/docs/narr/handlers.rst @@ -86,6 +86,12 @@ This will result one of the methods that are configured for the ``action`` of handler class not named 'index' might be called if they were configured to be called when the ``action`` name is 'index' as will be seen below. +.. note:: + + Handler configuration may also be added to the system via :term:`ZCML` (see + :ref:`zcml_handler_configuration`). + +.. _using_add_handler: Using :meth:`~pyramid.config.Configurator.add_handler` ------------------------------------------------------------- diff --git a/docs/zcml.rst b/docs/zcml.rst index 60bbbf574..5913f9460 100644 --- a/docs/zcml.rst +++ b/docs/zcml.rst @@ -16,6 +16,7 @@ documentation is organized alphabetically by directive name. zcml/configure zcml/default_permission zcml/forbidden + zcml/handler zcml/include zcml/localenegotiator zcml/notfound diff --git a/docs/zcml/handler.rst b/docs/zcml/handler.rst new file mode 100644 index 000000000..301bf7895 --- /dev/null +++ b/docs/zcml/handler.rst @@ -0,0 +1,158 @@ +.. _handler_directive: + +``handler`` +----------- + +The ``handler`` directive adds the configuration of a :term:`view handler` to +the :term:`application registry`. + +Attributes +~~~~~~~~~~ + +``route_name`` + The name of the route, e.g. ``myroute``. This attribute is required. It + must be unique among all defined handler and route names in a given + configuration. + +``pattern`` + The pattern of the route e.g. ``ideas/{idea}``. This attribute is + required. See :ref:`route_pattern_syntax` for information about the syntax + of route patterns. The name ``{action}`` is treated specially in handler + patterns. See :ref:`using_add_handler` for a discussion of how + ``{action}`` in handler patterns is treated. + +``action`` + If the action name is not specified in the ``pattern``, use this name as the + handler action (method name). + +``factory`` + The :term:`dotted Python name` to a function that will generate a + :app:`Pyramid` context object when the associated route matches. + e.g. ``mypackage.models.MyFactoryClass``. If this argument is not + specified, a default root factory will be used. + +``xhr`` + This value should be either ``True`` or ``False``. If this value is + specified and is ``True``, the :term:`request` must possess an + ``HTTP_X_REQUESTED_WITH`` (aka ``X-Requested-With``) header for this + route to match. This is useful for detecting AJAX requests issued + from jQuery, Prototype and other Javascript libraries. If this + predicate returns false, route matching continues. + +``traverse`` + If you would like to cause the :term:`context` to be something other + than the :term:`root` object when this route matches, you can spell + a traversal pattern as the ``traverse`` argument. This traversal + pattern will be used as the traversal path: traversal will begin at + the root object implied by this route (either the global root, or + the object returned by the ``factory`` associated with this route). + + The syntax of the ``traverse`` argument is the same as it is for + ``pattern``. For example, if the ``pattern`` provided to the + ``route`` directive is ``articles/{article}/edit``, and the + ``traverse`` argument provided to the ``route`` directive is + ``/{article}``, when a request comes in that causes the route to + match in such a way that the ``article`` match value is '1' (when + the request URI is ``/articles/1/edit``), the traversal path will be + generated as ``/1``. This means that the root object's + ``__getitem__`` will be called with the name ``1`` during the + traversal phase. If the ``1`` object exists, it will become the + :term:`context` of the request. :ref:`traversal_chapter` has more + information about traversal. + + If the traversal path contains segment marker names which are not + present in the ``pattern`` argument, a runtime error will occur. + The ``traverse`` pattern should not contain segment markers that do + not exist in the ``pattern``. + + A similar combining of routing and traversal is available when a + route is matched which contains a ``*traverse`` remainder marker in + its ``pattern`` (see :ref:`using_traverse_in_a_route_pattern`). The + ``traverse`` argument to the ``route`` directive allows you to + associate route patterns with an arbitrary traversal path without + using a a ``*traverse`` remainder marker; instead you can use other + match information. + + Note that the ``traverse`` argument to the ``handler`` directive is + ignored when attached to a route that has a ``*traverse`` remainder + marker in its pattern. + +``request_method`` + A string representing an HTTP method name, e.g. ``GET``, ``POST``, + ``HEAD``, ``DELETE``, ``PUT``. If this argument is not specified, + this route will match if the request has *any* request method. If + this predicate returns false, route matching continues. + +``path_info`` + The value of this attribute represents a regular expression pattern + that will be tested against the ``PATH_INFO`` WSGI environment + variable. If the regex matches, this predicate will be true. If + this predicate returns false, route matching continues. + +``request_param`` + This value can be any string. A view declaration with this + attribute ensures that the associated route will only match when the + request has a key in the ``request.params`` dictionary (an HTTP + ``GET`` or ``POST`` variable) that has a name which matches the + supplied value. If the value supplied to the attribute has a ``=`` + sign in it, e.g. ``request_params="foo=123"``, then the key + (``foo``) must both exist in the ``request.params`` dictionary, and + the value must match the right hand side of the expression (``123``) + for the route to "match" the current request. If this predicate + returns false, route matching continues. + +``header`` + The value of this attribute represents an HTTP header name or a + header name/value pair. If the value contains a ``:`` (colon), it + will be considered a name/value pair (e.g. ``User-Agent:Mozilla/.*`` + or ``Host:localhost``). The *value* of an attribute that represent + a name/value pair should be a regular expression. If the value does + not contain a colon, the entire value will be considered to be the + header name (e.g. ``If-Modified-Since``). If the value evaluates to + a header name only without a value, the header specified by the name + must be present in the request for this predicate to be true. If + the value evaluates to a header name/value pair, the header + specified by the name must be present in the request *and* the + regular expression specified as the value must match the header + value. Whether or not the value represents a header name or a + header name/value pair, the case of the header name is not + significant. If this predicate returns false, route matching + continues. + +``accept`` + The value of this attribute represents a match query for one or more + mimetypes in the ``Accept`` HTTP request header. If this value is + specified, it must be in one of the following forms: a mimetype + match token in the form ``text/plain``, a wildcard mimetype match + token in the form ``text/*`` or a match-all wildcard mimetype match + token in the form ``*/*``. If any of the forms matches the + ``Accept`` header of the request, this predicate will be true. If + this predicate returns false, route matching continues. + +``custom_predicates`` + This value should be a sequence of references to custom predicate + callables. Use custom predicates when no set of predefined + predicates does what you need. Custom predicates can be combined + with predefined predicates as necessary. Each custom predicate + callable should accept two arguments: ``info`` and ``request`` + and should return either ``True`` or ``False`` after doing arbitrary + evaluation of the info and/or the request. If all custom and + non-custom predicate callables return ``True`` the associated route + will be considered viable for a given request. If any predicate + callable returns ``False``, route matching continues. Note that the + value ``info`` passed to a custom route predicate is a dictionary + containing matching information; see :ref:`custom_route_predicates` + for more information about ``info``. + + +Alternatives +~~~~~~~~~~~~ + +You can also add a :term:`route configuration` via: + +- Using the :meth:`pyramid.config.Configurator.add_handler` method. + +See Also +~~~~~~~~ + +See also :ref:`handlers_chapter`. diff --git a/pyramid/includes/meta.zcml b/pyramid/includes/meta.zcml index bd16c3ea6..58af814ef 100644 --- a/pyramid/includes/meta.zcml +++ b/pyramid/includes/meta.zcml @@ -35,6 +35,12 @@ /> <meta:directive + name="handler" + schema="pyramid.zcml.IHandlerDirective" + handler="pyramid.zcml.handler" + /> + + <meta:directive name="resource" schema="pyramid.zcml.IResourceDirective" handler="pyramid.zcml.resource" diff --git a/pyramid/tests/test_zcml.py b/pyramid/tests/test_zcml.py index 33a67873f..8bc7c3eac 100644 --- a/pyramid/tests/test_zcml.py +++ b/pyramid/tests/test_zcml.py @@ -639,6 +639,86 @@ class TestRouteDirective(unittest.TestCase): context = self.config._ctx self.assertRaises(ConfigurationError, self._callFUT, context, 'name') +class TestHandlerDirective(unittest.TestCase): + def setUp(self): + self.config = testing.setUp(autocommit=False) + self.config._ctx = self.config._make_context() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, *arg, **kw): + from pyramid.zcml import handler + return handler(*arg, **kw) + + def _assertRoute(self, name, pattern, num_predicates=0): + from pyramid.interfaces import IRoutesMapper + reg = self.config.registry + mapper = reg.getUtility(IRoutesMapper) + routes = mapper.get_routes() + route = routes[0] + self.assertEqual(len(routes), 1) + self.assertEqual(route.name, name) + self.assertEqual(route.pattern, pattern) + self.assertEqual(len(routes[0].predicates), num_predicates) + return route + + def test_it(self): + from pyramid.view import action + from zope.interface import Interface + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRouteRequest + reg = self.config.registry + context = self.config._ctx + class Handler(object): # pragma: no cover + def __init__(self, request): + self.request = request + action(renderer='json') + def one(self): + return 'OK' + action(renderer='json') + def two(self): + return 'OK' + self._callFUT(context, 'name', '/:action', Handler) + actions = extract_actions(context.actions) + self.assertEqual(len(actions), 3) + + route_action = actions[0] + route_discriminator = route_action['discriminator'] + self.assertEqual(route_discriminator, + ('route', 'name', False, None, None, None, None,None)) + self._assertRoute('name', '/:action') + + view_action = actions[1] + request_type = reg.getUtility(IRouteRequest, 'name') + view_discriminator = view_action['discriminator'] + discrim = ('view', None, '', None, IView, None, None, None, 'name', + 'one', False, None, None, None) + self.assertEqual(view_discriminator[:14], discrim) + view_action['callable'](*view_action['args'], **view_action['kw']) + + view_action = actions[2] + request_type = reg.getUtility(IRouteRequest, 'name') + view_discriminator = view_action['discriminator'] + discrim = ('view', None, '', None, IView, None, None, None, 'name', + 'two', False, None, None, None) + self.assertEqual(view_discriminator[:14], discrim) + view_action['callable'](*view_action['args'], **view_action['kw']) + + wrapped = reg.adapters.lookup( + (IViewClassifier, request_type, Interface), IView, name='') + self.failUnless(wrapped) + + def test_pattern_is_None(self): + from pyramid.exceptions import ConfigurationError + + context = self.config._ctx + class Handler(object): + pass + self.assertRaises(ConfigurationError, self._callFUT, + context, 'name', None, Handler) + class TestStaticDirective(unittest.TestCase): def setUp(self): self.config = testing.setUp(autocommit=False) diff --git a/pyramid/zcml.py b/pyramid/zcml.py index 07dc3f0b5..eb7c11d10 100644 --- a/pyramid/zcml.py +++ b/pyramid/zcml.py @@ -178,13 +178,11 @@ def view( _view = view # for directives that take a view arg -class IRouteDirective(Interface): + +class IRouteLikeDirective(Interface): """ The interface for the ``route`` ZCML directive """ - name = TextLine(title=u'name', required=True) pattern = TextLine(title=u'pattern', required=False) - # alias for pattern - path = TextLine(title=u'path', required=False) factory = GlobalObject(title=u'context factory', required=False) view = GlobalObject(title=u'view', required=False) @@ -224,6 +222,11 @@ class IRouteDirective(Interface): ) use_global_views = Bool(title=u'use_global_views', required=False) +class IRouteDirective(IRouteLikeDirective): + name = TextLine(title=u'name', required=True) + # alias for pattern + path = TextLine(title=u'path', required=False) + def route(_context, name, pattern=None, @@ -287,6 +290,74 @@ def route(_context, traverse=traverse, ) +class IHandlerDirective(IRouteLikeDirective): + route_name = TextLine(title=u'route_name', required=True) + for_ = GlobalObject(title=u'handler', required=True) + action = TextLine(title=u"action", required=False) + +def handler(_context, + route_name, + pattern, + handler, + action=None, + view=None, + view_for=None, + permission=None, + factory=None, + for_=None, + header=None, + xhr=False, + accept=None, + path_info=None, + request_method=None, + request_param=None, + custom_predicates=(), + view_permission=None, + view_attr=None, + renderer=None, + view_renderer=None, + view_context=None, + traverse=None, + use_global_views=False): + """ Handle ``handler`` ZCML directives + """ + # the strange ordering of the request kw args above is for b/w + # compatibility purposes. + + # these are route predicates; if they do not match, the next route + # in the routelist will be tried + if view_context is None: + view_context = view_for or for_ + + view_permission = view_permission or permission + view_renderer = view_renderer or renderer + + if pattern is None: + raise ConfigurationError('handler directive must include a "pattern"') + + config = Configurator.with_context(_context) + config.add_handler( + route_name, + pattern, + handler, + action=action, + factory=factory, + header=header, + xhr=xhr, + accept=accept, + path_info=path_info, + request_method=request_method, + request_param=request_param, + custom_predicates=custom_predicates, + view=view, + view_context=view_context, + view_permission=view_permission, + view_renderer=view_renderer, + view_attr=view_attr, + use_global_views=use_global_views, + traverse=traverse, + ) + class ISystemViewDirective(Interface): view = GlobalObject( title=u"", |
