diff options
| author | Chris McDonough <chrism@plope.com> | 2011-08-08 23:27:21 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-08-08 23:27:21 -0400 |
| commit | 932fc55e19b47ee670fa608c268a35738e499487 (patch) | |
| tree | 113f47df27536eb7b069594cbd482458f3d5b82c | |
| parent | 7a83ba1c6d997829f13b20676cbaaf0a6ee488ab (diff) | |
| parent | 05f610e6ed66f8d5aca9d77ae0748feb0c8f8479 (diff) | |
| download | pyramid-932fc55e19b47ee670fa608c268a35738e499487.tar.gz pyramid-932fc55e19b47ee670fa608c268a35738e499487.tar.bz2 pyramid-932fc55e19b47ee670fa608c268a35738e499487.zip | |
Merge branch 'topotween'
| -rw-r--r-- | docs/api/tweens.rst | 17 | ||||
| -rw-r--r-- | docs/glossary.rst | 14 | ||||
| -rw-r--r-- | docs/narr/commandline.rst | 74 | ||||
| -rw-r--r-- | docs/narr/hooks.rst | 208 | ||||
| -rw-r--r-- | pyramid/config.py | 158 | ||||
| -rw-r--r-- | pyramid/paster.py | 52 | ||||
| -rw-r--r-- | pyramid/router.py | 230 | ||||
| -rw-r--r-- | pyramid/tests/test_config.py | 117 | ||||
| -rw-r--r-- | pyramid/tests/test_paster.py | 33 | ||||
| -rw-r--r-- | pyramid/tests/test_router.py | 4 | ||||
| -rw-r--r-- | pyramid/tests/test_tweens.py | 286 | ||||
| -rw-r--r-- | pyramid/tweens.py | 175 |
12 files changed, 1020 insertions, 348 deletions
diff --git a/docs/api/tweens.rst b/docs/api/tweens.rst index 5fc15cb00..ddacd2cde 100644 --- a/docs/api/tweens.rst +++ b/docs/api/tweens.rst @@ -6,3 +6,20 @@ .. automodule:: pyramid.tweens .. autofunction:: excview_tween_factory + + .. attribute:: MAIN + + Constant representing the main Pyramid handling function, for use in + ``under`` and ``over`` arguments to + :meth:`pyramid.config.Configurator.add_tween`. + + .. attribute:: INGRESS + + Constant representing the request ingress, for use in ``under`` and + ``over`` arguments to :meth:`pyramid.config.Configurator.add_tween`. + + .. attribute:: EXCVIEW + + Constant representing the exception view tween, for use in ``under`` + and ``over`` arguments to + :meth:`pyramid.config.Configurator.add_tween`. diff --git a/docs/glossary.rst b/docs/glossary.rst index ccb62bbc8..f0ad81ded 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -920,11 +920,13 @@ Glossary tween A bit of code that sits between the Pyramid router's main request handling function and the upstream WSGI component that uses - :app:`Pyramid` as its 'app'. A tween may be used by Pyramid framework - extensions, to provide, for example, Pyramid-specific view timing - support bookkeeping code that examines exceptions before they are - returned to the upstream WSGI application. Tweens behave a bit like - :mod:`WSGI` 'middleware' but they have the benefit of running in a + :app:`Pyramid` as its 'app'. The word "tween" is a contraction of + "between". A tween may be used by Pyramid framework extensions, to + provide, for example, Pyramid-specific view timing support, bookkeeping + code that examines exceptions before they are returned to the upstream + WSGI application, or a variety of other features. Tweens behave a bit + like :mod:`WSGI` 'middleware' but they have the benefit of running in a context in which they have access to the Pyramid :term:`application - registry` as well as the Pyramid rendering machinery. + registry` as well as the Pyramid rendering machinery. See + :ref:`registering_tweens`. diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 6f969196f..0f23c153c 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -306,15 +306,16 @@ is executed. Displaying "Tweens" ------------------- -A user can get a representation of both the implicit :term:`tween` ordering -(the ordering specified by calls to -:meth:`pyramid.config.Configurator.add_tween`) and the explicit tween -ordering (specified by the ``pyramid.tweens`` configuration setting) -orderings using the ``paster ptweens`` command. Handler factories which are -functions or classes will show up as a standard Python dotted name in the -``paster ptweens`` output. Tween factories which are *instances* will show -their module and class name; the Python object id of the instance will be -appended. +A :term:`tween` is a bit of code that sits between the main Pyramid +application request handler and the WSGI application which calls it. A user +can get a representation of both the implicit tween ordering (the ordering +specified by calls to :meth:`pyramid.config.Configurator.add_tween`) and the +explicit tween ordering (specified by the ``pyramid.tweens`` configuration +setting) orderings using the ``paster ptweens`` command. Handler factories +which are functions or classes will show up as a standard Python dotted name +in the ``paster ptweens`` output. Tween factories which are *instances* will +show their module and class name; the Python object id of the instance will +be appended. For example, here's the ``paster pwteens`` command run against a system configured without any explicit tweens: @@ -322,12 +323,17 @@ configured without any explicit tweens: .. code-block:: text :linenos: - [chrism@thinko starter]$ ../bin/paster ptweens development.ini + [chrism@thinko pyramid]$ paster ptweens development.ini "pyramid.tweens" config value NOT set (implicitly ordered tweens used) - Position Name - -------- ---- - 0 pyramid.router.excview_tween_factory + Implicit Tween Chain + + Position Name Alias + -------- ---- ----- + - - INGRESS + 0 pyramid_debugtoolbar.toolbar.toolbar_tween_factory pdbt + 1 pyramid.tweens.excview_tween_factory excview + - - MAIN Here's the ``paster pwteens`` command run against a system configured *with* explicit tweens defined in its ``development.ini`` file: @@ -335,22 +341,27 @@ explicit tweens defined in its ``development.ini`` file: .. code-block:: text :linenos: - [chrism@thinko starter]$ ../bin/paster ptweens development.ini + [chrism@thinko pyramid]$ paster ptweens development.ini "pyramid.tweens" config value set (explicitly ordered tweens used) Explicit Tween Chain (used) - Position Name - -------- ---- - 0 pyramid.tweens.excview_tween_factory - 1 starter.tween_factory1 - 2 starter.tween_factory2 + Position Name + -------- ---- + - INGRESS + 0 starter.tween_factory2 + 1 starter.tween_factory1 + 2 pyramid.tweens.excview_tween_factory + - MAIN Implicit Tween Chain (not used) - Position Name - -------- ---- - 0 pyramid.tweens.excview_tween_factory + Position Name Alias + -------- ---- ----- + - - INGRESS + 0 pyramid_debugtoolbar.toolbar.toolbar_tween_factory pdbt + 1 pyramid.tweens.excview_tween_factory excview + - - MAIN Here's the application configuration section of the ``development.ini`` used by the above ``paster ptweens`` command which reprorts that the explicit @@ -361,15 +372,18 @@ tween chain is used: [app:starter] use = egg:starter - pyramid.reload_templates = true - pyramid.debug_authorization = false - pyramid.debug_notfound = false - pyramid.debug_routematch = false - pyramid.debug_templates = true - pyramid.default_locale_name = en - pyramid.tweens = pyramid.tweens.excview_tween_factory + reload_templates = true + debug_authorization = false + debug_notfound = false + debug_routematch = false + debug_templates = true + default_locale_name = en + pyramid.include = pyramid_debugtoolbar + pyramid.tweens = starter.tween_factory2 starter.tween_factory1 - starter.tween_factory2 + pyramid.tweens.excview_tween_factory + +See :ref:`registering_tweens` for more information about tweens. .. _writing_a_script: diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 889e8d6d8..f7ee82821 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -836,11 +836,11 @@ Registering "Tweens" .. note:: Tweens are a feature which were added in Pyramid 1.1.1. They are not available in any previous version. -A :term:`tween` (think: "between") is a bit of code that sits between the -Pyramid router's main request handling function and the upstream WSGI -component that uses :app:`Pyramid` as its "app". This is a feature that may -be used by Pyramid framework extensions, to provide, for example, -Pyramid-specific view timing support bookkeeping code that examines +A :term:`tween` (a contraction of the word "between") is a bit of code that +sits between the Pyramid router's main request handling function and the +upstream WSGI component that uses :app:`Pyramid` as its "app". This is a +feature that may be used by Pyramid framework extensions, to provide, for +example, Pyramid-specific view timing support bookkeeping code that examines exceptions before they are returned to the upstream WSGI application. Tweens behave a bit like :term:`WSGI` middleware but they have the benefit of running in a context in which they have access to the Pyramid @@ -857,8 +857,8 @@ Configurator. A tween factory must return a tween when it is called. A tween is a callable which accepts a :term:`request` object and returns a two-tuple a :term:`response` object. -Once you've created a tween factory, you can register it using the -:meth:`pyramid.config.Configurator.add_tween` method. +Once you've created a tween factory, you can register it into the implicit +tween chain using the :meth:`pyramid.config.Configurator.add_tween` method. Here's an example creating a tween factory and registering it: @@ -897,30 +897,145 @@ The ``request`` argument to a tween will be the request created by Pyramid's router when it receives a WSGI request. If more than one call to :meth:`pyramid.config.Configurator.add_tween` is -made within a single application configuration, the added tweens will be -chained together. The first tween factory added will be called with the -default Pyramid request handler as its ``handler`` argument, the second tween -factory added will be called with the result of the first tween factory as -its ``handler`` argument, and so on, ad infinitum. The Pyramid router will -use the outermost tween produced by this chain (the tween generated by the -very last tween factory added) as its request handler function. - -Pyramid will prevent the same tween factory from being added to the implicit -tween chain more than once using configuration conflict detection. If you -wish to add the same tween factory more than once in a configuration, you -should either: a) use a tween factory that is an instance rather than a -function or class, b) use a function or class as a tween factory with the -same logic as the other tween factory it conflicts with but with a different -``__name__`` attribute or c) call :meth:`pyramid.config.Configurator.commit` -between calls to :meth:`pyramid.config.Configurator.add_tween`. - -By default, the ordering of the chain is controlled entirely by the relative -ordering of calls to :meth:`pyramid.config.Configurator.add_tween`. However, -the deploying user can override tween inclusion and ordering entirely in his -Pyramid configuration using the ``pyramid.tweens`` settings value. When -used, this settings value will be a list of Python dotted names which imply -the ordering (and inclusion) of tween factories in the tween chain. For -example: +made within a single application configuration, the tweens will be chained +together at application startup time. The *first* tween factory added via +``add_tween`` will be called with the Pyramid exception view tween factory as +its ``handler`` argument, then the tween factory added directly after that +one will be called with the result of the first tween factory as its +``handler`` argument, and so on, ad infinitum until all tween factories have +been called. The Pyramid router will use the outermost tween produced by this +chain (the tween generated by the very last tween factory added) as its +request handler function. For example: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + config = Configurator() + config.add_tween('myapp.tween_factory1') + config.add_tween('myapp.tween_factory2') + +The above example will generate an implicit tween chain that looks like +this:: + + INGRESS (implicit) + myapp.tween_factory2 + myapp.tween_factory1 + pyramid.tweens.excview_tween_factory (implicit) + MAIN (implicit) + +By default, as described above, the ordering of the chain is controlled +entirely by the relative ordering of calls to +:meth:`pyramid.config.Configurator.add_tween`. However, the caller of +add_tween can provide an optional hint that can influence the implicit tween +chain ordering by supplying ``under`` or ``over`` (or both) arguments to +:meth:`~pyramid.config.Configurator.add_tween`. These hints are only used +used when an explicit tween chain is not used (when the ``pyramid.tweens`` +configuration value is not set). + +Allowable values for ``under`` or ``over`` (or both) are: + +- ``None`` (the default). + +- A :term:`dotted Python name` to a tween factory: a string representing the + predicted dotted name of a tween factory added in a call to ``add_tween`` + in the same configuration session. + +- A "tween alias": a string representing the predicted value of ``alias`` in + a separate call to ``add_tween`` in the same configuration session + +- One of the constants :attr:`pyramid.tweens.MAIN`, + :attr:`pyramid.tweens.INGRESS`, or :attr:`pyramid.tweens.EXCVIEW`. + +Effectively, ``under`` means "closer to the main Pyramid application than", +``over`` means "closer to the request ingress than". + +For example, the following call to +:meth:`~pyramid.config.Configurator.add_tween` will attempt to place the +tween factory represented by ``myapp.tween_factory`` directly 'above' (in +``paster ptweens`` order) the main Pyramid request handler. + +.. code-block:: python + :linenos: + + import pyramid.tweens + + config.add_tween('myapp.tween_factory', over=pyramid.tweens.MAIN) + +The above example will generate an implicit tween chain that looks like +this:: + + INGRESS (implicit) + pyramid.tweens.excview_tween_factory (implicit) + myapp.tween_factory + MAIN (implicit) + +Likewise, calling the following call to +:meth:`~pyramid.config.Configurator.add_tween` will attempt to place this +tween factory 'above' the main handler but 'below' a separately added tween +factory: + +.. code-block:: python + :linenos: + + import pyramid.tweens + + config.add_tween('myapp.tween_factory1', + over=pyramid.tweens.MAIN) + config.add_tween('myapp.tween_factory2', + over=pyramid.tweens.MAIN, + under='myapp.tween_factory1') + +The above example will generate an implicit tween chain that looks like +this:: + + INGRESS (implicit) + pyramid.tweens.excview_tween_factory (implicit) + myapp.tween_factory1 + myapp.tween_factory2 + MAIN (implicit) + +Specifying neither ``over`` nor ``under`` is equivalent to specifying +``under=INGRESS``. + +If an ``under`` or ``over`` value is provided that does not match a tween +factory dotted name or alias in the current configuration, that value will be +ignored. It is not an error to provide an ``under`` or ``over`` value that +matches an unused tween factory. + +:meth:`~pyramid.config.Configurator.add_tween` also accepts an ``alias`` +argument. If ``alias`` is not ``None``, should be a string. The string will +represent a value that other callers of ``add_tween`` may pass as an +``under`` and ``over`` argument instead of a dotted name to a tween factory. +For example: + +.. code-block:: python + :linenos: + + import pyramid.tweens + + config.add_tween('myapp.tween_factory1', + alias='one' + over=pyramid.tweens.MAIN) + config.add_tween('myapp.tween_factory2', + alias='two' + over=pyramid.tweens.MAIN, + under='one') + +Alias names are only useful in relation to ``under`` and ``over`` values. +They cannot be used in explicit tween chain configuration, or anywhere else. + +Implicit tween ordering is obviously only best-effort. Pyramid will attempt +to provide an implicit order of tweens as best it can using hints provided by +calls to :meth:`~pyramid.config.Configurator.add_tween`, but because it's +only best-effort, if very precise tween ordering is required, the only +surefire way to get it is to use an explicit tween order. The deploying user +can override the implicit tween inclusion and ordering implied by calls to +:meth:`~pyramid.config.Configurator.add_tween` entirely by using the +``pyramid.tweens`` settings value. When used, this settings value must be a +list of Python dotted names which will override the ordering (and inclusion) +of tween factories in the implicit tween chain. For example: .. code-block:: ini :linenos: @@ -932,17 +1047,19 @@ example: pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.debug_templates = true - pyramid.tweens = pyramid.tweens.excview_tween_factory - myapp.my_cool_tween_factory + pyramid.tweens = myapp.my_cool_tween_factory + pyramid.tweens.excview_tween_factory In the above configuration, calls made during configuration to :meth:`pyramid.config.Configurator.add_tween` are ignored, and the user is telling the system to use the tween factories he has listed in the -``pyramid.tweens`` configuration setting (each is a:term:`Python dotted name` -which points to a tween factory). The *last* tween factory in the -``pyramid.tweens`` list will be used as the producer of the effective +``pyramid.tweens`` configuration setting (each is a :term:`dotted Python +name` which points to a tween factory) instead of any tween factories added +via :meth:`pyramid.config.Configurator.add_tween`. The *first* tween factory +in the ``pyramid.tweens`` list will be used as the producer of the effective :app:`Pyramid` request handling function; it will wrap the tween factory -declared directly "above" it, ad infinitum. +declared directly "below" it, ad infinitum. The "main" Pyramid request +handler is implicit, and always "at the bottom". .. note:: Pyramid's own :term:`exception view` handling logic is implemented as a tween factory function: :func:`pyramid.tweens.excview_tween_factory`. @@ -952,6 +1069,19 @@ declared directly "above" it, ad infinitum. ``pyramid.tweens`` configuration setting list explicitly. If it is not present, Pyramid will not perform exception view handling. -The ``paster ptweens`` command-line utility can be used to report the current -tween chain used by an application. See :ref:`displaying_tweens`. +Pyramid will prevent the same tween factory from being added to the tween +chain more than once using configuration conflict detection. If you wish to +add the same tween factory more than once in a configuration, you should +either: a) use a tween factory that is an instance rather than a function or +class, b) use a function or class as a tween factory with the same logic as +the other tween factory it conflicts with but with a different ``__name__`` +attribute or c) call :meth:`pyramid.config.Configurator.commit` between calls +to :meth:`pyramid.config.Configurator.add_tween`. +If a cycle is detected in implicit tween ordering when ``over`` and ``under`` +are used in any call to "add_tween", an exception will be raised at startup +time. + +The ``paster ptweens`` command-line utility can be used to report the current +implict and explicit tween chains used by an application. See +:ref:`displaying_tweens`. diff --git a/pyramid/config.py b/pyramid/config.py index 3fac6847d..45c5b743f 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -80,6 +80,9 @@ from pyramid.traversal import DefaultRootFactory from pyramid.traversal import find_interface from pyramid.traversal import traversal_path from pyramid.tweens import excview_tween_factory +from pyramid.tweens import Tweens +from pyramid.tweens import tween_factory_name +from pyramid.tweens import MAIN, INGRESS, EXCVIEW from pyramid.urldispatch import RoutesMapper from pyramid.util import DottedNameResolver from pyramid.util import WeakOrderedSet @@ -733,9 +736,6 @@ class Configurator(object): # cope with WebOb exc objects not decoratored with IExceptionResponse from webob.exc import WSGIHTTPException as WebobWSGIHTTPException registry.registerSelfAdapter((WebobResponse,), IResponse) - # add a handler manager - for factory in tweens: - self._add_tween(factory, explicit=True) if debug_logger is None: debug_logger = logging.getLogger(self.package_name) @@ -784,6 +784,8 @@ class Configurator(object): self.commit() for inc in includes: self.include(inc) + for factory in tweens: + self._add_tween(factory, explicit=True) def hook_zca(self): """ Call :func:`zope.component.getSiteManager.sethook` with @@ -903,40 +905,110 @@ class Configurator(object): return self._derive_view(view, attr=attr, renderer=renderer) @action_method - def add_tween(self, tween_factory): + def add_tween(self, tween_factory, alias=None, under=None, over=None): """ - Add a 'tween factory'. A :term:`tween` (think: 'between') is a bit - of code that sits between the Pyramid router's main request handling - function and the upstream WSGI component that uses :app:`Pyramid` as - its 'app'. This is a feature that may be used by Pyramid framework - extensions, to provide, for example, Pyramid-specific view timing - support bookkeeping code that examines exceptions before they are - returned to the upstream WSGI application. Tweens behave a bit like + Add a 'tween factory'. A :term:`tween` (a contraction of 'between') + is a bit of code that sits between the Pyramid router's main request + handling function and the upstream WSGI component that uses + :app:`Pyramid` as its 'app'. This is a feature that may be used by + Pyramid framework extensions, to provide, for example, + Pyramid-specific view timing support, bookkeeping code that examines + exceptions before they are returned to the upstream WSGI application, + or a variety of other features. Tweens behave a bit like :term:`WSGI` 'middleware' but they have the benefit of running in a context in which they have access to the Pyramid :term:`application registry` as well as the Pyramid rendering machinery. + .. note:: You can view the tween ordering configured into a given + Pyramid application by using the ``paster ptweens`` + command. See :ref:`displaying_tweens`. + + The ``alias`` argument, if it is not ``None``, should be a string. + The string will represent a value that other callers of ``add_tween`` + may pass as an ``under`` and ``over`` argument instead of a dotted + name to a tween factory. + + The ``under`` and ``over`` arguments allow the caller of + ``add_tween`` to provide a hint about where in the tween chain this + tween factory should be placed when an implicit tween chain is used. + These hints are only used used when an explicit tween chain is not + used (when the ``pyramid.tweens`` configuration value is not set). + Allowable values for ``under`` or ``over`` (or both) are: + + - ``None`` (the default). + + - A :term:`dotted Python name` to a tween factory: a string + representing the predicted dotted name of a tween factory added in + a call to ``add_tween`` in the same configuration session. + + - A tween alias: a string representing the predicted value of + ``alias`` in a separate call to ``add_tween`` in the same + configuration session + + - One of the constants :attr:`pyramid.tweens.MAIN`, + :attr:`pyramid.tweens.INGRESS`, or :attr:`pyramid.tweens.EXCVIEW`. + + ``under`` means 'closer to the main Pyramid application than', + ``over`` means 'closer to the request ingress than'. + + For example, calling ``add_tween(factory, over=pyramid.tweens.MAIN)`` + will attempt to place the tween factory represented by ``factory`` + directly 'above' (in ``paster ptweens`` order) the main Pyramid + request handler. Likewise, calling ``add_tween(factory, + over=pyramid.tweens.MAIN, under='someothertween')`` will attempt to + place this tween factory 'above' the main handler but 'below' (a + fictional) 'someothertween' tween factory (which was presumably added + via ``add_tween(factory, alias='someothertween')``). + + If an ``under`` or ``over`` value is provided that does not match a + tween factory dotted name or alias in the current configuration, that + value will be ignored. It is not an error to provide an ``under`` or + ``over`` value that matches an unused tween factory. + + Specifying neither ``over`` nor ``under`` is equivalent to specifying + ``under=INGRESS``. + + Implicit tween ordering is obviously only best-effort. Pyramid will + attempt to present an implicit order of tweens as best it can, but + the only surefire way to get any particular ordering is to use an + explicit tween order. A user may always override the implicit tween + ordering by using an explicit ``pyramid.tweens`` configuration value + setting. + + ``alias``, ``under``, and ``over`` arguments are ignored when an + explicit tween chain is specified using the ``pyramid.tweens`` + configuration value. + For more information, see :ref:`registering_tweens`. .. note:: This feature is new as of Pyramid 1.1.1. """ - return self._add_tween(tween_factory, explicit=False) + return self._add_tween(tween_factory, alias=alias, under=under, + over=over, explicit=False) - def _add_tween(self, tween_factory, explicit): + def _add_tween(self, tween_factory, alias=None, under=None, over=None, + explicit=False): tween_factory = self.maybe_dotted(tween_factory) name = tween_factory_name(tween_factory) - def register(): - registry = self.registry - tweens = registry.queryUtility(ITweens) - if tweens is None: - tweens = Tweens() - registry.registerUtility(tweens, ITweens) - tweens.add( - tween_factory_name(excview_tween_factory), - excview_tween_factory, - explicit=False) - tweens.add(name, tween_factory, explicit) - self.action(('tween', name, explicit), register) + if alias in (MAIN, INGRESS): + raise ConfigurationError('%s is a reserved tween name' % alias) + + registry = self.registry + tweens = registry.queryUtility(ITweens) + if tweens is None: + tweens = Tweens() + registry.registerUtility(tweens, ITweens) + tweens.add_implicit(tween_factory_name(excview_tween_factory), + excview_tween_factory, alias=EXCVIEW, + over=MAIN) + if explicit: + tweens.add_explicit(name, tween_factory) + else: + tweens.add_implicit(name, tween_factory, alias=alias, under=under, + over=over) + self.action(('tween', name, explicit)) + if not explicit and alias is not None: + self.action(('tween', alias, explicit)) @action_method def add_request_handler(self, factory, name): # pragma: no cover @@ -3378,39 +3450,3 @@ def isexception(o): global_registries = WeakOrderedSet() -class Tweens(object): - implements(ITweens) - def __init__(self): - self.explicit = [] - self.implicit = [] - - def add(self, name, factory, explicit=False): - if explicit: - self.explicit.append((name, factory)) - else: - self.implicit.append((name, factory)) - - def __call__(self, handler, registry): - factories = self.implicit - if self.explicit: - factories = self.explicit - for name, factory in factories: - handler = factory(handler, registry) - return handler - -def tween_factory_name(factory): - if (hasattr(factory, '__name__') and - hasattr(factory, '__module__')): - # function or class - name = '.'.join([factory.__module__, - factory.__name__]) - elif hasattr(factory, '__module__'): - # instance - name = '.'.join([factory.__module__, - factory.__class__.__name__, - str(id(factory))]) - else: - raise ConfigurationError( - 'A tween factory must be a class, an instance, or a function; ' - '%s is not a suitable tween factory' % factory) - return name diff --git a/pyramid/paster.py b/pyramid/paster.py index 54f5a51a6..47f9ebc34 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -14,6 +14,9 @@ from pyramid.interfaces import ITweens from pyramid.scripting import prepare from pyramid.util import DottedNameResolver +from pyramid.tweens import MAIN +from pyramid.tweens import INGRESS + from pyramid.scaffolds import PyramidTemplate # bw compat zope.deprecation.deprecated( 'PyramidTemplate', ('pyramid.paster.PyramidTemplate was moved to ' @@ -567,6 +570,28 @@ class PTweensCommand(PCommand): def out(self, msg): # pragma: no cover print msg + + def show_implicit(self, tweens): + implicit = tweens.implicit() + fmt = '%-10s %-50s %-15s' + self.out(fmt % ('Position', 'Name', 'Alias')) + self.out(fmt % ( + '-'*len('Position'), '-'*len('Name'), '-'*len('Alias'))) + self.out(fmt % ('-', '-', INGRESS)) + for pos, (name, _) in enumerate(implicit): + alias = tweens.name_to_alias.get(name, None) + self.out(fmt % (pos, name, alias)) + self.out(fmt % ('-', '-', MAIN)) + + def show_explicit(self, tweens): + explicit = tweens.explicit + fmt = '%-10s %-65s' + self.out(fmt % ('Position', 'Name')) + self.out(fmt % ('-'*len('Position'), '-'*len('Name'))) + self.out(fmt % ('-', INGRESS)) + for pos, (name, _) in enumerate(explicit): + self.out(fmt % (pos, name)) + self.out(fmt % ('-', MAIN)) def command(self): config_uri = self.args[0] @@ -574,27 +599,22 @@ class PTweensCommand(PCommand): registry = env['registry'] tweens = self._get_tweens(registry) if tweens is not None: - ordering = [] - if tweens.explicit: + explicit = tweens.explicit + if explicit: self.out('"pyramid.tweens" config value set ' '(explicitly ordered tweens used)') self.out('') - ordering.append((tweens.explicit, - 'Explicit Tween Chain (used)')) - ordering.append((tweens.implicit, - 'Implicit Tween Chain (not used)')) + self.out('Explicit Tween Chain (used)') + self.out('') + self.show_explicit(tweens) + self.out('') + self.out('Implicit Tween Chain (not used)') + self.out('') + self.show_implicit(tweens) else: self.out('"pyramid.tweens" config value NOT set ' '(implicitly ordered tweens used)') self.out('') - ordering.append((tweens.implicit, '')) - for L, title in ordering: - if title: - self.out(title) - self.out('') - fmt = '%-8s %-30s' - self.out(fmt % ('Position', 'Name')) - self.out(fmt % ('-'*len('Position'), '-'*len('Name'))) - for pos, (name, item) in enumerate(L): - self.out(fmt % (pos, name)) + self.out('Implicit Tween Chain') self.out('') + self.show_implicit(tweens) diff --git a/pyramid/router.py b/pyramid/router.py index 79fd7992b..248ab3b5b 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -39,15 +39,11 @@ class Router(object): self.request_factory = q(IRequestFactory, default=Request) tweens = q(ITweens) if tweens is None: - self.handle_request = excview_tween_factory(self.handle_request, - registry) - else: - self.handle_request = tweens(self.handle_request, registry) - + tweens = excview_tween_factory + self.handle_request = tweens(self.handle_request, registry) self.root_policy = self.root_factory # b/w compat self.registry = registry settings = registry.settings - if settings is not None: self.debug_notfound = settings['debug_notfound'] self.debug_routematch = settings['debug_routematch'] @@ -56,117 +52,107 @@ class Router(object): attrs = request.__dict__ registry = attrs['registry'] - try: # matches finally: if request is not None - request.request_iface = IRequest - context = None - routes_mapper = self.routes_mapper - debug_routematch = self.debug_routematch - adapters = registry.adapters - has_listeners = registry.has_listeners - notify = registry.notify - logger = self.logger - - has_listeners and notify(NewRequest(request)) - # find the root object - root_factory = self.root_factory - if routes_mapper is not None: - info = routes_mapper(request) - match, route = info['match'], info['route'] - if route is None: - if debug_routematch: - msg = ('no route matched for url %s' % - request.url) - logger and logger.debug(msg) - else: - # TODO: kill off bfg.routes.* environ keys - # when traverser requires request arg, and - # cant cope with environ anymore (they are - # docs-deprecated as of BFG 1.3) - environ = request.environ - environ['bfg.routes.route'] = route - environ['bfg.routes.matchdict'] = match - attrs['matchdict'] = match - attrs['matched_route'] = route - - if debug_routematch: - msg = ( - 'route matched for url %s; ' - 'route_name: %r, ' - 'path_info: %r, ' - 'pattern: %r, ' - 'matchdict: %r, ' - 'predicates: %r' % ( - request.url, - route.name, - request.path_info, - route.pattern, match, - route.predicates) - ) - logger and logger.debug(msg) - - request.request_iface = registry.queryUtility( - IRouteRequest, - name=route.name, - default=IRequest) - - root_factory = route.factory or self.root_factory - - root = root_factory(request) - attrs['root'] = root - - # find a context - traverser = adapters.queryAdapter(root, ITraverser) - if traverser is None: - traverser = ResourceTreeTraverser(root) - tdict = traverser(request) - - context, view_name, subpath, traversed, vroot, vroot_path = ( - tdict['context'], - tdict['view_name'], - tdict['subpath'], - tdict['traversed'], - tdict['virtual_root'], - tdict['virtual_root_path'] - ) - - attrs.update(tdict) - has_listeners and notify(ContextFound(request)) - - # find a view callable - context_iface = providedBy(context) - view_callable = adapters.lookup( - (IViewClassifier, request.request_iface, context_iface), - IView, name=view_name, default=None) - - # invoke the view callable - if view_callable is None: - if self.debug_notfound: + request.request_iface = IRequest + context = None + routes_mapper = self.routes_mapper + debug_routematch = self.debug_routematch + adapters = registry.adapters + has_listeners = registry.has_listeners + notify = registry.notify + logger = self.logger + + has_listeners and notify(NewRequest(request)) + # find the root object + root_factory = self.root_factory + if routes_mapper is not None: + info = routes_mapper(request) + match, route = info['match'], info['route'] + if route is None: + if debug_routematch: + msg = ('no route matched for url %s' % + request.url) + logger and logger.debug(msg) + else: + # TODO: kill off bfg.routes.* environ keys + # when traverser requires request arg, and + # cant cope with environ anymore (they are + # docs-deprecated as of BFG 1.3) + environ = request.environ + environ['bfg.routes.route'] = route + environ['bfg.routes.matchdict'] = match + attrs['matchdict'] = match + attrs['matched_route'] = route + + if debug_routematch: msg = ( - 'debug_notfound of url %s; path_info: %r, ' - 'context: %r, view_name: %r, subpath: %r, ' - 'traversed: %r, root: %r, vroot: %r, ' - 'vroot_path: %r' % ( - request.url, request.path_info, context, - view_name, subpath, traversed, root, vroot, - vroot_path) + 'route matched for url %s; ' + 'route_name: %r, ' + 'path_info: %r, ' + 'pattern: %r, ' + 'matchdict: %r, ' + 'predicates: %r' % ( + request.url, + route.name, + request.path_info, + route.pattern, match, + route.predicates) ) logger and logger.debug(msg) - else: - msg = request.path_info - raise HTTPNotFound(msg) - else: - response = view_callable(context, request) - - has_listeners and notify(NewResponse(request, response)) - - if request.response_callbacks: - request._process_response_callbacks(response) - return response + request.request_iface = registry.queryUtility( + IRouteRequest, + name=route.name, + default=IRequest) + + root_factory = route.factory or self.root_factory + + root = root_factory(request) + attrs['root'] = root + + # find a context + traverser = adapters.queryAdapter(root, ITraverser) + if traverser is None: + traverser = ResourceTreeTraverser(root) + tdict = traverser(request) + + context, view_name, subpath, traversed, vroot, vroot_path = ( + tdict['context'], + tdict['view_name'], + tdict['subpath'], + tdict['traversed'], + tdict['virtual_root'], + tdict['virtual_root_path'] + ) + + attrs.update(tdict) + has_listeners and notify(ContextFound(request)) + + # find a view callable + context_iface = providedBy(context) + view_callable = adapters.lookup( + (IViewClassifier, request.request_iface, context_iface), + IView, name=view_name, default=None) + + # invoke the view callable + if view_callable is None: + if self.debug_notfound: + msg = ( + 'debug_notfound of url %s; path_info: %r, ' + 'context: %r, view_name: %r, subpath: %r, ' + 'traversed: %r, root: %r, vroot: %r, ' + 'vroot_path: %r' % ( + request.url, request.path_info, context, + view_name, subpath, traversed, root, vroot, + vroot_path) + ) + logger and logger.debug(msg) + else: + msg = request.path_info + raise HTTPNotFound(msg) + else: + response = view_callable(context, request) - finally: - if request is not None and request.finished_callbacks: - request._process_finished_callbacks() + return response def __call__(self, environ, start_response): """ @@ -177,14 +163,28 @@ class Router(object): return an iterable. """ registry = self.registry + has_listeners = self.registry.has_listeners + notify = self.registry.notify request = self.request_factory(environ) threadlocals = {'registry':registry, 'request':request} manager = self.threadlocal_manager manager.push(threadlocals) + request.registry = registry try: - request.registry = registry - response = self.handle_request(request) - return response(request.environ, start_response) + + try: + response = self.handle_request(request) + has_listeners and notify(NewResponse(request, response)) + + if request.response_callbacks: + request._process_response_callbacks(response) + + return response(request.environ, start_response) + + finally: + if request.finished_callbacks: + request._process_finished_callbacks() + finally: manager.pop() diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index d73fd7f7d..652fd94dd 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -592,6 +592,7 @@ pyramid.tests.test_config.dummy_include2""", 'pyramid.tweens': 'pyramid.tests.test_config.dummy_tween_factory' } config.setup_registry(settings=settings) + config.commit() tweens = config.registry.getUtility(ITweens) self.assertEqual(tweens.explicit, [('pyramid.tests.test_config.dummy_tween_factory', @@ -638,11 +639,36 @@ pyramid.tests.test_config.dummy_include2""", config.add_tween(factory2) config.commit() tweens = config.registry.queryUtility(ITweens) + implicit = tweens.implicit() self.assertEqual( - tweens.implicit, - [('pyramid.tweens.excview_tween_factory', excview_tween_factory), - ('pyramid.tests.test_config.factory1', factory1), - ('pyramid.tests.test_config.factory2', factory2)]) + implicit, + [ + ('pyramid.tests.test_config.factory2', factory2), + ('pyramid.tests.test_config.factory1', factory1), + ('pyramid.tweens.excview_tween_factory', excview_tween_factory), + ] + ) + + def test_add_tweens_names_with_underover(self): + from pyramid.interfaces import ITweens + from pyramid.tweens import excview_tween_factory + from pyramid.tweens import MAIN + def factory1(handler, registry): return handler + def factory2(handler, registry): return handler + config = self._makeOne() + config.add_tween(factory1, over=MAIN) + config.add_tween(factory2, over=MAIN, + under='pyramid.tests.test_config.factory1') + config.commit() + tweens = config.registry.queryUtility(ITweens) + implicit = tweens.implicit() + self.assertEqual( + implicit, + [ + ('pyramid.tweens.excview_tween_factory', excview_tween_factory), + ('pyramid.tests.test_config.factory1', factory1), + ('pyramid.tests.test_config.factory2', factory2), + ]) def test_add_tween_dottedname(self): from pyramid.interfaces import ITweens @@ -652,11 +678,11 @@ pyramid.tests.test_config.dummy_include2""", config.commit() tweens = config.registry.queryUtility(ITweens) self.assertEqual( - tweens.implicit, + tweens.implicit(), [ - ('pyramid.tweens.excview_tween_factory', excview_tween_factory), ('pyramid.tests.test_config.dummy_tween_factory', - dummy_tween_factory) + dummy_tween_factory), + ('pyramid.tweens.excview_tween_factory', excview_tween_factory), ]) def test_add_tween_instance(self): @@ -668,13 +694,15 @@ pyramid.tests.test_config.dummy_include2""", config.add_tween(atween) config.commit() tweens = config.registry.queryUtility(ITweens) - self.assertEqual(len(tweens.implicit), 2) + implicit = tweens.implicit() + self.assertEqual(len(implicit), 2) + self.assertTrue( + implicit[0][0].startswith( + 'pyramid.tests.test_config.ATween.')) + self.assertEqual(implicit[0][1], atween) self.assertEqual( - tweens.implicit[0], + implicit[1], ('pyramid.tweens.excview_tween_factory', excview_tween_factory)) - self.assertTrue( - tweens.implicit[1][0].startswith('pyramid.tests.test_config.ATween.')) - self.assertEqual(tweens.implicit[1][1], atween) def test_add_tween_unsuitable(self): from pyramid.exceptions import ConfigurationError @@ -682,6 +710,22 @@ pyramid.tests.test_config.dummy_include2""", config = self._makeOne() self.assertRaises(ConfigurationError, config.add_tween, pyramid.tests) + def test_add_tween_alias_ingress(self): + from pyramid.exceptions import ConfigurationError + from pyramid.tweens import INGRESS + config = self._makeOne() + self.assertRaises(ConfigurationError, + config.add_tween, 'pyramid.tests.test_config.dummy_tween_factory', + alias=INGRESS) + + def test_add_tween_alias_main(self): + from pyramid.exceptions import ConfigurationError + from pyramid.tweens import MAIN + config = self._makeOne() + self.assertRaises(ConfigurationError, + config.add_tween, 'pyramid.tests.test_config.dummy_tween_factory', + alias=MAIN) + def test_add_tweens_conflict(self): from zope.configuration.config import ConfigurationConflictError config = self._makeOne() @@ -689,6 +733,16 @@ pyramid.tests.test_config.dummy_include2""", config.add_tween('pyramid.tests.test_config.dummy_tween_factory') self.assertRaises(ConfigurationConflictError, config.commit) + def test_add_tweens_conflict_same_alias(self): + from zope.configuration.config import ConfigurationConflictError + class ATween(object): pass + atween1 = ATween() + atween2 = ATween() + config = self._makeOne() + config.add_tween(atween1, alias='a') + config.add_tween(atween2, alias='a') + self.assertRaises(ConfigurationConflictError, config.commit) + def test_add_subscriber_defaults(self): from zope.interface import implements from zope.interface import Interface @@ -5511,45 +5565,6 @@ class Test_isexception(unittest.TestCase): pass self.assertEqual(self._callFUT(ISubException), True) -class TestTweens(unittest.TestCase): - def _makeOne(self): - from pyramid.config import Tweens - return Tweens() - - def test_add_explicit(self): - tweens = self._makeOne() - tweens.add('name', 'factory', explicit=True) - self.assertEqual(tweens.explicit, [('name', 'factory')]) - tweens.add('name2', 'factory2', explicit=True) - self.assertEqual(tweens.explicit, [('name', 'factory'), - ('name2', 'factory2')]) - - def test_add_implicit(self): - tweens = self._makeOne() - tweens.add('name', 'factory', explicit=False) - self.assertEqual(tweens.implicit, [('name', 'factory')]) - tweens.add('name2', 'factory2', explicit=False) - self.assertEqual(tweens.implicit, [('name', 'factory'), - ('name2', 'factory2')]) - - def test___call___explicit(self): - tweens = self._makeOne() - def factory1(handler, registry): - return handler - def factory2(handler, registry): - return '123' - tweens.explicit = [('name', factory1), ('name', factory2)] - self.assertEqual(tweens(None, None), '123') - - def test___call___implicit(self): - tweens = self._makeOne() - def factory1(handler, registry): - return handler - def factory2(handler, registry): - return '123' - tweens.implicit = [('name', factory1), ('name', factory2)] - self.assertEqual(tweens(None, None), '123') - class DummyRequest: subpath = () matchdict = None diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index 58ed73d2c..36c3a51be 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -853,13 +853,9 @@ class TestPTweensCommand(unittest.TestCase): result = command.command() self.assertEqual(result, None) self.assertEqual( - L, - ['"pyramid.tweens" config value NOT set (implicitly ordered tweens used)', - '', - 'Position Name ', - '-------- ---- ', - '0 name ', - '']) + L[0], + '"pyramid.tweens" config value NOT set (implicitly ordered tweens ' + 'used)') def test_command_implicit_and_explicit_tweens(self): command = self._makeOne() @@ -870,22 +866,8 @@ class TestPTweensCommand(unittest.TestCase): result = command.command() self.assertEqual(result, None) self.assertEqual( - L, - ['"pyramid.tweens" config value set (explicitly ordered tweens used)', - '', - 'Explicit Tween Chain (used)', - '', - 'Position Name ', - '-------- ---- ', - '0 name2 ', - '', - 'Implicit Tween Chain (not used)', - '', - 'Position Name ', - '-------- ---- ', - '0 name ', - '' - ]) + L[0], + '"pyramid.tweens" config value set (explicitly ordered tweens used)') def test__get_tweens(self): command = self._makeOne() @@ -894,8 +876,11 @@ class TestPTweensCommand(unittest.TestCase): class DummyTweens(object): def __init__(self, implicit, explicit): - self.implicit = implicit + self._implicit = implicit self.explicit = explicit + self.name_to_alias = {} + def implicit(self): + return self._implicit class Dummy: pass diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 6b0354468..19134813f 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -159,8 +159,8 @@ class TestRouter(unittest.TestCase): wrapper.name = 'two' wrapper.child = handler return wrapper - tweens.add('one', tween_factory1) - tweens.add('two', tween_factory2) + tweens.add_implicit('one', tween_factory1) + tweens.add_implicit('two', tween_factory2) router = self._makeOne() self.assertEqual(router.handle_request.name, 'two') self.assertEqual(router.handle_request.child.name, 'one') diff --git a/pyramid/tests/test_tweens.py b/pyramid/tests/test_tweens.py new file mode 100644 index 000000000..a626afa11 --- /dev/null +++ b/pyramid/tests/test_tweens.py @@ -0,0 +1,286 @@ +import unittest + +class TestTweens(unittest.TestCase): + def _makeOne(self): + from pyramid.config import Tweens + return Tweens() + + def test_add_explicit(self): + tweens = self._makeOne() + tweens.add_explicit('name', 'factory') + self.assertEqual(tweens.explicit, [('name', 'factory')]) + tweens.add_explicit('name2', 'factory2') + self.assertEqual(tweens.explicit, [('name', 'factory'), + ('name2', 'factory2')]) + + def test_add_implicit_noaliases(self): + from pyramid.tweens import INGRESS + from pyramid.tweens import MAIN + D = {MAIN:MAIN, INGRESS:INGRESS} + tweens = self._makeOne() + tweens.add_implicit('name', 'factory') + self.assertEqual(tweens.names, ['name']) + self.assertEqual(tweens.factories, + {'name':'factory'}) + self.assertEqual(tweens.alias_to_name, D) + self.assertEqual(tweens.name_to_alias, D) + self.assertEqual(tweens.order, [(INGRESS, 'name')]) + self.assertEqual(tweens.ingress_alias_names, ['name']) + tweens.add_implicit('name2', 'factory2') + self.assertEqual(tweens.names, ['name', 'name2']) + self.assertEqual(tweens.factories, + {'name':'factory', 'name2':'factory2'}) + self.assertEqual(tweens.alias_to_name, D) + self.assertEqual(tweens.name_to_alias, D) + self.assertEqual(tweens.order, + [(INGRESS, 'name'), (INGRESS, 'name2')]) + self.assertEqual(tweens.ingress_alias_names, ['name', 'name2']) + tweens.add_implicit('name3', 'factory3', over='name2') + self.assertEqual(tweens.names, + ['name', 'name2', 'name3']) + self.assertEqual(tweens.factories, + {'name':'factory', 'name2':'factory2', + 'name3':'factory3'}) + self.assertEqual(tweens.alias_to_name, D) + self.assertEqual(tweens.name_to_alias, D) + self.assertEqual(tweens.order, + [(INGRESS, 'name'), (INGRESS, 'name2'), + ('name3', 'name2')]) + self.assertEqual(tweens.ingress_alias_names, ['name', 'name2']) + + def test_add_implicit_withaliases(self): + from pyramid.tweens import INGRESS + tweens = self._makeOne() + tweens.add_implicit('name1', 'factory', alias='n1') + self.assertEqual(tweens.names, ['name1']) + self.assertEqual(tweens.factories, + {'name1':'factory'}) + self.assertEqual(tweens.alias_to_name['n1'], 'name1') + self.assertEqual(tweens.name_to_alias['name1'], 'n1') + self.assertEqual(tweens.order, [(INGRESS, 'n1')]) + self.assertEqual(tweens.ingress_alias_names, ['n1']) + tweens.add_implicit('name2', 'factory2', alias='n2') + self.assertEqual(tweens.names, ['name1', 'name2']) + self.assertEqual(tweens.factories, + {'name1':'factory', 'name2':'factory2'}) + self.assertEqual(tweens.alias_to_name['n2'], 'name2') + self.assertEqual(tweens.name_to_alias['name2'], 'n2') + self.assertEqual(tweens.order, + [(INGRESS, 'n1'), (INGRESS, 'n2')]) + self.assertEqual(tweens.ingress_alias_names, ['n1', 'n2']) + tweens.add_implicit('name3', 'factory3', alias='n3', over='name2') + self.assertEqual(tweens.names, + ['name1', 'name2', 'name3']) + self.assertEqual(tweens.factories, + {'name1':'factory', 'name2':'factory2', + 'name3':'factory3'}) + self.assertEqual(tweens.alias_to_name['n3'], 'name3') + self.assertEqual(tweens.name_to_alias['name3'], 'n3') + self.assertEqual(tweens.order, + [(INGRESS, 'n1'), (INGRESS, 'n2'), + ('n3', 'name2')]) + self.assertEqual(tweens.ingress_alias_names, ['n1', 'n2']) + + def test___call___explicit(self): + tweens = self._makeOne() + def factory1(handler, registry): + return handler + def factory2(handler, registry): + return '123' + tweens.explicit = [('name', factory1), ('name', factory2)] + self.assertEqual(tweens(None, None), '123') + + def test___call___implicit(self): + tweens = self._makeOne() + def factory1(handler, registry): + return handler + def factory2(handler, registry): + return '123' + tweens.names = ['name', 'name2'] + tweens.alias_to_name = {'name':'name', 'name2':'name2'} + tweens.name_to_alias = {'name':'name', 'name2':'name2'} + tweens.factories = {'name':factory1, 'name2':factory2} + self.assertEqual(tweens(None, None), '123') + + def test___call___implicit_with_aliasnames_different_than_names(self): + tweens = self._makeOne() + def factory1(handler, registry): + return handler + def factory2(handler, registry): + return '123' + tweens.names = ['foo1', 'foo2'] + tweens.alias_to_name = {'foo1':'name', 'foo2':'name2'} + tweens.name_to_alias = {'name':'foo1', 'name2':'foo2'} + tweens.factories = {'name':factory1, 'name2':factory2} + self.assertEqual(tweens(None, None), '123') + + def test_implicit_ordering_1(self): + tweens = self._makeOne() + tweens.add_implicit('name1', 'factory1') + tweens.add_implicit('name2', 'factory2') + self.assertEqual(tweens.implicit(), + [ + ('name2', 'factory2'), + ('name1', 'factory1'), + ]) + + def test_implicit_ordering_2(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + tweens.add_implicit('name1', 'factory1') + tweens.add_implicit('name2', 'factory2', over=MAIN) + self.assertEqual(tweens.implicit(), + [ + ('name1', 'factory1'), + ('name2', 'factory2'), + ]) + + def test_implicit_ordering_3(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('auth', 'auth_factory', under='browserid') + add('dbt', 'dbt_factory') + add('retry', 'retry_factory', over='txnmgr', under='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', under='exceptionview') + add('exceptionview', 'excview_factory', over=MAIN) + self.assertEqual(tweens.implicit(), + [ + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ]) + + def test_implicit_ordering_4(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', over=MAIN) + add('auth', 'auth_factory', under='browserid') + add('retry', 'retry_factory', over='txnmgr', under='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', under='exceptionview') + add('dbt', 'dbt_factory') + self.assertEqual(tweens.implicit(), + [ + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ]) + + def test_implicit_ordering_withaliases(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', alias='e', over=MAIN) + add('auth', 'auth_factory', under='b') + add('retry', 'retry_factory', over='t', under='exceptionview') + add('browserid', 'browserid_factory', alias='b') + add('txnmgr', 'txnmgr_factory', alias='t', under='exceptionview') + add('dbt', 'dbt_factory') + self.assertEqual(tweens.implicit(), + [ + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ]) + + def test_implicit_ordering_missing_partial(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', over=MAIN) + add('auth', 'auth_factory', under='browserid') + add('retry', 'retry_factory', over='txnmgr', under='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(tweens.implicit(), + [ + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_implicit_ordering_missing_partial2(self): + tweens = self._makeOne() + add = tweens.add_implicit + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', under='browserid') + add('retry', 'retry_factory', over='txnmgr', under='exceptionview') + add('browserid', 'browserid_factory') + self.assertEqual(tweens.implicit(), + [ + ('retry', 'retry_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_implicit_ordering_missing_partial3(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', over=MAIN) + add('retry', 'retry_factory', over='txnmgr', under='exceptionview') + add('browserid', 'browserid_factory') + self.assertEqual(tweens.implicit(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_implicit_ordering_missing_partial_with_aliases(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', alias='e', over=MAIN) + add('retry', 'retry_factory', over='txnmgr', under='e') + add('browserid', 'browserid_factory') + self.assertEqual(tweens.implicit(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_implicit_ordering_conflict_direct(self): + from pyramid.tweens import CyclicDependencyError + tweens = self._makeOne() + add = tweens.add_implicit + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', over='browserid', under='browserid') + self.assertRaises(CyclicDependencyError, tweens.implicit) + + def test_implicit_ordering_conflict_indirect(self): + from pyramid.tweens import CyclicDependencyError + tweens = self._makeOne() + add = tweens.add_implicit + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', over='browserid') + add('dbt', 'dbt_factory', under='browserid', over='auth') + self.assertRaises(CyclicDependencyError, tweens.implicit) + +class TestCyclicDependencyError(unittest.TestCase): + def _makeOne(self, cycles): + from pyramid.tweens import CyclicDependencyError + return CyclicDependencyError(cycles) + + def test___str__(self): + exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) + result = str(exc) + self.assertEqual(result, + "'a' sorts over ['c', 'd']; 'c' sorts over ['a']") + diff --git a/pyramid/tweens.py b/pyramid/tweens.py index f7673a738..9a7d432dc 100644 --- a/pyramid/tweens.py +++ b/pyramid/tweens.py @@ -1,18 +1,18 @@ import sys +from pyramid.exceptions import ConfigurationError from pyramid.interfaces import IExceptionViewClassifier from pyramid.interfaces import IView +from pyramid.interfaces import ITweens -from pyramid.events import NewResponse from zope.interface import providedBy +from zope.interface import implements def excview_tween_factory(handler, registry): """ A :term:`tween` factory which produces a tween that catches an exception raised by downstream tweens (or the main Pyramid request handler) and, if possible, converts it into a Response using an :term:`exception view`.""" - has_listeners = registry.has_listeners adapters = registry.adapters - notify = registry.notify def excview_tween(request): attrs = request.__dict__ @@ -35,11 +35,178 @@ def excview_tween_factory(handler, registry): if view_callable is None: raise response = view_callable(exc, request) - has_listeners and notify(NewResponse(request, response)) finally: + # prevent leakage attrs['exc_info'] = None return response return excview_tween +class CyclicDependencyError(Exception): + def __init__(self, cycles): + self.cycles = cycles + + def __str__(self): + L = [] + cycles = self.cycles + for cycle in cycles: + dependent = cycle + dependees = cycles[cycle] + L.append('%r sorts over %r' % (dependent, dependees)) + msg = '; '.join(L) + return msg + +class Tweens(object): + implements(ITweens) + def __init__(self): + self.explicit = [] + self.names = [] + self.factories = {} + self.order = [] + self.ingress_alias_names = [] + self.alias_to_name = {INGRESS:INGRESS, MAIN:MAIN} + self.name_to_alias = {INGRESS:INGRESS, MAIN:MAIN} + + def add_explicit(self, name, factory): + self.explicit.append((name, factory)) + + def add_implicit(self, name, factory, alias=None, under=None, over=None): + if alias is not None: + self.alias_to_name[alias] = name + self.name_to_alias[name] = alias + else: + alias = name + self.names.append(name) + self.factories[name] = factory + if under is None and over is None: + under = INGRESS + self.ingress_alias_names.append(alias) + if under is not None: + self.order.append((under, alias)) + if over is not None: + self.order.append((alias, over)) + + def implicit(self): + order = [] + roots = [] + graph = {} + has_order = {} + aliases = [INGRESS, MAIN] + ingress_alias_names = self.ingress_alias_names[:] + + for name in self.names: + aliases.append(self.name_to_alias.get(name, name)) + + for a, b in self.order: + # try to convert both a and b to an alias + a = self.name_to_alias.get(a, a) + b = self.name_to_alias.get(b, b) + order.append((a, b)) + + def add_node(graph, node): + if not graph.has_key(node): + roots.append(node) + graph[node] = [0] # 0 = number of arcs coming into this node + + def add_arc(graph, fromnode, tonode): + graph[fromnode].append(tonode) + graph[tonode][0] += 1 + if tonode in roots: + roots.remove(tonode) + + # remove ordering information that mentions unknown names/aliases + for pos, (first, second) in enumerate(order): + has_first = first in aliases + has_second = second in aliases + if (not has_first) or (not has_second): + order[pos] = None, None + else: + has_order[first] = has_order[second] = True + + for v in aliases: + # any alias that doesn't have an ordering after we detect all + # nodes with orders should get an ordering relative to INGRESS, + # as if it were added with no under or over in add_implicit + if (not v in has_order) and (v not in (INGRESS, MAIN)): + order.append((INGRESS, v)) + ingress_alias_names.append(v) + add_node(graph, v) + + for a, b in order: + if a is not None and b is not None: # deal with removed orders + add_arc(graph, a, b) + + def sortroots(alias): + # sort roots so that roots (and their children) that depend only + # on the ingress sort nearer the (nearer the ingress) + if alias in ingress_alias_names: + return -1 + children = graph[alias][1:] + for child in children: + if sortroots(child) == -1: + return -1 + return 1 + + roots.sort(key=sortroots) + + sorted_aliases = [] + + while roots: + root = roots.pop(0) + sorted_aliases.append(root) + children = graph[root][1:] + for child in children: + arcs = graph[child][0] + arcs -= 1 + graph[child][0] = arcs + if arcs == 0: + roots.insert(0, child) + del graph[root] + + if graph: + # loop in input + cycledeps = {} + for k, v in graph.items(): + cycledeps[k] = v[1:] + raise CyclicDependencyError(cycledeps) + + result = [] + + for alias in sorted_aliases: + if alias not in (MAIN, INGRESS): + name = self.alias_to_name.get(alias, alias) + result.append((name, self.factories[name])) + + return result + + def __call__(self, handler, registry): + if self.explicit: + use = self.explicit + else: + use = self.implicit() + for name, factory in use[::-1]: + handler = factory(handler, registry) + return handler + +def tween_factory_name(factory): + if (hasattr(factory, '__name__') and + hasattr(factory, '__module__')): + # function or class + name = '.'.join([factory.__module__, + factory.__name__]) + elif hasattr(factory, '__module__'): + # instance + name = '.'.join([factory.__module__, + factory.__class__.__name__, + str(id(factory))]) + else: + raise ConfigurationError( + 'A tween factory must be a class, an instance, or a function; ' + '%s is not a suitable tween factory' % factory) + return name + +MAIN = 'MAIN' +INGRESS = 'INGRESS' +EXCVIEW = 'excview' + |
