diff --git a/docs/api/tweens.rst b/docs/api/tweens.rst index ddacd2cdef..d1f3178965 100644 --- a/docs/api/tweens.rst +++ b/docs/api/tweens.rst @@ -7,6 +7,8 @@ .. autofunction:: excview_tween_factory + .. autofunction:: response_callbacks_tween_factory + .. attribute:: MAIN Constant representing the main Pyramid handling function, for use in @@ -21,5 +23,9 @@ .. attribute:: EXCVIEW Constant representing the exception view tween, for use in ``under`` - and ``over`` arguments to - :meth:`pyramid.config.Configurator.add_tween`. + and ``over`` arguments to :meth:`pyramid.config.Configurator.add_tween`. + + .. attribute:: RESPONSE_CALLBACKS + + Constant representing the response callbacks tween for use in ``under`` + and ``over`` arguments to :meth:`pyramid.config.Configurator.add_tween`. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 63279027a5..baf7dec402 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1249,6 +1249,7 @@ very last tween factory added) as its request handler function. For example: The above example will generate an implicit tween chain that looks like this:: INGRESS (implicit) + pyramid.tweens.response_callbacks_tween_factory (implicit) myapp.tween_factory2 myapp.tween_factory1 pyramid.tweens.excview_tween_factory (implicit) @@ -1276,6 +1277,7 @@ Allowable values for ``under`` or ``over`` (or both) are: - one of the constants :attr:`pyramid.tweens.MAIN`, :attr:`pyramid.tweens.INGRESS`, or :attr:`pyramid.tweens.EXCVIEW`, or + :attr:`pyramid.tweens.RESPONSE_CALLBACKS`, or - an iterable of any combination of the above. This allows the user to specify fallbacks if the desired tween is not included, as well as compatibility @@ -1296,7 +1298,8 @@ order) the main Pyramid request handler. import pyramid.tweens - config.add_tween('myapp.tween_factory', over=pyramid.tweens.MAIN) + config.add_tween('myapp.tween_factory', + under=pyramid.tweens.EXCVIEW, over=pyramid.tweens.MAIN) The above example will generate an implicit tween chain that looks like this:: @@ -1315,7 +1318,7 @@ factory "above" the main handler but "below" a separately added tween factory: import pyramid.tweens config.add_tween('myapp.tween_factory1', - over=pyramid.tweens.MAIN) + under=pyramid.tweens.EXCVIEW, over=pyramid.tweens.MAIN) config.add_tween('myapp.tween_factory2', over=pyramid.tweens.MAIN, under='myapp.tween_factory1') @@ -1328,8 +1331,12 @@ The above example will generate an implicit tween chain that looks like this:: myapp.tween_factory2 MAIN (implicit) -Specifying neither ``over`` nor ``under`` is equivalent to specifying -``under=INGRESS``. +The default value for ``over`` is :attr:`pyramid.tweens.EXCVIEW` and ``under`` +is :attr:`pyramid.tweens.RESPONSE_CALLBACKS`. This places the tween somewhere +between the two in the tween ordering. If the tween should be placed elsewhere, +such as under ``EXCVIEW``, then you MUST also specify ``over`` to +something later in the order (such as ``MAIN``), or a ``CyclicDependencyError`` +will be raised when trying to sort the tweens. If all options for ``under`` (or ``over``) cannot be found in the current configuration, it is an error. If some options are specified purely for diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index 16712ab16c..3ef2684b31 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -10,22 +10,28 @@ from pyramid.exceptions import ConfigurationError from pyramid.tweens import ( - MAIN, - INGRESS, EXCVIEW, + INGRESS, + MAIN, + RESPONSE_CALLBACKS, ) from pyramid.config.util import ( action_method, + as_sorted_tuple, TopologicalSorter, ) -from pyramid.util import is_string_or_iterable class TweensConfiguratorMixin(object): def add_tween(self, tween_factory, under=None, over=None): """ .. versionadded:: 1.2 + .. versionchanged:: 1.9 + The default values of ``under=INGRESS`` and ``over=MAIN`` were + been changed to ``under=RESPONSE_CALLBACKS`` and + ``over=EXCVIEW``. + 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 @@ -80,15 +86,21 @@ def add_tween(self, tween_factory, under=None, over=None): If all options for ``under`` (or ``over``) cannot be found in the current configuration, it is an error. If some options are specified purely for compatibilty with other tweens, just add a fallback of - MAIN or INGRESS. For example, ``under=('mypkg.someothertween', - 'mypkg.someothertween2', INGRESS)``. This constraint will require + ``MAIN``, ``INGRESS``, ``RESPONSE_CALLBACKS``, or ``EXCVIEW``. For + example, ``under=('mypkg.someothertween', 'mypkg.someothertween2', + RESPONSE_CALLBACKS)``. This constraint will require the tween to be located under both the 'mypkg.someothertween' tween, - the 'mypkg.someothertween2' tween, and INGRESS. If any of these is - not in the current configuration, this constraint will only organize - itself based on the tweens that are present. - - Specifying neither ``over`` nor ``under`` is equivalent to specifying - ``under=INGRESS``. + the 'mypkg.someothertween2' tween, and ``RESPONSE_CALLBACKS``. If any + of these is not in the current configuration, this constraint will + only organize itself based on the tweens that are present. + + The default value for ``over`` is ``EXCVIEW`` and ``under`` is + ``RESPONSE_CALLBACKS``. This places the deriver somewhere between the + two in the tween ordering. If the deriver should be placed elsewhere, + such as under ``EXCVIEW``, then you MUST also specify ``over`` to + something later in the order (such as ``MAIN``), or a + ``CyclicDependencyError`` will be raised when trying to sort the + tweens. Implicit tween ordering is obviously only best-effort. Pyramid will attempt to present an implicit order of tweens as best it can, but @@ -107,7 +119,16 @@ def add_tween(self, tween_factory, under=None, over=None): explicit=False) def add_default_tweens(self): - self.add_tween(EXCVIEW) + self.add_tween( + 'pyramid.tweens.excview_tween_factory', + over=MAIN, + under=[RESPONSE_CALLBACKS, INGRESS], + ) + self.add_tween( + 'pyramid.tweens.response_callbacks_tween_factory', + over=[EXCVIEW, MAIN], + under=INGRESS, + ) @action_method def _add_tween(self, tween_factory, under=None, over=None, explicit=False): @@ -125,27 +146,41 @@ def _add_tween(self, tween_factory, under=None, over=None, explicit=False): tween_factory = self.maybe_dotted(tween_factory) + # ensure over/under contain only strings for t, p in [('over', over), ('under', under)]: if p is not None: - if not is_string_or_iterable(p): + if is_nonstr_iter(p): + for v in p: + if not isinstance(v, string_types): + raise ConfigurationError( + '"%s" must contain strings, not %s' % (t, v)) + elif not isinstance(p, string_types): raise ConfigurationError( '"%s" must be a string or iterable, not %s' % (t, p)) - if over is INGRESS or is_nonstr_iter(over) and INGRESS in over: + if over is None: + over = EXCVIEW + + if under is None: + under = RESPONSE_CALLBACKS + + over = as_sorted_tuple(over) + under = as_sorted_tuple(under) + + if INGRESS in over: raise ConfigurationError('%s cannot be over INGRESS' % name) - if under is MAIN or is_nonstr_iter(under) and MAIN in under: + if MAIN in under: raise ConfigurationError('%s cannot be under MAIN' % name) registry = self.registry introspectables = [] - tweens = registry.queryUtility(ITweens) - if tweens is None: - tweens = Tweens() - registry.registerUtility(tweens, ITweens) - def register(): + tweens = registry.queryUtility(ITweens) + if tweens is None: + tweens = Tweens() + registry.registerUtility(tweens, ITweens) if explicit: tweens.add_explicit(name, tween_factory) else: diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 67bba9593e..85ce826a4e 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -4,7 +4,7 @@ from pyramid.compat import ( bytes_, getargspec, - is_nonstr_iter + is_nonstr_iter, ) from pyramid.compat import im_func @@ -23,6 +23,12 @@ MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() +def as_sorted_tuple(val): + if not is_nonstr_iter(val): + val = (val,) + val = tuple(sorted(val)) + return val + class not_(object): """ diff --git a/pyramid/request.py b/pyramid/request.py index c1c1da5143..facbe7e083 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -20,6 +20,7 @@ iteritems_, ) +from pyramid.events import NewResponse from pyramid.decorator import reify from pyramid.i18n import LocalizerRequestMixin from pyramid.response import Response, _get_response_factory @@ -332,3 +333,12 @@ def apply_request_extensions(request, extensions=None): InstancePropertyHelper.apply_properties( request, extensions.descriptors) + +def _execute_response_callbacks(request, response): + """ Execute response callbacks and emit a NewResponse event.""" + registry = request.registry + if getattr(request, 'response_callbacks', False): + request._process_response_callbacks(response) + + if registry.has_listeners: + registry.notify(NewResponse(request, response)) diff --git a/pyramid/router.py b/pyramid/router.py index 8b7b7b6bc2..970abf217c 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -20,7 +20,6 @@ from pyramid.events import ( ContextFound, NewRequest, - NewResponse, BeforeTraversal, ) @@ -28,6 +27,7 @@ from pyramid.request import Request from pyramid.view import _call_view from pyramid.request import apply_request_extensions +from pyramid.request import _execute_response_callbacks from pyramid.threadlocal import manager from pyramid.traversal import ( @@ -208,8 +208,6 @@ def make_request(self, environ): def invoke_request(self, request, _use_tweens=True, _apply_extensions=False): registry = self.registry - has_listeners = self.registry.has_listeners - notify = self.registry.notify threadlocals = {'registry': registry, 'request': request} manager = self.threadlocal_manager manager.push(threadlocals) @@ -225,12 +223,13 @@ def invoke_request(self, request, extensions = self.request_extensions if _apply_extensions and extensions is not None: apply_request_extensions(request, extensions=extensions) - response = handle_request(request) - if request.response_callbacks: - request._process_response_callbacks(response) + response = handle_request(request) - has_listeners and notify(NewResponse(request, response)) + # bw-compat, in Pyramid < 1.9 callbacks were executed even + # when use_tweens was false + if not _use_tweens: + _execute_response_callbacks(request, response) return response diff --git a/pyramid/tests/test_config/test_tweens.py b/pyramid/tests/test_config/test_tweens.py index 9c34334680..05fa1dd8f3 100644 --- a/pyramid/tests/test_config/test_tweens.py +++ b/pyramid/tests/test_config/test_tweens.py @@ -14,6 +14,7 @@ def _makeOne(self, *arg, **kw): def test_add_tweens_names_distinct(self): from pyramid.interfaces import ITweens from pyramid.tweens import excview_tween_factory + from pyramid.tweens import response_callbacks_tween_factory def factory1(handler, registry): return handler def factory2(handler, registry): return handler config = self._makeOne() @@ -27,6 +28,8 @@ def factory2(handler, registry): return handler self.assertEqual( implicit, [ + ('pyramid.tweens.response_callbacks_tween_factory', + response_callbacks_tween_factory), ('pyramid.tests.test_config.dummy_tween_factory2', dummy_tween_factory2), ('pyramid.tests.test_config.dummy_tween_factory', @@ -39,6 +42,7 @@ def factory2(handler, registry): return handler def test_add_tweens_names_with_underover(self): from pyramid.interfaces import ITweens from pyramid.tweens import excview_tween_factory + from pyramid.tweens import response_callbacks_tween_factory from pyramid.tweens import MAIN config = self._makeOne() config.add_tween( @@ -50,16 +54,39 @@ def test_add_tweens_names_with_underover(self): under='pyramid.tests.test_config.dummy_tween_factory') config.commit() tweens = config.registry.queryUtility(ITweens) - implicit = tweens.implicit() self.assertEqual( - implicit, + tweens.implicit(), [ + ('pyramid.tweens.response_callbacks_tween_factory', + response_callbacks_tween_factory), + ('pyramid.tests.test_config.dummy_tween_factory', + dummy_tween_factory), + ('pyramid.tests.test_config.dummy_tween_factory2', + dummy_tween_factory2), ('pyramid.tweens.excview_tween_factory', excview_tween_factory), + ]) + + def test_add_tween_default_order(self): + from pyramid.interfaces import ITweens + from pyramid.tweens import excview_tween_factory + from pyramid.tweens import response_callbacks_tween_factory + config = self._makeOne() + config.add_tween('pyramid.tests.test_config.dummy_tween_factory') + config.add_tween('pyramid.tests.test_config.dummy_tween_factory2', + under='pyramid.tests.test_config.dummy_tween_factory') + config.commit() + tweens = config.registry.queryUtility(ITweens) + self.assertEqual( + tweens.implicit(), + [ + ('pyramid.tweens.response_callbacks_tween_factory', + response_callbacks_tween_factory), ('pyramid.tests.test_config.dummy_tween_factory', dummy_tween_factory), ('pyramid.tests.test_config.dummy_tween_factory2', dummy_tween_factory2), - ]) + ('pyramid.tweens.excview_tween_factory', excview_tween_factory), + ]) def test_add_tweens_names_with_under_nonstringoriter(self): from pyramid.exceptions import ConfigurationError @@ -77,9 +104,30 @@ def test_add_tweens_names_with_over_nonstringoriter(self): 'pyramid.tests.test_config.dummy_tween_factory', over=False) + def test_add_tweens_names_with_over_nonstriter(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', + over=[MAIN, object()]) + + def test_add_tweens_names_with_under_nonstriter(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', + under=[INGRESS, object()]) + def test_add_tween_dottedname(self): from pyramid.interfaces import ITweens from pyramid.tweens import excview_tween_factory + from pyramid.tweens import response_callbacks_tween_factory config = self._makeOne() config.add_tween('pyramid.tests.test_config.dummy_tween_factory') config.commit() @@ -87,6 +135,8 @@ def test_add_tween_dottedname(self): self.assertEqual( tweens.implicit(), [ + ('pyramid.tweens.response_callbacks_tween_factory', + response_callbacks_tween_factory), ('pyramid.tests.test_config.dummy_tween_factory', dummy_tween_factory), ('pyramid.tweens.excview_tween_factory', diff --git a/pyramid/tweens.py b/pyramid/tweens.py index a842b11333..61a7b3d011 100644 --- a/pyramid/tweens.py +++ b/pyramid/tweens.py @@ -6,6 +6,7 @@ IExceptionViewClassifier, IRequest, ) +from pyramid.request import _execute_response_callbacks from zope.interface import providedBy from pyramid.view import _call_view @@ -65,6 +66,19 @@ def excview_tween(request): return excview_tween +def response_callbacks_tween_factory(handler, registry): + """ A :term:`tween` factory which produces a tween that processes + any response callbacks registered using + :meth:`pyramid.request.Request.add_response_callback`. + + """ + def response_callbacks_tween(request): + response = handler(request) + _execute_response_callbacks(request, response) + return response + return response_callbacks_tween + MAIN = 'MAIN' INGRESS = 'INGRESS' EXCVIEW = 'pyramid.tweens.excview_tween_factory' +RESPONSE_CALLBACKS = 'pyramid.tweens.response_callbacks_tween_factory' diff --git a/pyramid/util.py b/pyramid/util.py index 2827884a34..e22f69b026 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -35,12 +35,6 @@ class DottedNameResolver(_DottedNameResolver): def __init__(self, package=None): # default to package = None for bw compat _DottedNameResolver.__init__(self, package) -def is_string_or_iterable(v): - if isinstance(v, string_types): - return True - if hasattr(v, '__iter__'): - return True - def as_sorted_tuple(val): if not is_nonstr_iter(val): val = (val,)