From c231d8174e811eec5a3faeafa5aee60757c6d31f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 8 Apr 2016 00:47:01 -0500 Subject: update constraints for derivers as well as docs --- docs/api/viewderivers.rst | 7 +++--- docs/narr/hooks.rst | 34 +++++++++++++++++++------ pyramid/config/views.py | 51 +++++++++++++++++++------------------- pyramid/tests/test_viewderivers.py | 49 +++++++++++++++++++++--------------- pyramid/viewderivers.py | 2 +- 5 files changed, 85 insertions(+), 58 deletions(-) diff --git a/docs/api/viewderivers.rst b/docs/api/viewderivers.rst index a4ec107b6..2a141501e 100644 --- a/docs/api/viewderivers.rst +++ b/docs/api/viewderivers.rst @@ -10,7 +10,8 @@ Constant representing the request ingress, for use in ``under`` arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. - .. attribute:: MAPPED_VIEW + .. attribute:: VIEW - Constant representing the closest view deriver, for use in ``over`` - arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. + Constant representing the :term:`view callable` at the end of the view + pipeline, for use in ``over`` arguments to + :meth:`pyramid.config.Configurator.add_view_deriver`. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 3a1ad8363..2c3782387 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1617,6 +1617,14 @@ the user-defined :term:`view callable`: view pipeline interface to accept ``(context, request)`` from all previous view derivers. +.. warning:: + + Any view derivers defined ``under`` the ``rendered_view`` are not + guaranteed to receive a valid response object. Rather they will receive the + result from the :term:`view mapper` which is likely the original response + returned from the view. This is possibly a dictionary for a renderer but it + may be any Python object that may be adapted into a response. + Custom View Derivers ~~~~~~~~~~~~~~~~~~~~ @@ -1642,7 +1650,7 @@ view pipeline: response.headers['X-View-Performance'] = '%.3f' % (end - start,) return wrapper_view - config.add_view_deriver(timing_view, 'timing view') + config.add_view_deriver(timing_view) View derivers are unique in that they have access to most of the options passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what @@ -1668,7 +1676,7 @@ token unless ``disable_csrf=True`` is passed to the view: require_csrf_view.options = ('disable_csrf',) - config.add_view_deriver(require_csrf_view, 'require_csrf_view') + config.add_view_deriver(require_csrf_view) def protected_view(request): return Response('protected') @@ -1691,13 +1699,19 @@ By default, every new view deriver is added between the ``decorated_view`` and ``rendered_view`` built-in derivers. It is possible to customize this ordering using the ``over`` and ``under`` options. Each option can use the names of other view derivers in order to specify an ordering. There should rarely be a -reason to worry about the ordering of the derivers. +reason to worry about the ordering of the derivers except when the deriver +depends on other operations in the view pipeline. Both ``over`` and ``under`` may also be iterables of constraints. For either option, if one or more constraints was defined, at least one must be satisfied, else a :class:`pyramid.exceptions.ConfigurationError` will be raised. This may be used to define fallback constraints if another deriver is missing. +Two sentinel values exist, :attr:`pyramid.viewderivers.INGRESS` and +:attr:`pyramid.viewderivers.VIEW`, which may be used when specifying +constraints at the edges of the view pipeline. For example, to add a deriver +at the start of the pipeline you may use ``under=INGRESS``. + It is not possible to add a view deriver under the ``mapped_view`` as the :term:`view mapper` is intimately tied to the signature of the user-defined :term:`view callable`. If you simply need to know what the original view @@ -1707,8 +1721,12 @@ deriver. .. warning:: - Any view derivers defined ``under`` the ``rendered_view`` are not - guaranteed to receive a valid response object. Rather they will receive the - result from the :term:`view mapper` which is likely the original response - returned from the view. This is possibly a dictionary for a renderer but it - may be any Python object that may be adapted into a response. + The default constraints for any view deriver are ``over='rendered_view'`` + and ``under='decorated_view'``. When escaping these constraints you must + take care to avoid cyclic dependencies between derivers. For example, if + you want to add a new view deriver before ``secured_view`` then + simply specifying ``over='secured_view'`` is not enough, because the + default is also under ``decorated view`` there will be an unsatisfiable + cycle. You must specify a valid ``under`` constraint as well, such as + ``under=INGRESS`` to fall between INGRESS and ``secured_view`` at the + beginning of the view pipeline. diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1e161177b..3f6a9080d 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -80,7 +80,7 @@ import pyramid.viewderivers from pyramid.viewderivers import ( INGRESS, - MAPPED_VIEW, + VIEW, preserve_view_attrs, view_description, requestonly, @@ -1115,9 +1115,12 @@ class ViewsConfiguratorMixin(object): ``under`` means closer to the user-defined :term:`view callable`, and ``over`` means closer to view pipeline ingress. - Specifying neither ``under`` nor ``over`` is equivalent to specifying - ``over='rendered_view'`` and ``under='decorated_view'``, placing the - deriver somewhere between the ``decorated_view`` and ``rendered_view`` + The default value for ``over`` is ``rendered_view`` and ``under`` is + ``decorated_view``. This places the deriver somewhere between the two + in the view pipeline. If the deriver should be placed elsewhere in the + pipeline, such as above ``decorated_view``, then you MUST also specify + ``under`` to something earlier in the order, or a + ``CyclicDependencyError`` will be raised when trying to sort the derivers. See :ref:`view_derivers` for more information. @@ -1128,33 +1131,30 @@ class ViewsConfiguratorMixin(object): if name is None: name = deriver.__name__ - if name in (INGRESS,): + if name in (INGRESS, VIEW): raise ConfigurationError('%s is a reserved view deriver name' % name) - if under is None and over is None: + if under is None: under = 'decorated_view' + + if over is None: over = 'rendered_view' - if over is None and name != MAPPED_VIEW: - raise ConfigurationError('must specify an "over" constraint for ' - 'the %s view deriver' % name) - elif over is not None: - over = as_sorted_tuple(over) + over = as_sorted_tuple(over) + under = as_sorted_tuple(under) - if under is None: - raise ConfigurationError('must specify an "under" constraint for ' - 'the %s view deriver' % name) - else: - under = as_sorted_tuple(under) + if INGRESS in over: + raise ConfigurationError('%s cannot be over INGRESS' % name) - if over is not None and INGRESS in over: - raise ConfigurationError('%s cannot be over view deriver INGRESS' - % name) + # ensure everything is always over mapped_view + if VIEW in over and name != 'mapped_view': + over = as_sorted_tuple(over + ('mapped_view',)) - if MAPPED_VIEW in under: - raise ConfigurationError('%s cannot be under view deriver ' - 'MAPPED_VIEW' % name) + if VIEW in under: + raise ConfigurationError('%s cannot be under VIEW' % name) + if 'mapped_view' in under: + raise ConfigurationError('%s cannot be under "mapped_view"' % name) discriminator = ('view deriver', name) intr = self.introspectable( @@ -1173,7 +1173,7 @@ class ViewsConfiguratorMixin(object): default_before=None, default_after=INGRESS, first=INGRESS, - last=MAPPED_VIEW, + last=VIEW, ) self.registry.registerUtility(derivers, IViewDerivers) derivers.add(name, deriver, before=over, after=under) @@ -1188,6 +1188,7 @@ class ViewsConfiguratorMixin(object): ('http_cached_view', d.http_cached_view), ('decorated_view', d.decorated_view), ('rendered_view', d.rendered_view), + ('mapped_view', d.mapped_view), ] last = INGRESS for name, deriver in derivers: @@ -1195,12 +1196,10 @@ class ViewsConfiguratorMixin(object): deriver, name=name, under=last, - over=MAPPED_VIEW, + over=VIEW, ) last = name - self.add_view_deriver(d.mapped_view, name=MAPPED_VIEW, under=last) - def derive_view(self, view, attr=None, renderer=None): """ Create a :term:`view callable` using the function, instance, diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index dd142769b..1823beb4d 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -1239,6 +1239,25 @@ class TestAddDeriver(unittest.TestCase): self.assertFalse(flags.get('deriv1')) self.assertTrue(flags.get('deriv2')) + def test_override_mapped_view(self): + from pyramid.viewderivers import VIEW + response = DummyResponse() + view = lambda *arg: response + flags = {} + + def deriv1(view, info): + flags['deriv1'] = True + return view + + result = self.config._derive_view(view) + self.assertFalse(flags.get('deriv1')) + + flags.clear() + self.config.add_view_deriver( + deriv1, name='mapped_view', under='rendered_view', over=VIEW) + result = self.config._derive_view(view) + self.assertTrue(flags.get('deriv1')) + def test_add_multi_derivers_ordered(self): from pyramid.viewderivers import INGRESS response = DummyResponse() @@ -1277,34 +1296,25 @@ class TestAddDeriver(unittest.TestCase): self.assertRaises( ConfigurationError, self.config.add_view_deriver, deriv1, INGRESS) - def test_add_deriver_enforces_over_is_defined(self): - from pyramid.exceptions import ConfigurationError - def deriv1(view, info): pass - try: - self.config.add_view_deriver(deriv1, under='rendered_view') - except ConfigurationError as ex: - self.assertTrue('must specify an "over" constraint' in ex.args[0]) - else: # pragma: no cover - raise AssertionError - - def test_add_deriver_enforces_under_is_defined(self): + def test_add_deriver_enforces_ingress_is_first(self): from pyramid.exceptions import ConfigurationError + from pyramid.viewderivers import INGRESS def deriv1(view, info): pass try: - self.config.add_view_deriver(deriv1, over='rendered_view') + self.config.add_view_deriver(deriv1, over=INGRESS) except ConfigurationError as ex: - self.assertTrue('must specify an "under" constraint' in ex.args[0]) + self.assertTrue('cannot be over INGRESS' in ex.args[0]) else: # pragma: no cover raise AssertionError - def test_add_deriver_enforces_ingress_is_first(self): + def test_add_deriver_enforces_view_is_last(self): from pyramid.exceptions import ConfigurationError - from pyramid.viewderivers import INGRESS + from pyramid.viewderivers import VIEW def deriv1(view, info): pass try: - self.config.add_view_deriver(deriv1, under='rendered_view', over=INGRESS) + self.config.add_view_deriver(deriv1, under=VIEW) except ConfigurationError as ex: - self.assertTrue('cannot be over view deriver INGRESS' in ex.args[0]) + self.assertTrue('cannot be under VIEW' in ex.args[0]) else: # pragma: no cover raise AssertionError @@ -1312,10 +1322,9 @@ class TestAddDeriver(unittest.TestCase): from pyramid.exceptions import ConfigurationError def deriv1(view, info): pass try: - self.config.add_view_deriver( - deriv1, 'deriv1', 'mapped_view', 'rendered_view') + self.config.add_view_deriver(deriv1, 'deriv1', under='mapped_view') except ConfigurationError as ex: - self.assertTrue('cannot be under view deriver MAPPED_VIEW' in ex.args[0]) + self.assertTrue('cannot be under "mapped_view"' in ex.args[0]) else: # pragma: no cover raise AssertionError diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index f97099cc8..8061e5d4a 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -455,5 +455,5 @@ def decorated_view(view, info): decorated_view.options = ('decorator',) -MAPPED_VIEW = 'mapped_view' +VIEW = 'VIEW' INGRESS = 'INGRESS' -- cgit v1.2.3