From 1311321d454ded6226b09f929ebc9e4aa2c12771 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 18:58:26 -0400 Subject: add tweens module, add docs for ptweens and tweens to hooks --- docs/api.rst | 1 + docs/api/config.rst | 2 +- docs/api/tweens.rst | 8 +++ docs/narr/commandline.rst | 75 ++++++++++++++++++++++++- docs/narr/hooks.rst | 128 +++++++++++++++++++++++++++++++++++++++++++ pyramid/config.py | 90 +----------------------------- pyramid/router.py | 38 +------------ pyramid/tests/test_config.py | 12 ++-- pyramid/tweens.py | 45 +++++++++++++++ 9 files changed, 265 insertions(+), 134 deletions(-) create mode 100644 docs/api/tweens.rst create mode 100644 pyramid/tweens.py 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/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..7290876e6 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -828,3 +828,131 @@ performed, enabling you to set up the utility in advance: For full details, please read the `Venusian documentation `_. +.. _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 consisting of a :term:`request` object and 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 + :lineno: + + 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: + request, response = handler(request) + finally: + end = time.time() + log.debug('The request took %s seconds' % + (end - start)) + return request, 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 918e3bce4..ebbea35ea 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -920,93 +920,7 @@ class Configurator(object): context in which they have access to the Pyramid :term:`application registry` as well as the Pyramid rendering machinery. - A tween factory (passed as ``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 consisting of a :term:`request` object and a - :term:`response` object. - - Here's an example creating a tween factory and registering it: - - .. code-block:: python - - 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: - request, response = handler(request) - finally: - end = time.time() - log.debug('The request took %s seconds' % - (end - start)) - return request, response - return timing_tween - # if timing support is not enabled, return the original - # handler - return handler - - 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 ``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. - - By default, the ordering of the chain is controlled entirely by the - relative ordering of calls to ``add_tween``. However, the deploying - user can override tween inclusion and ordering 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. - - Note that Pyramid's own exception view handling logic is implemented - as a tween factory: ``pyramid.router.excview_tween_factory``. If - Pyramid exception view handling is desired, and explicit tween - factories are specified via ``pyramid.tweens``, this function must be - added to the ``pyramid.tweens`` setting. If it is not present, - Pyramid will not perform exception view handling. - - 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 config.commit() between calls to ``add_tween``. - - A user can get a representation of both the implicit tween ordering - (the ordering specified by calls to ``add_tween``) and the explicit - request handler ordering (specified by the ``pyramid.tweens`` - 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 ``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 more information, see :ref:`registering_tweens`. .. note:: This feature is new as of Pyramid 1.1.1. """ @@ -1032,7 +946,7 @@ class Configurator(object): registry = self.registry handler_manager = registry.getUtility(ITweens) handler_manager.add(name, tween_factory, explicit) - self.action(('tween', name), register) + self.action(('tween', name, explicit), register) @action_method def add_subscriber(self, subscriber, iface=None): diff --git a/pyramid/router.py b/pyramid/router.py index dcf828377..5faafb4bb 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 @@ -24,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) @@ -190,37 +188,3 @@ class Router(object): finally: manager.pop() -def excview_tween_factory(handler, registry): - has_listeners = registry.has_listeners - adapters = registry.adapters - notify = registry.notify - - def exception_view_handler(request): - attrs = request.__dict__ - try: - request, 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 request, response - - return exception_view_handler - diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 8b11bb22a..d73fd7f7d 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -630,7 +630,7 @@ pyramid.tests.test_config.dummy_include2""", def test_add_tweens_names_distinct(self): from pyramid.interfaces import ITweens - from pyramid.router import excview_tween_factory + from pyramid.tweens import excview_tween_factory def factory1(handler, registry): return handler def factory2(handler, registry): return handler config = self._makeOne() @@ -640,13 +640,13 @@ pyramid.tests.test_config.dummy_include2""", tweens = config.registry.queryUtility(ITweens) self.assertEqual( tweens.implicit, - [('pyramid.router.excview_tween_factory', excview_tween_factory), + [('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.router import excview_tween_factory + from pyramid.tweens import excview_tween_factory config = self._makeOne() config.add_tween('pyramid.tests.test_config.dummy_tween_factory') config.commit() @@ -654,14 +654,14 @@ pyramid.tests.test_config.dummy_include2""", self.assertEqual( tweens.implicit, [ - ('pyramid.router.excview_tween_factory', excview_tween_factory), + ('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.router import excview_tween_factory + from pyramid.tweens import excview_tween_factory class ATween(object): pass atween = ATween() config = self._makeOne() @@ -671,7 +671,7 @@ pyramid.tests.test_config.dummy_include2""", self.assertEqual(len(tweens.implicit), 2) self.assertEqual( tweens.implicit[0], - ('pyramid.router.excview_tween_factory', excview_tween_factory)) + ('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) diff --git a/pyramid/tweens.py b/pyramid/tweens.py new file mode 100644 index 000000000..209768198 --- /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: + request, 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 request, response + + return excview_tween + -- cgit v1.2.3