diff options
| author | Chris McDonough <chrism@plope.com> | 2011-08-06 20:35:44 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-08-06 20:35:44 -0400 |
| commit | 6c3f78b0a5563a642b98f7a0eb01d1854c45e6b0 (patch) | |
| tree | 57c0fb799ed585a8c2c5af609a297fdeccc62d8f | |
| parent | 37699448a389c2712f262c574e5115aaab27adda (diff) | |
| parent | 18a99ae15e7f36cd21da6e2d2e70d61d1733bf30 (diff) | |
| download | pyramid-6c3f78b0a5563a642b98f7a0eb01d1854c45e6b0.tar.gz pyramid-6c3f78b0a5563a642b98f7a0eb01d1854c45e6b0.tar.bz2 pyramid-6c3f78b0a5563a642b98f7a0eb01d1854c45e6b0.zip | |
Merge branch 'handlerchanges'
| -rw-r--r-- | CHANGES.txt | 86 | ||||
| -rw-r--r-- | docs/api.rst | 1 | ||||
| -rw-r--r-- | docs/api/config.rst | 2 | ||||
| -rw-r--r-- | docs/api/tweens.rst | 8 | ||||
| -rw-r--r-- | docs/glossary.rst | 11 | ||||
| -rw-r--r-- | docs/narr/commandline.rst | 75 | ||||
| -rw-r--r-- | docs/narr/hooks.rst | 127 | ||||
| -rw-r--r-- | pyramid/config.py | 155 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 17 | ||||
| -rw-r--r-- | pyramid/paster.py | 66 | ||||
| -rw-r--r-- | pyramid/router.py | 111 | ||||
| -rw-r--r-- | pyramid/tests/test_config.py | 146 | ||||
| -rw-r--r-- | pyramid/tests/test_paster.py | 73 | ||||
| -rw-r--r-- | pyramid/tests/test_router.py | 25 | ||||
| -rw-r--r-- | pyramid/tweens.py | 45 | ||||
| -rw-r--r-- | pyramid/util.py | 1 | ||||
| -rw-r--r-- | setup.py | 1 |
17 files changed, 680 insertions, 270 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index c94ecb800..b6f33715e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,63 +12,19 @@ Features ``rendering_val``. This can be used to introspect the value returned by a view in a BeforeRender subscriber. -- New configurator directive: - ``pyramid.config.Configurator.add_request_handler``. This directive adds - a request handler factory. - - A request handler factory is used to wrap the Pyramid router's primary - request handling function. This is a feature may be used by framework - extensions, to provide, for example, view timing support and as a - convenient place to hang bookkeeping code that examines exceptions before - they are returned to the server. - - A request handler factory (passed as ``handler_factory``) must be a - callable which accepts two arguments: ``handler`` and ``registry``. - ``handler`` will be the request handler being wrapped. ``registry`` will - be the Pyramid application registry represented by this Configurator. A - request handler factory must return a request handler when it is called. - - A request handler accepts a request object and returns a response object. - - Here's an example of creating both a handler factory and a handler, and - registering the handler factory: - - .. code-block:: python - - import time - - def timing_handler_factory(handler, registry): - if registry.settings['do_timing']: - # if timing support is enabled, return a wrapper - def timing_handler(request): - start = time.time() - try: - response = handler(request) - finally: - end = time.time() - print: 'The request took %s seconds' % (end - start) - return response - return timing_handler - # if timing support is not enabled, return the original handler - return handler - - config.add_request_handler(timing_handler_factory, 'timing') - - The ``request`` argument to the handler will be the request created by - Pyramid's router when it receives a WSGI request. - - If more than one request handler factory is registered into a single - configuration, the request handlers will be chained together. The first - request handler factory added (in code execution order) will be called with - the default Pyramid request handler, the second handler factory added will - be called with the result of the first handler factory, ad infinitum. The - Pyramid router will use the outermost wrapper in this chain (which is a bit - like a WSGI middleware "pipeline") as its handler function. - - The ``name`` argument to this function is required. The name is used as a - key for conflict detection. No two request handler factories may share the - same name in the same configuration (unless automatic_conflict_resolution - is able to resolve the conflict or this is an autocommitting configurator). +- New configurator directive: ``pyramid.config.Configurator.add_tween``. + This directive adds a "tween". A "tween" is used to wrap the Pyramid + router's primary request handling function. This is a feature may be used + by Pyramid framework extensions, to provide, for example, view timing + support and as a convenient place to hang bookkeeping code. + + Tweens are further described in the narrative docs section in the Hooks + chapter, named "Registering Tweens". + +- New paster command ``paster ptweens``, which prints the current "tween" + configuration for an application. See the section entitled "Displaying + Tweens" in the Command-Line Pyramid chapter of the narrative documentation + for more info. - The Pyramid debug logger now uses the standard logging configuration (usually set up by Paste as part of startup). This means that output from @@ -80,6 +36,12 @@ Features will be ``None`` until an exception is caught by the Pyramid router, after which it will be the result of ``sys.exc_info()``. +Internal +-------- + +- The Pyramid "exception view" machinery is now implemented as a "tween" + (``pyramid.tweens.excview_tween_factory``). + Deprecations ------------ @@ -97,6 +59,16 @@ Backwards Incompatibilities that string is considered to be the name of a global Python logger rather than a dotted name to an instance of a logger. +Documentation +------------- + +- Added a new module to the API docs: ``pyramid.tweens``. + +- Added a "Registering Tweens" section to the "Hooks" narrative chapter. + +- Added a "Displaying Tweens" section to the "Command-Line Pyramid" narrative + chapter. + 1.1 (2011-07-22) ================ diff --git a/docs/api.rst b/docs/api.rst index a7e1566d3..6ff6e9fb1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -32,6 +32,7 @@ documentation is organized alphabetically by module name. api/testing api/threadlocal api/traversal + api/tweens api/url api/view api/wsgi diff --git a/docs/api/config.rst b/docs/api/config.rst index 21e2b828d..1a9bb6ba4 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -78,7 +78,7 @@ .. automethod:: set_view_mapper - .. automethod:: add_request_handler + .. automethod:: add_tween .. automethod:: testing_securitypolicy diff --git a/docs/api/tweens.rst b/docs/api/tweens.rst new file mode 100644 index 000000000..5fc15cb00 --- /dev/null +++ b/docs/api/tweens.rst @@ -0,0 +1,8 @@ +.. _tweens_module: + +:mod:`pyramid.tweens` +--------------------- + +.. automodule:: pyramid.tweens + + .. autofunction:: excview_tween_factory diff --git a/docs/glossary.rst b/docs/glossary.rst index c6705fdc5..ccb62bbc8 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -917,3 +917,14 @@ Glossary PyPy is an "alternative implementation of the Python language":http://pypy.org/ + 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 + context in which they have access to the Pyramid :term:`application + registry` as well as the Pyramid rendering machinery. + diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 509af7dd3..6f969196f 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -297,8 +297,79 @@ application, nothing will be printed to the console when ``paster proutes`` is executed. .. index:: - single: scripting - single: bootstrap + pair: tweens; printing + single: paster ptweens + single: ptweens + +.. _displaying_tweens: + +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. + +For example, here's the ``paster pwteens`` command run against a system +configured without any explicit tweens: + +.. code-block:: text + :linenos: + + [chrism@thinko starter]$ ../bin/paster ptweens development.ini + "pyramid.tweens" config value NOT set (implicitly ordered tweens used) + + Position Name + -------- ---- + 0 pyramid.router.excview_tween_factory + +Here's the ``paster pwteens`` command run against a system configured *with* +explicit tweens defined in its ``development.ini`` file: + +.. code-block:: text + :linenos: + + [chrism@thinko starter]$ ../bin/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 + + Implicit Tween Chain (not used) + + Position Name + -------- ---- + 0 pyramid.tweens.excview_tween_factory + +Here's the application configuration section of the ``development.ini`` used +by the above ``paster ptweens`` command which reprorts that the explicit +tween chain is used: + +.. code-block:: text + :linenos: + + [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 + starter.tween_factory1 + starter.tween_factory2 .. _writing_a_script: diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 4f493c854..889e8d6d8 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -828,3 +828,130 @@ performed, enabling you to set up the utility in advance: For full details, please read the `Venusian documentation <http://docs.repoze.org/venusian>`_. +.. _registering_tweens: + +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 +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 +:term:`application registry` as well as the Pyramid rendering machinery. + +To make use of tweens, you must construct a "tween factory". A tween factory +must be a callable (or a :term:`dotted Python name` to such a callable) which +accepts two arguments: ``handler`` and ``registry``. ``handler`` will be the +either the main Pyramid request handling function or another tween (if more +than one tween is configured into the request handling chain). ``registry`` +will be the Pyramid :term:`application registry` represented by this +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. + +Here's an example creating a tween factory and registering it: + +.. code-block:: python + :linenos: + + import time + from pyramid.settings import asbool + import logging + + log = logging.getLogger(__name__) + + def timing_tween_factory(handler, registry): + if asbool(registry.settings.get('do_timing')): + # if timing support is enabled, return a wrapper + def timing_tween(request): + start = time.time() + try: + response = handler(request) + finally: + end = time.time() + log.debug('The request took %s seconds' % + (end - start)) + return response + return timing_tween + # if timing support is not enabled, return the original + # handler + return handler + + from pyramid.config import Configurator + + config = Configurator() + config.add_tween(timing_tween_factory) + +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: + +.. code-block:: ini + :linenos: + + [app:main] + use = egg:MyApp + pyramid.reload_templates = true + pyramid.debug_authorization = false + pyramid.debug_notfound = false + pyramid.debug_routematch = false + pyramid.debug_templates = true + pyramid.tweens = pyramid.tweens.excview_tween_factory + myapp.my_cool_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 +:app:`Pyramid` request handling function; it will wrap the tween factory +declared directly "above" it, ad infinitum. + +.. note:: Pyramid's own :term:`exception view` handling logic is implemented + as a tween factory function: :func:`pyramid.tweens.excview_tween_factory`. + If Pyramid exception view handling is desired, and tween factories are + specified via the ``pyramid.tweens`` configuration setting, the + :func:`pyramid.tweens.excview_tween_factory` function must be added to the + ``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`. + diff --git a/pyramid/config.py b/pyramid/config.py index 159422c22..a12df8ef7 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -38,8 +38,7 @@ from pyramid.interfaces import IRendererFactory from pyramid.interfaces import IRendererGlobalsFactory from pyramid.interfaces import IRequest from pyramid.interfaces import IRequestFactory -from pyramid.interfaces import IRequestHandlerFactory -from pyramid.interfaces import IRequestHandlerFactories +from pyramid.interfaces import ITweens from pyramid.interfaces import IResponse from pyramid.interfaces import IRootFactory from pyramid.interfaces import IRouteRequest @@ -80,6 +79,7 @@ from pyramid.threadlocal import manager 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.urldispatch import RoutesMapper from pyramid.util import DottedNameResolver from pyramid.util import WeakOrderedSet @@ -717,11 +717,13 @@ class Configurator(object): policies, renderers, a debug logger, a locale negotiator, and various other settings using the configurator's current registry, as per the descriptions in the Configurator constructor.""" + tweens = [] + includes = [] if settings: - includes = settings.pop('pyramid.include', '') - includes = [x.strip() for x in includes.splitlines()] - else: - includes = [] + includes = [x.strip() for x in + settings.get('pyramid.include', '').splitlines()] + tweens = [x.strip() for x in + settings.get('pyramid.tweens','').splitlines()] registry = self.registry self._fix_registry() self._set_settings(settings) @@ -731,6 +733,9 @@ 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) @@ -898,80 +903,45 @@ class Configurator(object): return self._derive_view(view, attr=attr, renderer=renderer) @action_method - def add_request_handler(self, handler_factory, name): + def add_tween(self, tween_factory): """ - Add a request handler factory. A request handler factory is used to - wrap the Pyramid router's primary request handling function. This is - a feature that may be used by framework extensions, to provide, for - example, view timing support and as a convenient place to hang - bookkeeping code that examines exceptions before they are returned to - the server. - - A request handler factory (passed as ``handler_factory``) must be a - callable (or a :term:`dotted Python name` to a callable) which - accepts two arguments: ``handler`` and ``registry``. ``handler`` - will be the request handler being wrapped. ``registry`` will be the - Pyramid :term:`application registry` represented by this - Configurator. A request handler factory must return a request - handler when it is called. - - A request handler accepts a :term:`request` object and returns a - :term:`response` object. - - Here's an example of creating both a handler factory and a handler, - and registering the handler factory: - - .. code-block:: python - - import time - - def timing_handler_factory(handler, registry): - if registry.settings['do_timing']: - # if timing support is enabled, return a wrapper - def timing_handler(request): - start = time.time() - try: - response = handler(request) - finally: - end = time.time() - print: 'The request took %s seconds' % (end - start) - return response - return timing_handler - # if timing support is not enabled, return the original handler - return handler - - config.add_request_handler(timing_handler_factory, 'timing') - - The ``request`` argument to the handler will be the request created - by Pyramid's router when it receives a WSGI request. - - If more than one request handler factory is registered into a single - configuration, the request handlers will be chained together. The - first request handler factory added (in code execution order) will be - called with the default Pyramid request handler, the second handler - factory added will be called with the result of the first handler - factory, ad infinitum. The Pyramid router will use the outermost - wrapper in this chain (which is a bit like a WSGI middleware - "pipeline") as its handler function. - - The ``name`` argument to this function is required. The name is used - as a key for conflict detection. No two request handler factories - may share the same name in the same configuration (unless - :ref:`automatic_conflict_resolution` is able to resolve the conflict - or this is an autocommitting configurator). + 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 + :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. + + For more information, see :ref:`registering_tweens`. .. note:: This feature is new as of Pyramid 1.1.1. """ - handler_factory = self.maybe_dotted(handler_factory) + return self._add_tween(tween_factory, explicit=False) + + def _add_tween(self, tween_factory, explicit): + tween_factory = self.maybe_dotted(tween_factory) + name = tween_factory_name(tween_factory) def register(): registry = self.registry - registry.registerUtility(handler_factory, IRequestHandlerFactory, - name=name) - existing_names = registry.queryUtility(IRequestHandlerFactories, - default=[]) - existing_names.append(name) - registry.registerUtility(existing_names, IRequestHandlerFactories) - self.action(('requesthandler', name), register) + 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) + + @action_method + def add_request_handler(self, factory, name): # pragma: no cover + # XXX bw compat for debugtoolbar + return self._add_tween(factory, explicit=False) @action_method def add_subscriber(self, subscriber, iface=None): @@ -3407,3 +3377,40 @@ 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/interfaces.py b/pyramid/interfaces.py index 41e896adf..d97632018 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -446,23 +446,16 @@ class IMultiDict(Interface): # docs-only interface class IRequest(Interface): """ Request type interface attached to all request objects """ -class IRequestHandlerFactories(Interface): +class ITweens(Interface): """ Marker interface for utility registration representing the ordered - set of a configuration's request handler factories""" - -class IRequestHandlerFactory(Interface): - """ A request handler factory can be used to augment Pyramid's default - mainloop request handling.""" - def __call__(self, handler, registry): - """ Return an IRequestHandler; the ``handler`` argument passed will - be the previous request handler added, or the default request handler - if no request handlers have yet been added .""" + set of a configuration's tween factories""" class IRequestHandler(Interface): """ """ def __call__(self, request): - """ Must return an IResponse or raise an exception. The ``request`` - argument will be an instance of an object that provides IRequest.""" + """ Must return a tuple of IReqest, IResponse or raise an exception. + The ``request`` argument will be an instance of an object that + provides IRequest.""" IRequest.combined = IRequest # for exception view lookups diff --git a/pyramid/paster.py b/pyramid/paster.py index 3143fa91e..54f5a51a6 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -9,8 +9,8 @@ from paste.deploy import loadapp from paste.script.command import Command from pyramid.interfaces import IMultiView +from pyramid.interfaces import ITweens -from pyramid.scripting import get_root from pyramid.scripting import prepare from pyramid.util import DottedNameResolver @@ -534,3 +534,67 @@ class PViewsCommand(PCommand): self.out(" Not found.") self.out('') + +class PTweensCommand(PCommand): + """Print all implicit and explicit :term:`tween` objects used by a + Pyramid application. The handler output includes whether the system is + using an explicit tweens ordering (will be true when the + ``pyramid.tweens`` setting is used) or an implicit tweens ordering (will + be true when the ``pyramid.tweens`` setting is *not* used). + + This command accepts one positional argument: + + ``config_uri`` -- specifies the PasteDeploy config file to use for the + interactive shell. The format is ``inifile#name``. If the name is left + off, ``main`` will be assumed. + + Example:: + + $ paster ptweens myapp.ini#main + + """ + summary = "Print all tweens related to a Pyramid application" + min_args = 1 + max_args = 1 + stdout = sys.stdout + + parser = Command.standard_parser(simulate=True) + + def _get_tweens(self, registry): + from pyramid.config import Configurator + config = Configurator(registry = registry) + return config.registry.queryUtility(ITweens) + + def out(self, msg): # pragma: no cover + print msg + + def command(self): + config_uri = self.args[0] + env = self.bootstrap[0](config_uri) + registry = env['registry'] + tweens = self._get_tweens(registry) + if tweens is not None: + ordering = [] + if tweens.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)')) + 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('') diff --git a/pyramid/router.py b/pyramid/router.py index ddec23cdb..79fd7992b 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -1,10 +1,7 @@ -import sys - from zope.interface import implements from zope.interface import providedBy from pyramid.interfaces import IDebugLogger -from pyramid.interfaces import IExceptionViewClassifier from pyramid.interfaces import IRequest from pyramid.interfaces import IRootFactory from pyramid.interfaces import IRouteRequest @@ -14,8 +11,7 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import ITraverser from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier -from pyramid.interfaces import IRequestHandlerFactory -from pyramid.interfaces import IRequestHandlerFactories +from pyramid.interfaces import ITweens from pyramid.events import ContextFound from pyramid.events import NewRequest @@ -25,6 +21,7 @@ from pyramid.request import Request from pyramid.threadlocal import manager from pyramid.traversal import DefaultRootFactory from pyramid.traversal import ResourceTreeTraverser +from pyramid.tweens import excview_tween_factory class Router(object): implements(IRouter) @@ -40,14 +37,13 @@ class Router(object): self.root_factory = q(IRootFactory, default=DefaultRootFactory) self.routes_mapper = q(IRoutesMapper) self.request_factory = q(IRequestFactory, default=Request) - handler_factory_names = q(IRequestHandlerFactories) - handler = self.handle_request - if handler_factory_names: - for name in handler_factory_names: - handler_factory = registry.getUtility(IRequestHandlerFactory, - name=name) - handler = handler_factory(handler, registry) - self.handle_request = handler + 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) + self.root_policy = self.root_factory # b/w compat self.registry = registry settings = registry.settings @@ -59,16 +55,17 @@ class Router(object): def handle_request(self, request): attrs = request.__dict__ registry = attrs['registry'] - 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 - - try: # matches except Exception (exception view execution) + + 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 @@ -107,13 +104,12 @@ class Router(object): ) logger and logger.debug(msg) - request_iface = registry.queryUtility( + request.request_iface = registry.queryUtility( IRouteRequest, name=route.name, default=IRequest) - - root_factory = route.factory or \ - self.root_factory + + root_factory = route.factory or self.root_factory root = root_factory(request) attrs['root'] = root @@ -124,8 +120,7 @@ class Router(object): traverser = ResourceTreeTraverser(root) tdict = traverser(request) - context, view_name, subpath, traversed, vroot, \ - vroot_path = ( + context, view_name, subpath, traversed, vroot, vroot_path = ( tdict['context'], tdict['view_name'], tdict['subpath'], @@ -140,7 +135,7 @@ class Router(object): # find a view callable context_iface = providedBy(context) view_callable = adapters.lookup( - (IViewClassifier, request_iface, context_iface), + (IViewClassifier, request.request_iface, context_iface), IView, name=view_name, default=None) # invoke the view callable @@ -152,8 +147,7 @@ class Router(object): 'traversed: %r, root: %r, vroot: %r, ' 'vroot_path: %r' % ( request.url, request.path_info, context, - view_name, - subpath, traversed, root, vroot, + view_name, subpath, traversed, root, vroot, vroot_path) ) logger and logger.debug(msg) @@ -161,39 +155,18 @@ class Router(object): msg = request.path_info raise HTTPNotFound(msg) else: - # if there were any view wrappers for the current - # request, use them to wrap the view - response = view_callable(context, request) - # handle exceptions raised during root finding and view-exec - except Exception, why: - # clear old generated request.response, if any; it may - # have been mutated by the view, and its state is not - # sane (e.g. caching headers) - if 'response' in attrs: - del attrs['response'] - - attrs['exception'] = why - attrs['exc_info'] = sys.exc_info() + has_listeners and notify(NewResponse(request, response)) - for_ = (IExceptionViewClassifier, - request_iface.combined, - providedBy(why)) - view_callable = adapters.lookup(for_, IView, - default=None) - - if view_callable is None: - raise + if request.response_callbacks: + request._process_response_callbacks(response) - response = view_callable(why, request) + return response - has_listeners and notify(NewResponse(request, response)) - - if request.response_callbacks: - request._process_response_callbacks(response) - - return response + finally: + if request is not None and request.finished_callbacks: + request._process_finished_callbacks() def __call__(self, environ, start_response): """ @@ -204,23 +177,13 @@ class Router(object): return an iterable. """ registry = self.registry - manager = self.threadlocal_manager - request = None + request = self.request_factory(environ) threadlocals = {'registry':registry, 'request':request} + manager = self.threadlocal_manager manager.push(threadlocals) - try: - try: - request = self.request_factory(environ) - threadlocals['request'] = request - request.registry = registry - response = self.handle_request(request) - finally: - if request is not None: - if request.finished_callbacks: - request._process_finished_callbacks() - request.exc_info = None # avoid leak - + request.registry = registry + response = self.handle_request(request) return response(request.environ, start_response) finally: manager.pop() diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 250c53b9a..d73fd7f7d 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -336,6 +336,7 @@ class ConfiguratorTests(unittest.TestCase): reg = DummyRegistry() config = self._makeOne(reg) config.add_view = lambda *arg, **kw: False + config._add_tween = lambda *arg, **kw: False config.setup_registry() self.assertEqual(reg.has_listeners, True) @@ -347,6 +348,7 @@ class ConfiguratorTests(unittest.TestCase): config = self._makeOne(reg) views = [] config.add_view = lambda *arg, **kw: views.append((arg, kw)) + config._add_tween = lambda *arg, **kw: False config.setup_registry() self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) @@ -581,6 +583,20 @@ pyramid.tests.test_config.dummy_include2""", self.assert_(reg.included) self.assert_(reg.also_included) + def test_setup_registry_tweens(self): + from pyramid.interfaces import ITweens + from pyramid.registry import Registry + reg = Registry() + config = self._makeOne(reg) + settings = { + 'pyramid.tweens': 'pyramid.tests.test_config.dummy_tween_factory' + } + config.setup_registry(settings=settings) + tweens = config.registry.getUtility(ITweens) + self.assertEqual(tweens.explicit, + [('pyramid.tests.test_config.dummy_tween_factory', + dummy_tween_factory)]) + def test_get_settings_nosettings(self): from pyramid.registry import Registry reg = Registry() @@ -612,48 +628,66 @@ pyramid.tests.test_config.dummy_include2""", settings = reg.getUtility(ISettings) self.assertEqual(settings['a'], 1) - def test_add_request_handlers_names_distinct(self): - from pyramid.interfaces import IRequestHandlerFactories - from pyramid.interfaces import IRequestHandlerFactory + def test_add_tweens_names_distinct(self): + from pyramid.interfaces import ITweens + from pyramid.tweens import excview_tween_factory def factory1(handler, registry): return handler def factory2(handler, registry): return handler config = self._makeOne() - config.add_request_handler(factory1, 'name1') - config.add_request_handler(factory2, 'name2') + config.add_tween(factory1) + config.add_tween(factory2) config.commit() - names = config.registry.queryUtility(IRequestHandlerFactories) - self.assertEqual(names, ['name1', 'name2']) - f1 = config.registry.getUtility(IRequestHandlerFactory, name='name1') - f2 = config.registry.getUtility(IRequestHandlerFactory, name='name2') - self.assertEqual(f1, factory1) - self.assertEqual(f2, factory2) - - def test_add_request_handlers_dottednames(self): - import pyramid.tests - from pyramid.interfaces import IRequestHandlerFactories - from pyramid.interfaces import IRequestHandlerFactory + tweens = config.registry.queryUtility(ITweens) + self.assertEqual( + tweens.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 + from pyramid.tweens import excview_tween_factory config = self._makeOne() - config.add_request_handler('pyramid.tests', 'name1') + config.add_tween('pyramid.tests.test_config.dummy_tween_factory') config.commit() - names = config.registry.queryUtility(IRequestHandlerFactories) - self.assertEqual(names, ['name1']) - f1 = config.registry.getUtility(IRequestHandlerFactory, name='name1') - self.assertEqual(f1, pyramid.tests) - - def test_add_request_handlers_names_overlap(self): - from pyramid.interfaces import IRequestHandlerFactories - from pyramid.interfaces import IRequestHandlerFactory - def factory1(handler, registry): return handler - def factory2(handler, registry): return handler - def factory3(handler, registry): return handler - config = self._makeOne(autocommit=True) - config.add_request_handler(factory1, 'name1') - config.add_request_handler(factory2, 'name2') - config.add_request_handler(factory3, 'name1') - names = config.registry.queryUtility(IRequestHandlerFactories) - self.assertEqual(names, ['name1', 'name2', 'name1']) - f3 = config.registry.getUtility(IRequestHandlerFactory, name='name1') - self.assertEqual(f3, factory3) + tweens = config.registry.queryUtility(ITweens) + self.assertEqual( + tweens.implicit, + [ + ('pyramid.tweens.excview_tween_factory', excview_tween_factory), + ('pyramid.tests.test_config.dummy_tween_factory', + dummy_tween_factory) + ]) + + def test_add_tween_instance(self): + from pyramid.interfaces import ITweens + from pyramid.tweens import excview_tween_factory + class ATween(object): pass + atween = ATween() + config = self._makeOne() + config.add_tween(atween) + config.commit() + tweens = config.registry.queryUtility(ITweens) + self.assertEqual(len(tweens.implicit), 2) + self.assertEqual( + tweens.implicit[0], + ('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 + import pyramid.tests + config = self._makeOne() + self.assertRaises(ConfigurationError, config.add_tween, pyramid.tests) + + def test_add_tweens_conflict(self): + from zope.configuration.config import ConfigurationConflictError + config = self._makeOne() + config.add_tween('pyramid.tests.test_config.dummy_tween_factory') + config.add_tween('pyramid.tests.test_config.dummy_tween_factory') + self.assertRaises(ConfigurationConflictError, config.commit) def test_add_subscriber_defaults(self): from zope.interface import implements @@ -5477,6 +5511,45 @@ 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 @@ -5663,3 +5736,4 @@ from pyramid.interfaces import IResponse class DummyResponse(object): implements(IResponse) +def dummy_tween_factory(handler, registry): pass diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index 3cf249c5c..58ed73d2c 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -824,6 +824,79 @@ class TestBootstrap(unittest.TestCase): self.assertEqual(result['root'], self.root) self.assert_('closer' in result) +class TestPTweensCommand(unittest.TestCase): + def _getTargetClass(self): + from pyramid.paster import PTweensCommand + return PTweensCommand + + def _makeOne(self): + cmd = self._getTargetClass()('ptweens') + cmd.bootstrap = (DummyBootstrap(),) + cmd.args = ('/foo/bar/myapp.ini#myapp',) + return cmd + + def test_command_no_tweens(self): + command = self._makeOne() + command._get_tweens = lambda *arg: None + L = [] + command.out = L.append + result = command.command() + self.assertEqual(result, None) + self.assertEqual(L, []) + + def test_command_implicit_tweens_only(self): + command = self._makeOne() + tweens = DummyTweens([('name', 'item')], None) + command._get_tweens = lambda *arg: tweens + L = [] + command.out = L.append + result = command.command() + self.assertEqual(result, None) + self.assertEqual( + L, + ['"pyramid.tweens" config value NOT set (implicitly ordered tweens used)', + '', + 'Position Name ', + '-------- ---- ', + '0 name ', + '']) + + def test_command_implicit_and_explicit_tweens(self): + command = self._makeOne() + tweens = DummyTweens([('name', 'item')], [('name2', 'item2')]) + command._get_tweens = lambda *arg: tweens + L = [] + command.out = L.append + 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 ', + '' + ]) + + def test__get_tweens(self): + command = self._makeOne() + registry = DummyRegistry() + self.assertEqual(command._get_tweens(registry), None) + +class DummyTweens(object): + def __init__(self, implicit, explicit): + self.implicit = implicit + self.explicit = explicit + class Dummy: pass diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index b943f1ee6..6b0354468 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -134,11 +134,16 @@ class TestRouter(unittest.TestCase): router = self._makeOne() self.assertEqual(router.request_factory, DummyRequestFactory) - def test_request_handler_factories(self): - from pyramid.interfaces import IRequestHandlerFactory - from pyramid.interfaces import IRequestHandlerFactories + def test_tween_factories(self): + from pyramid.interfaces import ITweens + from pyramid.config import Tweens + from pyramid.response import Response + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IResponse + tweens = Tweens() + self.registry.registerUtility(tweens, ITweens) L = [] - def handler_factory1(handler, registry): + def tween_factory1(handler, registry): L.append((handler, registry)) def wrapper(request): request.environ['handled'].append('one') @@ -146,7 +151,7 @@ class TestRouter(unittest.TestCase): wrapper.name = 'one' wrapper.child = handler return wrapper - def handler_factory2(handler, registry): + def tween_factory2(handler, registry): L.append((handler, registry)) def wrapper(request): request.environ['handled'] = ['two'] @@ -154,19 +159,13 @@ class TestRouter(unittest.TestCase): wrapper.name = 'two' wrapper.child = handler return wrapper - self.registry.registerUtility(['one', 'two'], IRequestHandlerFactories) - self.registry.registerUtility(handler_factory1, - IRequestHandlerFactory, name='one') - self.registry.registerUtility(handler_factory2, - IRequestHandlerFactory, name='two') + tweens.add('one', tween_factory1) + tweens.add('two', tween_factory2) router = self._makeOne() self.assertEqual(router.handle_request.name, 'two') self.assertEqual(router.handle_request.child.name, 'one') self.assertEqual(router.handle_request.child.child.__name__, 'handle_request') - from pyramid.response import Response - from pyramid.interfaces import IViewClassifier - from pyramid.interfaces import IResponse context = DummyContext() self._registerTraverserFactory(context) environ = self._makeEnviron() diff --git a/pyramid/tweens.py b/pyramid/tweens.py new file mode 100644 index 000000000..f7673a738 --- /dev/null +++ b/pyramid/tweens.py @@ -0,0 +1,45 @@ +import sys +from pyramid.interfaces import IExceptionViewClassifier +from pyramid.interfaces import IView + +from pyramid.events import NewResponse +from zope.interface import providedBy + +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__ + try: + response = handler(request) + except Exception, exc: + # WARNING: do not assign the result of sys.exc_info() to a + # local var here, doing so will cause a leak + attrs['exc_info'] = sys.exc_info() + attrs['exception'] = exc + # clear old generated request.response, if any; it may + # have been mutated by the view, and its state is not + # sane (e.g. caching headers) + if 'response' in attrs: + del attrs['response'] + request_iface = attrs['request_iface'] + provides = providedBy(exc) + for_ = (IExceptionViewClassifier, request_iface.combined, provides) + view_callable = adapters.lookup(for_, IView, default=None) + if view_callable is None: + raise + response = view_callable(exc, request) + has_listeners and notify(NewResponse(request, response)) + finally: + attrs['exc_info'] = None + + return response + + return excview_tween + diff --git a/pyramid/util.py b/pyramid/util.py index 7fd1b0dc6..c0e7640c4 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -206,3 +206,4 @@ class WeakOrderedSet(object): if self._order: oid = self._order[-1] return self._items[oid]() + @@ -86,6 +86,7 @@ setup(name='pyramid', pshell=pyramid.paster:PShellCommand proutes=pyramid.paster:PRoutesCommand pviews=pyramid.paster:PViewsCommand + ptweens=pyramid.paster:PTweensCommand [console_scripts] bfg2pyramid = pyramid.fixers.fix_bfg_imports:main """ |
