Skip to content

Commit

Permalink
Subscriber predicates:
Browse files Browse the repository at this point in the history
- Add ``add_subscriber_predicate`` method to Configurator.

- Allow ``add_subscriber`` and ``subscriber`` venusian decorator to accept ``**predicates`` arguments.

- Document subscriber predicate feature.

- Share more code between view, route, and subscriber related method wrt predicates.
  • Loading branch information
mcdonc committed Aug 25, 2012
1 parent bf64f1e commit 95f766b
Show file tree
Hide file tree
Showing 10 changed files with 405 additions and 101 deletions.
13 changes: 8 additions & 5 deletions CHANGES.txt
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ Bug Fixes
Features Features
-------- --------


- Third-party custom view and route predicates can now be added for use by - Third-party custom view, route, and subscriber predicates can now be added
view authors via ``pyramid.config.Configurator.add_view_predicate`` and for use by view authors via
``pyramid.config.Configurator.add_route_predicate``. So, for example, ``pyramid.config.Configurator.add_view_predicate``,
``pyramid.config.Configurator.add_route_predicate`` and
``pyramid.config.Configurator.add_subscriber_predicate``. So, for example,
doing this:: doing this::


config.add_view_predicate('abc', my.package.ABCPredicate) config.add_view_predicate('abc', my.package.ABCPredicate)
Expand All @@ -52,8 +54,9 @@ Features


@view_config(abc=1) @view_config(abc=1)


See "Adding A Third Party View or Route Predicate" in the Hooks chapter for Similar features exist for ``add_route``, and ``add_subscriber``. See
more information. "Adding A Third Party View, Route, or Subscriber Predicate" in the Hooks
chapter for more information.


Note that changes made to support the above feature now means that only Note that changes made to support the above feature now means that only
actions registered using the same "order" can conflict with one another. actions registered using the same "order" can conflict with one another.
Expand Down
3 changes: 2 additions & 1 deletion docs/glossary.rst
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -997,6 +997,7 @@ Glossary


predicate factory predicate factory
A callable which is used by a third party during the registration of a A callable which is used by a third party during the registration of a
route or view predicates to extend the view and route configuration route, view, or subscriber predicates to extend the configuration
system. See :ref:`registering_thirdparty_predicates` for more system. See :ref:`registering_thirdparty_predicates` for more
information. information.

122 changes: 112 additions & 10 deletions docs/narr/hooks.rst
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -1235,17 +1235,23 @@ implict and explicit tween chains used by an application. See


.. _registering_thirdparty_predicates: .. _registering_thirdparty_predicates:


Adding A Third Party View or Route Predicate Adding A Third Party View, Route, or Subscriber Predicate
-------------------------------------------- ---------------------------------------------------------


.. note:: .. note::


Third-party predicates are a feature new as of Pyramid 1.4. Third-party view, route, and subscriber predicates are a feature new as of
Pyramid 1.4.


View and route predicates used during view configuration allow you to narrow .. _view_and_route_predicates:
the set of circumstances under which a view or route will match. For
example, the ``request_method`` view predicate can be used to ensure a view View and Route Predicates
callable is only invoked when the request's method is ``POST``: ~~~~~~~~~~~~~~~~~~~~~~~~~

View and route predicates used during configuration allow you to narrow the
set of circumstances under which a view or route will match. For example,
the ``request_method`` view predicate can be used to ensure a view callable
is only invoked when the request's method is ``POST``:


.. code-block:: python .. code-block:: python
Expand Down Expand Up @@ -1286,9 +1292,9 @@ The first argument to :meth:`pyramid.config.Configurator.add_view_predicate`,
the name, is a string representing the name that is expected to be passed to the name, is a string representing the name that is expected to be passed to
``view_config`` (or its imperative analogue ``add_view``). ``view_config`` (or its imperative analogue ``add_view``).


The second argument is a predicate factory. A predicate factory is most The second argument is a view or route predicate factory. A view or route
often a class with a constructor (``__init__``), a ``text`` method, a predicate factory is most often a class with a constructor (``__init__``), a
``phash`` method and a ``__call__`` method. For example: ``text`` method, a ``phash`` method and a ``__call__`` method. For example:


.. code-block:: python .. code-block:: python
:linenos: :linenos:
Expand Down Expand Up @@ -1330,3 +1336,99 @@ You can use the same predicate factory as both a view predicate and as a
route predicate, but you'll need to call ``add_view_predicate`` and route predicate, but you'll need to call ``add_view_predicate`` and
``add_route_predicate`` separately with the same factory. ``add_route_predicate`` separately with the same factory.


.. _subscriber_predicates:

Subscriber Predicates
~~~~~~~~~~~~~~~~~~~~~

Subscriber predicates work almost exactly like view and route predicates.
They narrow the set of circumstances in which a subscriber will be called.
There are several minor differences between a subscriber predicate and a
view/route predicate:

- There are no default subscriber predicates. You must register one to use
one.

- The ``__call__`` method of a subscriber predicate accepts a single
``event`` object instead of a ``context`` and a ``request``.

- Not every subscriber predicate can be used with every event type. Some
subscriber predicates will assume a certain event type.

Here's an example of a subscriber predicate that can be used in conjunction
with a subscriber that subscribes to the :class:`pyramid.events.NewReqest`
event type.

.. code-block:: python
:linenos:
class RequestPathStartsWith(object):
def __init__(self, val, config):
self.val = val
def text(self):
return 'path_startswith = %s' % (self.val,)
phash = text
def __call__(self, event):
return event.request.path.startswith(self.val)
Once you've created a subscriber predicate, it may registered via
:meth:`pyramid.config.Configurator.add_subscriber_predicate`. For example:

.. code-block:: python
config.add_subscriber_predicate(
'request_path_startswith', RequestPathStartsWith)
Once a subscriber predicate is registered, you can use it in a call to
:meth:`pyramid.config.Configurator.add_subscriber` or to
:class:`pyramid.events.subscriber`. Here's an example of using the
previously registered ``request_path_startswith`` predicate in a call to
:meth:`~pyramid.config.Configurator.add_subscriber`:

.. code-block:: python
:linenos:
# define a subscriber in your code
def yosubscriber(event):
event.request.yo = 'YO!'
# and at configuration time
config.add_subscriber(yosubscriber, NewRequest,
request_path_startswith='/add_yo')
Here's the same subscriber/predicate/event-type combination used via
:class:`~pyramid.events.subscriber`.

.. code-block:: python
:linenos:
from pyramid.events import subscriber
@subscriber(NewRequest, request_path_startswith='/add_yo')
def yosubscriber(event):
event.request.yo = 'YO!'
In either of the above configurations, the ``yosubscriber`` callable will
only be called if the request path starts with ``/add_yo``. Otherwise the
event subscriber will not be called.

Note that the ``request_path_startswith`` subscriber you defined can be used
with events that have a ``request`` attribute, but not ones that do not. So,
for example, the predicate can be used with subscribers registered for
:class:`pyramid.events.NewRequest` and :class:`pyramid.events.ContextFound`
events, but it cannot be used with subscribers registered for
:class:`pyramid.events.ApplicationCreated` because the latter type of event
has no ``request`` attribute. The point being: unlike route and view
predicates, not every type of subscriber predicate will necessarily be
applicable for use in every subscriber registration. It is not the
responsibility of the predicate author to make every predicate make sense for
every event type; it is the responsibility of the predicate consumer to use
predicates that make sense for a particular event type registration.



29 changes: 29 additions & 0 deletions pyramid/config/__init__.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from pyramid.interfaces import ( from pyramid.interfaces import (
IDebugLogger, IDebugLogger,
IExceptionResponse, IExceptionResponse,
IPredicateList,
PHASE1_CONFIG,
) )


from pyramid.asset import resolve_asset_spec from pyramid.asset import resolve_asset_spec
Expand Down Expand Up @@ -71,6 +73,7 @@
from pyramid.config.util import ( from pyramid.config.util import (
action_method, action_method,
ActionInfo, ActionInfo,
PredicateList,
) )
from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.views import ViewsConfiguratorMixin
from pyramid.config.zca import ZCAConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin
Expand Down Expand Up @@ -489,6 +492,32 @@ def _del_introspector(self):
_get_introspector, _set_introspector, _del_introspector _get_introspector, _set_introspector, _del_introspector
) )


def _get_predlist(self, name):
predlist = self.registry.queryUtility(IPredicateList, name=name)
if predlist is None:
predlist = PredicateList()
self.registry.registerUtility(predlist, IPredicateList, name=name)
return predlist

def _add_predicate(self, type, name, factory, weighs_more_than=None,
weighs_less_than=None):
discriminator = ('%s predicate' % type, name)
intr = self.introspectable(
'%s predicates' % type,
discriminator,
'%s predicate named %s' % (type, name),
'%s predicate' % type)
intr['name'] = name
intr['factory'] = factory
intr['weighs_more_than'] = weighs_more_than
intr['weighs_less_than'] = weighs_less_than
def register():
predlist = self._get_predlist(type)
predlist.add(name, factory, weighs_more_than=weighs_more_than,
weighs_less_than=weighs_less_than)
self.action(discriminator, register, introspectables=(intr,),
order=PHASE1_CONFIG) # must be registered early

@property @property
def action_info(self): def action_info(self):
info = self.info # usually a ZCML action (ParserInfo) if self.info info = self.info # usually a ZCML action (ParserInfo) if self.info
Expand Down
94 changes: 79 additions & 15 deletions pyramid/config/adapters.py
Original file line number Original file line Diff line number Diff line change
@@ -1,3 +1,5 @@
from functools import update_wrapper

from zope.interface import Interface from zope.interface import Interface


from pyramid.interfaces import ( from pyramid.interfaces import (
Expand All @@ -6,40 +8,103 @@
IResourceURL, IResourceURL,
) )


from pyramid.config.util import action_method from pyramid.config.util import (
action_method,
)



class AdaptersConfiguratorMixin(object): class AdaptersConfiguratorMixin(object):
@action_method @action_method
def add_subscriber(self, subscriber, iface=None): def add_subscriber(self, subscriber, iface=None, **predicates):
"""Add an event :term:`subscriber` for the event stream """Add an event :term:`subscriber` for the event stream
implied by the supplied ``iface`` interface. The implied by the supplied ``iface`` interface.
``subscriber`` argument represents a callable object (or a
:term:`dotted Python name` which identifies a callable); it The ``subscriber`` argument represents a callable object (or a
will be called with a single object ``event`` whenever :term:`dotted Python name` which identifies a callable); it will be
:app:`Pyramid` emits an :term:`event` associated with the called with a single object ``event`` whenever :app:`Pyramid` emits
``iface``, which may be an :term:`interface` or a class or a an :term:`event` associated with the ``iface``, which may be an
:term:`dotted Python name` to a global object representing an :term:`interface` or a class or a :term:`dotted Python name` to a
interface or a class. Using the default ``iface`` value, global object representing an interface or a class.
``None`` will cause the subscriber to be registered for all
event types. See :ref:`events_chapter` for more information Using the default ``iface`` value, ``None`` will cause the subscriber
about events and subscribers.""" to be registered for all event types. See :ref:`events_chapter` for
more information about events and subscribers.
Any number of predicate keyword arguments may be passed in
``**predicates``. Each predicate named will narrow the set of
circumstances that the subscriber will be invoked. Each named
predicate must have been registered via
:meth:`pyramid.config.Configurator.add_subscriber_predicate` before it
can be used. See :ref:`subscriber_predicates` for more information.
.. note::
THe ``**predicates`` argument is new as of Pyramid 1.4.
"""
dotted = self.maybe_dotted dotted = self.maybe_dotted
subscriber, iface = dotted(subscriber), dotted(iface) subscriber, iface = dotted(subscriber), dotted(iface)
if iface is None: if iface is None:
iface = (Interface,) iface = (Interface,)
if not isinstance(iface, (tuple, list)): if not isinstance(iface, (tuple, list)):
iface = (iface,) iface = (iface,)

def register(): def register():
self.registry.registerHandler(subscriber, iface) predlist = self._get_predlist('subscriber')
order, preds, phash = predlist.make(self, **predicates)
intr.update({'phash':phash, 'order':order, 'predicates':preds})
derived_subscriber = self._derive_subscriber(subscriber, preds)
self.registry.registerHandler(derived_subscriber, iface)

intr = self.introspectable('subscribers', intr = self.introspectable('subscribers',
id(subscriber), id(subscriber),
self.object_description(subscriber), self.object_description(subscriber),
'subscriber') 'subscriber')

intr['subscriber'] = subscriber intr['subscriber'] = subscriber
intr['interfaces'] = iface intr['interfaces'] = iface

self.action(None, register, introspectables=(intr,)) self.action(None, register, introspectables=(intr,))
return subscriber return subscriber


def _derive_subscriber(self, subscriber, predicates):
if not predicates:
return subscriber
def subscriber_wrapper(event):
if all((predicate(event) for predicate in predicates)):
subscriber(event)
if hasattr(subscriber, '__name__'):
update_wrapper(subscriber_wrapper, subscriber)
return subscriber_wrapper

@action_method
def add_subscriber_predicate(self, name, factory, weighs_more_than=None,
weighs_less_than=None):
"""
Adds a subscriber predicate factory. The associated subscriber
predicate can later be named as a keyword argument to
:meth:`pyramid.config.Configurator.add_subscriber` in the
``**predicates`` anonyous keyword argument dictionary.
``name`` should be the name of the predicate. It must be a valid
Python identifier (it will be used as a ``**predicates`` keyword
argument to :meth:`~pyramid.config.Configurator.add_subscriber`).
``factory`` should be a :term:`predicate factory`.
See :ref:`subscriber_predicates` for more information.
.. note::
This method is new as of Pyramid 1.4.
"""
self._add_predicate(
'subscriber',
name,
factory,
weighs_more_than=weighs_more_than,
weighs_less_than=weighs_less_than
)

@action_method @action_method
def add_response_adapter(self, adapter, type_or_iface): def add_response_adapter(self, adapter, type_or_iface):
""" When an object of type (or interface) ``type_or_iface`` is """ When an object of type (or interface) ``type_or_iface`` is
Expand Down Expand Up @@ -203,4 +268,3 @@ def register(resource_iface=resource_iface):
intr['resource_iface'] = resource_iface intr['resource_iface'] = resource_iface
self.action(discriminator, register, introspectables=(intr,)) self.action(discriminator, register, introspectables=(intr,))



Loading

0 comments on commit 95f766b

Please sign in to comment.