Merge pull request #2435 from mmerickel/feature/separate-viewderiver-module
separate viewderiver module
1 files added
2 files modified
2 files renamed
New file |
| | |
| | | .. _viewderivers_module: |
| | | |
| | | :mod:`pyramid.viewderivers` |
| | | --------------------------- |
| | | |
| | | .. automodule:: pyramid.viewderivers |
| | | |
| | | .. attribute:: INGRESS |
| | | |
| | | Constant representing the request ingress, for use in ``under`` |
| | | arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. |
| | | |
| | | .. attribute:: VIEW |
| | | |
| | | 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`. |
| | |
| | | apply to any view. Below they are defined in order from furthest to closest to |
| | | the user-defined :term:`view callable`: |
| | | |
| | | ``authdebug_view`` |
| | | |
| | | Used to output useful debugging information when |
| | | ``pyramid.debug_authorization`` is enabled. This element is a no-op |
| | | otherwise. |
| | | |
| | | ``secured_view`` |
| | | |
| | | Enforce the ``permission`` defined on the view. This element is a no-op if no |
| | | permission is defined. Note there will always be a permission defined if a |
| | | default permission was assigned via |
| | | :meth:`pyramid.config.Configurator.set_default_permission`. |
| | | |
| | | This element will also output useful debugging information when |
| | | ``pyramid.debug_authorization`` is enabled. |
| | | |
| | | ``owrapped_view`` |
| | | |
| | |
| | | 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 |
| | | ~~~~~~~~~~~~~~~~~~~~ |
| | | |
| | |
| | | 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 |
| | |
| | | |
| | | 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') |
| | |
| | | ``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 |
| | |
| | | |
| | | .. 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. |
| | |
| | | ) |
| | | |
| | | import pyramid.config.predicates |
| | | import pyramid.config.derivations |
| | | import pyramid.viewderivers |
| | | |
| | | from pyramid.config.derivations import ( |
| | | from pyramid.viewderivers import ( |
| | | INGRESS, |
| | | VIEW, |
| | | preserve_view_attrs, |
| | | view_description, |
| | | requestonly, |
| | |
| | | from pyramid.config.util import ( |
| | | DEFAULT_PHASH, |
| | | MAX_ORDER, |
| | | as_sorted_tuple, |
| | | ) |
| | | |
| | | urljoin = urlparse.urljoin |
| | |
| | | raise ConfigurationError('Unknown view options: %s' % (kw,)) |
| | | |
| | | def _apply_view_derivers(self, info): |
| | | d = pyramid.config.derivations |
| | | # These derivations have fixed order |
| | | d = pyramid.viewderivers |
| | | |
| | | # These derivers are not really derivers and so have fixed order |
| | | outer_derivers = [('attr_wrapped_view', d.attr_wrapped_view), |
| | | ('predicated_view', d.predicated_view)] |
| | | inner_derivers = [('mapped_view', d.mapped_view)] |
| | | |
| | | view = info.original_view |
| | | derivers = self.registry.getUtility(IViewDerivers) |
| | | for name, deriver in reversed( |
| | | outer_derivers + derivers.sorted() + inner_derivers |
| | | ): |
| | | for name, deriver in reversed(outer_derivers + derivers.sorted()): |
| | | view = wraps_view(deriver)(view, info) |
| | | return view |
| | | |
| | |
| | | self.add_view_predicate(name, factory) |
| | | |
| | | @action_method |
| | | def add_view_deriver(self, deriver, name, under=None, over=None): |
| | | def add_view_deriver(self, deriver, name=None, under=None, over=None): |
| | | """ |
| | | .. versionadded:: 1.7 |
| | | |
| | |
| | | restrictions on the name of a view deriver. If left unspecified, the |
| | | name will be constructed from the name of the ``deriver``. |
| | | |
| | | The ``under`` and ``over`` options may be used to control the ordering |
| | | The ``under`` and ``over`` options can be used to control the ordering |
| | | of view derivers by providing hints about where in the view pipeline |
| | | the deriver is used. |
| | | the deriver is used. Each option may be a string or a list of strings. |
| | | At least one view deriver in each, the over and under directions, must |
| | | exist to fully satisfy the constraints. |
| | | |
| | | ``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. |
| | |
| | | """ |
| | | deriver = self.maybe_dotted(deriver) |
| | | |
| | | if under is None and over is None: |
| | | if name is None: |
| | | name = deriver.__name__ |
| | | |
| | | if name in (INGRESS, VIEW): |
| | | raise ConfigurationError('%s is a reserved view deriver name' |
| | | % name) |
| | | |
| | | if under is None: |
| | | under = 'decorated_view' |
| | | |
| | | if over is None: |
| | | over = 'rendered_view' |
| | | |
| | | over = as_sorted_tuple(over) |
| | | under = as_sorted_tuple(under) |
| | | |
| | | if INGRESS in over: |
| | | raise ConfigurationError('%s cannot be over 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 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( |
| | |
| | | def register(): |
| | | derivers = self.registry.queryUtility(IViewDerivers) |
| | | if derivers is None: |
| | | derivers = TopologicalSorter() |
| | | derivers = TopologicalSorter( |
| | | default_before=None, |
| | | default_after=INGRESS, |
| | | first=INGRESS, |
| | | last=VIEW, |
| | | ) |
| | | self.registry.registerUtility(derivers, IViewDerivers) |
| | | derivers.add(name, deriver, before=over, after=under) |
| | | self.action(discriminator, register, introspectables=(intr,), |
| | | order=PHASE1_CONFIG) # must be registered before add_view |
| | | |
| | | def add_default_view_derivers(self): |
| | | d = pyramid.config.derivations |
| | | d = pyramid.viewderivers |
| | | derivers = [ |
| | | ('authdebug_view', d.authdebug_view), |
| | | ('secured_view', d.secured_view), |
| | | ('owrapped_view', d.owrapped_view), |
| | | ('http_cached_view', d.http_cached_view), |
| | | ('decorated_view', d.decorated_view), |
| | | ('rendered_view', d.rendered_view), |
| | | ('mapped_view', d.mapped_view), |
| | | ] |
| | | last = pyramid.util.FIRST |
| | | last = INGRESS |
| | | for name, deriver in derivers: |
| | | self.add_view_deriver(deriver, name=name, under=last) |
| | | self.add_view_deriver( |
| | | deriver, |
| | | name=name, |
| | | under=last, |
| | | over=VIEW, |
| | | ) |
| | | last = name |
| | | |
| | | # ensure rendered_view is over LAST |
| | | self.add_view_deriver( |
| | | d.rendered_view, |
| | | 'rendered_view', |
| | | under=last, |
| | | over=pyramid.util.LAST, |
| | | ) |
| | | |
| | | def derive_view(self, view, attr=None, renderer=None): |
| | | """ |
File was renamed from pyramid/tests/test_config/test_derivations.py |
| | |
| | | self.assertEqual( |
| | | e.args[0], |
| | | 'Could not convert return value of the view callable function ' |
| | | 'pyramid.tests.test_config.test_derivations.view into a response ' |
| | | 'pyramid.tests.test_viewderivers.view into a response ' |
| | | 'object. The value returned was None. You may have forgotten ' |
| | | 'to return a value from the view callable.' |
| | | ) |
| | |
| | | self.assertEqual( |
| | | e.args[0], |
| | | "Could not convert return value of the view callable function " |
| | | "pyramid.tests.test_config.test_derivations.view into a response " |
| | | "pyramid.tests.test_viewderivers.view into a response " |
| | | "object. The value returned was {'a': 1}. You may have " |
| | | "forgotten to define a renderer in the view configuration." |
| | | ) |
| | |
| | | msg = e.args[0] |
| | | self.assertTrue(msg.startswith( |
| | | 'Could not convert return value of the view callable object ' |
| | | '<pyramid.tests.test_config.test_derivations.')) |
| | | '<pyramid.tests.test_viewderivers.')) |
| | | self.assertTrue(msg.endswith( |
| | | '> into a response object. The value returned was None. You ' |
| | | 'may have forgotten to return a value from the view callable.')) |
| | |
| | | e.args[0], |
| | | 'Could not convert return value of the view callable ' |
| | | 'method __call__ of ' |
| | | 'class pyramid.tests.test_config.test_derivations.AView into a ' |
| | | 'class pyramid.tests.test_viewderivers.AView into a ' |
| | | 'response object. The value returned was None. You may have ' |
| | | 'forgotten to return a value from the view callable.' |
| | | ) |
| | |
| | | e.args[0], |
| | | 'Could not convert return value of the view callable ' |
| | | 'method theviewmethod of ' |
| | | 'class pyramid.tests.test_config.test_derivations.AView into a ' |
| | | 'class pyramid.tests.test_viewderivers.AView into a ' |
| | | 'response object. The value returned was None. You may have ' |
| | | 'forgotten to return a value from the view callable.' |
| | | ) |
| | |
| | | self.assertFalse(result is view) |
| | | self.assertEqual(view.__module__, result.__module__) |
| | | self.assertEqual(view.__doc__, result.__doc__) |
| | | self.assertTrue('test_derivations' in result.__name__) |
| | | self.assertTrue('test_viewderivers' in result.__name__) |
| | | self.assertFalse(hasattr(result, '__call_permissive__')) |
| | | self.assertEqual(result(None, None), response) |
| | | |
| | |
| | | from pyramid.interfaces import IViewDerivers |
| | | |
| | | self.config.add_view_deriver(None, 'deriv1') |
| | | self.config.add_view_deriver(None, 'deriv2', over='deriv1') |
| | | self.config.add_view_deriver(None, 'deriv3', under='deriv2') |
| | | self.config.add_view_deriver(None, 'deriv2', 'decorated_view', 'deriv1') |
| | | self.config.add_view_deriver(None, 'deriv3', 'deriv2', 'deriv1') |
| | | |
| | | derivers = self.config.registry.getUtility(IViewDerivers) |
| | | derivers_sorted = derivers.sorted() |
| | | dlist = [d for (d, _) in derivers_sorted] |
| | | self.assertEqual([ |
| | | 'authdebug_view', |
| | | 'secured_view', |
| | | 'owrapped_view', |
| | | 'http_cached_view', |
| | |
| | | 'deriv3', |
| | | 'deriv1', |
| | | 'rendered_view', |
| | | 'mapped_view', |
| | | ], dlist) |
| | | |
| | | def test_right_order_implicit(self): |
| | |
| | | derivers_sorted = derivers.sorted() |
| | | dlist = [d for (d, _) in derivers_sorted] |
| | | self.assertEqual([ |
| | | 'authdebug_view', |
| | | 'secured_view', |
| | | 'owrapped_view', |
| | | 'http_cached_view', |
| | |
| | | 'deriv2', |
| | | 'deriv1', |
| | | 'rendered_view', |
| | | 'mapped_view', |
| | | ], dlist) |
| | | |
| | | def test_right_order_under_rendered_view(self): |
| | | from pyramid.interfaces import IViewDerivers |
| | | |
| | | self.config.add_view_deriver(None, 'deriv1', under='rendered_view') |
| | | self.config.add_view_deriver(None, 'deriv1', 'rendered_view', 'mapped_view') |
| | | |
| | | derivers = self.config.registry.getUtility(IViewDerivers) |
| | | derivers_sorted = derivers.sorted() |
| | | dlist = [d for (d, _) in derivers_sorted] |
| | | self.assertEqual([ |
| | | 'authdebug_view', |
| | | 'secured_view', |
| | | 'owrapped_view', |
| | | 'http_cached_view', |
| | | 'decorated_view', |
| | | 'rendered_view', |
| | | 'deriv1', |
| | | 'mapped_view', |
| | | ], dlist) |
| | | |
| | | |
| | | def test_right_order_under_rendered_view_others(self): |
| | | from pyramid.interfaces import IViewDerivers |
| | | |
| | | self.config.add_view_deriver(None, 'deriv1', under='rendered_view') |
| | | self.config.add_view_deriver(None, 'deriv1', 'rendered_view', 'mapped_view') |
| | | self.config.add_view_deriver(None, 'deriv2') |
| | | self.config.add_view_deriver(None, 'deriv3') |
| | | |
| | |
| | | derivers_sorted = derivers.sorted() |
| | | dlist = [d for (d, _) in derivers_sorted] |
| | | self.assertEqual([ |
| | | 'authdebug_view', |
| | | 'secured_view', |
| | | 'owrapped_view', |
| | | 'http_cached_view', |
| | |
| | | 'deriv2', |
| | | 'rendered_view', |
| | | 'deriv1', |
| | | 'mapped_view', |
| | | ], dlist) |
| | | |
| | | |
| | |
| | | def __init__(self): |
| | | self.response = DummyResponse() |
| | | |
| | | def deriv1(view, value, **kw): |
| | | def deriv1(view, info): |
| | | flags['deriv1'] = True |
| | | return view |
| | | |
| | | def deriv2(view, value, **kw): |
| | | def deriv2(view, info): |
| | | flags['deriv2'] = True |
| | | return view |
| | | |
| | |
| | | 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() |
| | | view = lambda *arg: response |
| | | response.deriv = [] |
| | | |
| | | def deriv1(view, value, **kw): |
| | | def deriv1(view, info): |
| | | response.deriv.append('deriv1') |
| | | return view |
| | | |
| | | def deriv2(view, value, **kw): |
| | | def deriv2(view, info): |
| | | response.deriv.append('deriv2') |
| | | return view |
| | | |
| | | def deriv3(view, value, **kw): |
| | | def deriv3(view, info): |
| | | response.deriv.append('deriv3') |
| | | return view |
| | | |
| | | self.config.add_view_deriver(deriv1, 'deriv1') |
| | | self.config.add_view_deriver(deriv2, 'deriv2', under='deriv1') |
| | | self.config.add_view_deriver(deriv3, 'deriv3', over='deriv2') |
| | | self.config.add_view_deriver(deriv2, 'deriv2', INGRESS, 'deriv1') |
| | | self.config.add_view_deriver(deriv3, 'deriv3', 'deriv2', 'deriv1') |
| | | result = self.config._derive_view(view) |
| | | self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) |
| | | self.assertEqual(response.deriv, ['deriv1', 'deriv3', 'deriv2']) |
| | | |
| | | def test_add_deriver_without_name(self): |
| | | from pyramid.interfaces import IViewDerivers |
| | | def deriv1(view, info): pass |
| | | self.config.add_view_deriver(deriv1) |
| | | derivers = self.config.registry.getUtility(IViewDerivers) |
| | | self.assertTrue('deriv1' in derivers.names) |
| | | |
| | | def test_add_deriver_reserves_ingress(self): |
| | | from pyramid.exceptions import ConfigurationError |
| | | from pyramid.viewderivers import INGRESS |
| | | def deriv1(view, info): pass |
| | | self.assertRaises( |
| | | ConfigurationError, self.config.add_view_deriver, deriv1, INGRESS) |
| | | |
| | | 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=INGRESS) |
| | | except ConfigurationError as ex: |
| | | self.assertTrue('cannot be over INGRESS' in ex.args[0]) |
| | | else: # pragma: no cover |
| | | raise AssertionError |
| | | |
| | | def test_add_deriver_enforces_view_is_last(self): |
| | | from pyramid.exceptions import ConfigurationError |
| | | from pyramid.viewderivers import VIEW |
| | | def deriv1(view, info): pass |
| | | try: |
| | | self.config.add_view_deriver(deriv1, under=VIEW) |
| | | except ConfigurationError as ex: |
| | | self.assertTrue('cannot be under VIEW' in ex.args[0]) |
| | | else: # pragma: no cover |
| | | raise AssertionError |
| | | |
| | | def test_add_deriver_enforces_mapped_view_is_last(self): |
| | | from pyramid.exceptions import ConfigurationError |
| | | def deriv1(view, info): pass |
| | | try: |
| | | self.config.add_view_deriver(deriv1, 'deriv1', under='mapped_view') |
| | | except ConfigurationError as ex: |
| | | self.assertTrue('cannot be under "mapped_view"' in ex.args[0]) |
| | | else: # pragma: no cover |
| | | raise AssertionError |
| | | |
| | | |
| | | class TestDeriverIntegration(unittest.TestCase): |
File was renamed from pyramid/config/derivations.py |
| | |
| | | http_cached_view.options = ('http_cache',) |
| | | |
| | | def secured_view(view, info): |
| | | for wrapper in (_secured_view, _authdebug_view): |
| | | view = wraps_view(wrapper)(view, info) |
| | | return view |
| | | |
| | | secured_view.options = ('permission',) |
| | | |
| | | def _secured_view(view, info): |
| | | permission = info.options.get('permission') |
| | | if permission == NO_PERMISSION_REQUIRED: |
| | | # allow views registered within configurations that have a |
| | |
| | | |
| | | return wrapped_view |
| | | |
| | | secured_view.options = ('permission',) |
| | | |
| | | def authdebug_view(view, info): |
| | | def _authdebug_view(view, info): |
| | | wrapped_view = view |
| | | settings = info.settings |
| | | permission = info.options.get('permission') |
| | |
| | | wrapped_view = _authdebug_view |
| | | |
| | | return wrapped_view |
| | | |
| | | authdebug_view.options = ('permission',) |
| | | |
| | | def predicated_view(view, info): |
| | | preds = info.predicates |
| | |
| | | return decorator(view) |
| | | |
| | | decorated_view.options = ('decorator',) |
| | | |
| | | VIEW = 'VIEW' |
| | | INGRESS = 'INGRESS' |