From 2253647075ace9e99171f3e227f5debbcafdd8b8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 10:46:57 -0600 Subject: first cut at a re-entrant configurator where tests still pass --- pyramid/config/__init__.py | 67 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index cfa35ec6c..83683daeb 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -975,7 +975,7 @@ class Configurator( class ActionState(object): def __init__(self): # NB "actions" is an API, dep'd upon by pyramid_zcml's load_zcml func - self.actions = [] + self.actions = [] self._seen_files = set() def processSpec(self, spec): @@ -1059,10 +1059,54 @@ class ActionState(object): >>> output [('f', (1,), {}), ('f', (2,), {})] - """ + The execution is re-entrant such that actions may be added by other + actions with the one caveat that the order of any added actions must + be equal to or larger than the current action. + + >>> output = [] + >>> def f(*a, **k): + ... output.append(('f', a, k)) + ... context.actions.append((3, g, (8,), {})) + >>> def g(*a, **k): + ... output.append(('g', a, k)) + >>> context.actions = [ + ... (1, f, (1,)), + ... (2, f, (2,)), + ... ] + >>> context.execute_actions() + >>> output + [('f', (1,), {}), ('f', (2,), {}), ('g', (8,), {})] + """ try: - for action in resolveConflicts(self.actions): + all_actions = self.actions + self.actions = [] + executed_actions = [] + + # resolve the new action list against what we have already + # executed -- if a new action appears intertwined in the list + # of already-executed actions then someone wrote a broken + # re-entrant action because it scheduled the action *after* it + # should have been executed (as defined by the action order) + def resume(actions): + for a, b in itertools.izip_longest(actions, executed_actions): + if b is None and a is not None: + # common case is that we are executing every action + yield a + elif b is not None and a != b: + raise RuntimeError('Re-entrant failure - attempted ' + 'to resolve actions in a different ' + 'order from the active execution ' + 'path.') + else: + # resolved action is in the same location as before, + # so we are in good shape, but the action is already + # executed so we skip it + assert b is not None and a == b + + pending_actions = resume(resolveConflicts(all_actions)) + action = next(pending_actions, None) + while action is not None: callable = action['callable'] args = action['args'] kw = action['kw'] @@ -1088,10 +1132,25 @@ class ActionState(object): if introspector is not None: for introspectable in introspectables: introspectable.register(introspector, info) - + + executed_actions.append(action) + + # We cleared the actions list prior to execution so if there + # are some new actions then we add them to the mix and resolve + # conflicts again. This orders the new actions as well as + # ensures that the previously executed actions have no new + # conflicts. + if self.actions: + all_actions.extend(self.actions) + self.actions = [] + pending_actions = resume(resolveConflicts(all_actions)) + action = next(pending_actions, None) + finally: if clear: del self.actions[:] + else: + self.actions = all_actions # this function is licensed under the ZPL (stolen from Zope) def resolveConflicts(actions): -- cgit v1.2.3 From 5d2302a2b8d968245a123e54a8f01cd62c97cf69 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 15:57:08 -0600 Subject: izip_longest is not valid on py3 --- pyramid/compat.py | 4 ++++ pyramid/config/__init__.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index bfa345b88..301984749 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -244,3 +244,7 @@ else: def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None +if PY3: # pragma: no cover + from itertools import zip_longest +else: + from itertools import izip_longest as zip_longest diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 83683daeb..e907cbb14 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -23,6 +23,7 @@ from pyramid.compat import ( text_, reraise, string_types, + zip_longest, ) from pyramid.events import ApplicationCreated @@ -1089,7 +1090,7 @@ class ActionState(object): # re-entrant action because it scheduled the action *after* it # should have been executed (as defined by the action order) def resume(actions): - for a, b in itertools.izip_longest(actions, executed_actions): + for a, b in zip_longest(actions, executed_actions): if b is None and a is not None: # common case is that we are executing every action yield a -- cgit v1.2.3 From a52326b00b843b94b569d35a8d91a2a4c78b56a0 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 15:57:26 -0600 Subject: refactor loop to combine conflict resolution paths into one --- pyramid/config/__init__.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index e907cbb14..740c9c47d 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1080,9 +1080,9 @@ class ActionState(object): """ try: - all_actions = self.actions - self.actions = [] + all_actions = [] executed_actions = [] + pending_actions = iter([]) # resolve the new action list against what we have already # executed -- if a new action appears intertwined in the list @@ -1095,19 +1095,32 @@ class ActionState(object): # common case is that we are executing every action yield a elif b is not None and a != b: - raise RuntimeError('Re-entrant failure - attempted ' - 'to resolve actions in a different ' - 'order from the active execution ' - 'path.') + raise ConfigurationError( + 'Re-entrant failure - attempted to resolve ' + 'actions in a different order from the active ' + 'execution path.') else: # resolved action is in the same location as before, # so we are in good shape, but the action is already # executed so we skip it assert b is not None and a == b - pending_actions = resume(resolveConflicts(all_actions)) - action = next(pending_actions, None) - while action is not None: + while True: + # We clear the actions list prior to execution so if there + # are some new actions then we add them to the mix and resolve + # conflicts again. This orders the new actions as well as + # ensures that the previously executed actions have no new + # conflicts. + if self.actions: + all_actions.extend(self.actions) + self.actions = [] + pending_actions = resume(resolveConflicts(all_actions)) + + action = next(pending_actions, None) + if action is None: + # we are done! + break + callable = action['callable'] args = action['args'] kw = action['kw'] @@ -1128,7 +1141,7 @@ class ActionState(object): ConfigurationExecutionError(t, v, info), tb) finally: - del t, v, tb + del t, v, tb if introspector is not None: for introspectable in introspectables: @@ -1136,17 +1149,6 @@ class ActionState(object): executed_actions.append(action) - # We cleared the actions list prior to execution so if there - # are some new actions then we add them to the mix and resolve - # conflicts again. This orders the new actions as well as - # ensures that the previously executed actions have no new - # conflicts. - if self.actions: - all_actions.extend(self.actions) - self.actions = [] - pending_actions = resume(resolveConflicts(all_actions)) - action = next(pending_actions, None) - finally: if clear: del self.actions[:] -- cgit v1.2.3 From a1a5306b89bc652ad089551f0976a8b5f68d6b63 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 15:58:21 -0600 Subject: optimize the conflict resolution to occur against only executed actions --- pyramid/config/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 740c9c47d..1a9cc3f5a 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1112,9 +1112,20 @@ class ActionState(object): # ensures that the previously executed actions have no new # conflicts. if self.actions: + # Only resolve the new actions against executed_actions + # instead of everything to avoid redundant checks. + # Assume ``actions = resolveConflicts([A, B, C])`` which + # after conflict checks, resulted in ``actions == [A]`` + # then we know action A won out or a conflict would have + # been raised. Thus, when action D is added later, we only + # need to check the new action against A. + # ``actions = resolveConflicts([A, D]) should drop the + # number of redundant checks down from O(n^2) closer to + # O(n lg n). + pending_actions = resume(resolveConflicts( + executed_actions + self.actions)) all_actions.extend(self.actions) self.actions = [] - pending_actions = resume(resolveConflicts(all_actions)) action = next(pending_actions, None) if action is None: -- cgit v1.2.3 From d643c10413d49d5ec9c2bc0d6dc2dc4fb08c99c9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 16:05:20 -0600 Subject: improve error output a bit --- pyramid/config/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 1a9cc3f5a..0bd61bc39 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1096,9 +1096,10 @@ class ActionState(object): yield a elif b is not None and a != b: raise ConfigurationError( - 'Re-entrant failure - attempted to resolve ' - 'actions in a different order from the active ' - 'execution path.') + 'During execution a re-entrant action was added ' + 'that modified the planned execution order in a ' + 'way that is incompatible with what has already ' + 'been done.') else: # resolved action is in the same location as before, # so we are in good shape, but the action is already -- cgit v1.2.3 From cf6e03bf042483283c2a7a51fec29a6d73887965 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 26 Dec 2014 22:59:43 -0600 Subject: modify text --- pyramid/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 0bd61bc39..c35338826 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1099,7 +1099,7 @@ class ActionState(object): 'During execution a re-entrant action was added ' 'that modified the planned execution order in a ' 'way that is incompatible with what has already ' - 'been done.') + 'been executed.') else: # resolved action is in the same location as before, # so we are in good shape, but the action is already -- cgit v1.2.3 From 873fa0483a7bfeafa5590b6d992ac52228d1b509 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 27 Dec 2014 00:10:03 -0600 Subject: add reentrant tests --- pyramid/config/__init__.py | 11 ++++++---- pyramid/tests/test_config/test_init.py | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index c35338826..e81ccee3f 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1072,7 +1072,6 @@ class ActionState(object): ... output.append(('g', a, k)) >>> context.actions = [ ... (1, f, (1,)), - ... (2, f, (2,)), ... ] >>> context.execute_actions() >>> output @@ -1114,7 +1113,8 @@ class ActionState(object): # conflicts. if self.actions: # Only resolve the new actions against executed_actions - # instead of everything to avoid redundant checks. + # and pending_actions instead of everything to avoid + # redundant checks. # Assume ``actions = resolveConflicts([A, B, C])`` which # after conflict checks, resulted in ``actions == [A]`` # then we know action A won out or a conflict would have @@ -1123,9 +1123,12 @@ class ActionState(object): # ``actions = resolveConflicts([A, D]) should drop the # number of redundant checks down from O(n^2) closer to # O(n lg n). - pending_actions = resume(resolveConflicts( - executed_actions + self.actions)) all_actions.extend(self.actions) + pending_actions = resume(resolveConflicts( + executed_actions + + list(pending_actions) + + self.actions + )) self.actions = [] action = next(pending_actions, None) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 1e58e4d0f..40cc83885 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1503,6 +1503,45 @@ class TestActionState(unittest.TestCase): self.assertRaises(ConfigurationExecutionError, c.execute_actions) self.assertEqual(output, [('f', (1,), {}), ('f', (2,), {})]) + def test_reentrant_action(self): + output = [] + c = self._makeOne() + def f(*a, **k): + output.append(('f', a, k)) + c.actions.append((3, g, (8,), {})) + def g(*a, **k): + output.append(('g', a, k)) + c.actions = [ + (1, f, (1,)), + ] + c.execute_actions() + self.assertEqual(output, [('f', (1,), {}), ('g', (8,), {})]) + + def test_reentrant_action_error(self): + from pyramid.exceptions import ConfigurationError + c = self._makeOne() + def f(*a, **k): + c.actions.append((3, g, (8,), {}, (), None, -1)) + def g(*a, **k): pass + c.actions = [ + (1, f, (1,)), + ] + self.assertRaises(ConfigurationError, c.execute_actions) + + def test_reentrant_action_without_clear(self): + c = self._makeOne() + def f(*a, **k): + c.actions.append((3, g, (8,))) + def g(*a, **k): pass + c.actions = [ + (1, f, (1,)), + ] + c.execute_actions(clear=False) + self.assertEqual(c.actions, [ + (1, f, (1,)), + (3, g, (8,)), + ]) + class Test_resolveConflicts(unittest.TestCase): def _callFUT(self, actions): from pyramid.config import resolveConflicts -- cgit v1.2.3 From c569571bdb6e8c001ab0bc11777a2e0cca72d2fb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 27 Dec 2014 01:55:25 -0600 Subject: add action-order documentation --- docs/narr/extconfig.rst | 99 +++++++++++++++++++++++++++++++++++++++++++++- pyramid/config/__init__.py | 2 +- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index 6587aef92..c4d3e0250 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -215,13 +215,110 @@ registers an action with a higher order than the passed to it, that a route by this name was already registered by ``add_route``, and if such a route has not already been registered, it's a configuration error (a view that names a nonexistent route via its -``route_name`` parameter will never be called). +``route_name`` parameter will never be called). As of Pyramid 1.6 it is +possible for one action to invoke another. See :ref:`ordering_actions` for +more information. ``introspectables`` is a sequence of :term:`introspectable` objects. You can pass a sequence of introspectables to the :meth:`~pyramid.config.Configurator.action` method, which allows you to augment Pyramid's configuration introspection system. +.. _ordering_actions: + +Ordering Actions +---------------- + +In Pyramid every :term:`action` has an inherent ordering relative to other +actions. The logic within actions is deferred until a call to +:meth:`pyramid.config.Configurator.commit` (which is automatically invoked by +:meth:`pyramid.config.Configurator.make_wsgi_app`). This means you may call +``config.add_view(route_name='foo')`` **before** +``config.add_route('foo', '/foo')`` because nothing actually happens until +commit-time when conflicts are resolved, actions are ordered and executed. + +By default, almost every action in Pyramid has an ``order`` of ``0``. Every +action within the same order-level will be executed in the order it was called. +This means that if an action must be reliably executed before or after another +action, the ``order`` must be defined explicitly to make this work. For +example, views are dependent on routes being defined. Thus the action created +by :meth:`pyramid.config.Configurator.add_route` has an ``order`` of +:const:`pyramid.interfaces.PHASE2_CONFIG`. + +Pre-defined Phases +~~~~~~~~~~~~~~~~~~ + +:const:`pyramid.interfaces.PHASE1_CONFIG` + +- :meth:`pyramid.config.Configurator.add_renderer` +- :meth:`pyramid.config.Configurator.add_route_predicate` +- :meth:`pyramid.config.Configurator.add_subscriber_predicate` +- :meth:`pyramid.config.Configurator.add_view_predicate` +- :meth:`pyramid.config.Configurator.set_authorization_policy` +- :meth:`pyramid.config.Configurator.set_default_permission` +- :meth:`pyramid.config.Configurator.set_view_mapper` + +:const:`pyramid.interfaces.PHASE2_CONFIG` + +- :meth:`pyramid.config.Configurator.add_route` +- :meth:`pyramid.config.Configurator.set_authentication_policy` + +``0`` + +- The default for all builtin or custom directives unless otherwise specified. + +Calling Actions From Actions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.6 + +Pyramid's configurator allows actions to be added during a commit-cycle as +long as they are added to the current or a later ``order`` phase. This means +that your custom action can defer decisions until commit-time and then do +things like invoke :meth:`pyramid.config.Configurator.add_route`. It can also +provide better conflict detection if your addon needs to call more than one +other action. + +For example, let's make an addon that invokes ``add_route`` and ``add_view``, +but we want it to conflict with any other call to our addon: + +.. code-block:: python + :linenos: + + from pyramid.interfaces import PHASE1_CONFIG + + PHASE0_CONFIG = PHASE1_CONFIG - 10 + + def includeme(config): + config.add_directive(add_auto_route, 'add_auto_route') + + def add_auto_route(config, name, view): + def register(): + config.add_view(route_name=name, view=view) + config.add_route(name, '/' + name) + config.action(('auto route', name), register, order=PHASE0_CONFIG) + +Now someone else can use your addon and be informed if there is a conflict +between this route and another, or two calls to ``add_auto_route``. +Notice how we had to invoke our action **before** ``add_view`` or +``add_route``. If we tried to invoke this afterward, the subsequent calls to +``add_view`` and ``add_route`` would cause conflicts because that phase had +already been executed, and the configurator cannot go back in time to add more +views during that commit-cycle. + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + def main(global_config, **settings): + config = Configurator() + config.include('auto_route_addon') + config.add_auto_route('foo', my_view) + + def my_view(request): + return request.response + .. _introspection: Adding Configuration Introspection diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index e81ccee3f..a114cf039 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1075,7 +1075,7 @@ class ActionState(object): ... ] >>> context.execute_actions() >>> output - [('f', (1,), {}), ('f', (2,), {}), ('g', (8,), {})] + [('f', (1,), {}), ('g', (8,), {})] """ try: -- cgit v1.2.3 From d35a916095943b020f30acb90e878abe9bfd4fb1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 27 Dec 2014 01:58:59 -0600 Subject: update changelog --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 46c331268..b60600198 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,12 @@ Next release Features -------- +- The ``pyramid.config.Configurator`` has grown the ability to allow + actions to call other actions during a commit-cycle. This enables much more + logic to be placed into actions, such as the ability to invoke other actions + or group them for improved conflict detection. + See https://github.com/Pylons/pyramid/pull/1513 + - Added support / testing for 'pypy3' under Tox and Travis. See https://github.com/Pylons/pyramid/pull/1469 -- cgit v1.2.3 From 568a025d3156ee1e7bdf92e14c9eba7390c1dd26 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 18:58:53 -0600 Subject: expose public config phases in pyramid.config --- CHANGES.txt | 4 +++- docs/api/config.rst | 5 +++++ docs/narr/extconfig.rst | 17 ++++++++++------- pyramid/config/__init__.py | 23 ++++++++++++++++------- pyramid/interfaces.py | 3 ++- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1c82e5f27..f2bedbcc9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,7 +7,9 @@ Features - The ``pyramid.config.Configurator`` has grown the ability to allow actions to call other actions during a commit-cycle. This enables much more logic to be placed into actions, such as the ability to invoke other actions - or group them for improved conflict detection. + or group them for improved conflict detection. We have also exposed and + documented the config phases that Pyramid uses in order to further assist + in building conforming addons. See https://github.com/Pylons/pyramid/pull/1513 - Add ``pyramid.request.apply_request_extensions`` function which can be diff --git a/docs/api/config.rst b/docs/api/config.rst index 48dd2f0b9..ae913d32c 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -132,3 +132,8 @@ are being used. .. autoclass:: not_ + +.. attribute:: PHASE0_CONFIG +.. attribute:: PHASE1_CONFIG +.. attribute:: PHASE2_CONFIG +.. attribute:: PHASE3_CONFIG diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index c4d3e0250..c805f1572 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -243,12 +243,17 @@ This means that if an action must be reliably executed before or after another action, the ``order`` must be defined explicitly to make this work. For example, views are dependent on routes being defined. Thus the action created by :meth:`pyramid.config.Configurator.add_route` has an ``order`` of -:const:`pyramid.interfaces.PHASE2_CONFIG`. +:const:`pyramid.config.PHASE2_CONFIG`. Pre-defined Phases ~~~~~~~~~~~~~~~~~~ -:const:`pyramid.interfaces.PHASE1_CONFIG` +:const:`pyramid.config.PHASE0_CONFIG` + +- This phase is reserved for developers who want to execute actions prior + to Pyramid's core directives. + +:const:`pyramid.config.PHASE1_CONFIG` - :meth:`pyramid.config.Configurator.add_renderer` - :meth:`pyramid.config.Configurator.add_route_predicate` @@ -258,12 +263,12 @@ Pre-defined Phases - :meth:`pyramid.config.Configurator.set_default_permission` - :meth:`pyramid.config.Configurator.set_view_mapper` -:const:`pyramid.interfaces.PHASE2_CONFIG` +:const:`pyramid.config.PHASE2_CONFIG` - :meth:`pyramid.config.Configurator.add_route` - :meth:`pyramid.config.Configurator.set_authentication_policy` -``0`` +:const:`pyramid.config.PHASE3_CONFIG` - The default for all builtin or custom directives unless otherwise specified. @@ -285,9 +290,7 @@ but we want it to conflict with any other call to our addon: .. code-block:: python :linenos: - from pyramid.interfaces import PHASE1_CONFIG - - PHASE0_CONFIG = PHASE1_CONFIG - 10 + from pyramid.config import PHASE0_CONFIG def includeme(config): config.add_directive(add_auto_route, 'add_auto_route') diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index b5b5e841d..ea84aa1dc 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -12,7 +12,10 @@ from pyramid.interfaces import ( IDebugLogger, IExceptionResponse, IPredicateList, + PHASE0_CONFIG, PHASE1_CONFIG, + PHASE2_CONFIG, + PHASE3_CONFIG, ) from pyramid.asset import resolve_asset_spec @@ -55,7 +58,9 @@ from pyramid.settings import aslist from pyramid.threadlocal import manager from pyramid.util import ( + ActionInfo, WeakOrderedSet, + action_method, object_description, ) @@ -69,17 +74,18 @@ from pyramid.config.security import SecurityConfiguratorMixin from pyramid.config.settings import SettingsConfiguratorMixin from pyramid.config.testing import TestingConfiguratorMixin from pyramid.config.tweens import TweensConfiguratorMixin -from pyramid.config.util import PredicateList, not_ +from pyramid.config.util import ( + PredicateList, + not_, + PHASE1_CONFIG, + PHASE2_CONFIG, + PHASE3_CONFIG, +) from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin from pyramid.path import DottedNameResolver -from pyramid.util import ( - action_method, - ActionInfo, - ) - empty = text_('') _marker = object() @@ -87,6 +93,10 @@ ConfigurationError = ConfigurationError # pyflakes not_ = not_ # pyflakes, this is an API +PHASE0_CONFIG = PHASE0_CONFIG # api +PHASE1_CONFIG = PHASE1_CONFIG # api +PHASE2_CONFIG = PHASE2_CONFIG # api +PHASE3_CONFIG = PHASE3_CONFIG # api class Configurator( TestingConfiguratorMixin, @@ -1301,4 +1311,3 @@ def expand_action(discriminator, callable=None, args=(), kw=None, ) global_registries = WeakOrderedSet() - diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 1508f282e..4c171f9cc 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1228,6 +1228,7 @@ class ICacheBuster(Interface): # with this phase will be executed earlier than those with later phase # numbers. The default phase number is 0, FTR. +PHASE0_CONFIG = -30 PHASE1_CONFIG = -20 PHASE2_CONFIG = -10 - +PHASE3_CONFIG = 0 -- cgit v1.2.3 From c0063b33e3b570120aab09b7d0a0adcf31c8705c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:01:15 -0600 Subject: fix odd sentence --- docs/narr/extconfig.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index c805f1572..47f2fcb46 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -235,7 +235,8 @@ actions. The logic within actions is deferred until a call to :meth:`pyramid.config.Configurator.make_wsgi_app`). This means you may call ``config.add_view(route_name='foo')`` **before** ``config.add_route('foo', '/foo')`` because nothing actually happens until -commit-time when conflicts are resolved, actions are ordered and executed. +commit-time. During a commit cycle conflicts are resolved, actions are ordered +and executed. By default, almost every action in Pyramid has an ``order`` of ``0``. Every action within the same order-level will be executed in the order it was called. -- cgit v1.2.3 From bba15920ee77a626c2ea3636d9d3b4f8d571afa6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:02:14 -0600 Subject: avoid saying order=0, instead say PHASE3_CONFIG --- docs/narr/extconfig.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index 47f2fcb46..d17842bf2 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -238,8 +238,9 @@ actions. The logic within actions is deferred until a call to commit-time. During a commit cycle conflicts are resolved, actions are ordered and executed. -By default, almost every action in Pyramid has an ``order`` of ``0``. Every -action within the same order-level will be executed in the order it was called. +By default, almost every action in Pyramid has an ``order`` of +:const:`pyramid.config.PHASE3_CONFIG`. Every action within the same order-level +will be executed in the order it was called. This means that if an action must be reliably executed before or after another action, the ``order`` must be defined explicitly to make this work. For example, views are dependent on routes being defined. Thus the action created -- cgit v1.2.3 From 0bf2fded1a5dfa1614120c989f1d051908fa0b56 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:03:13 -0600 Subject: fix syntax --- docs/narr/extconfig.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index d17842bf2..a61eca7b7 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -295,7 +295,7 @@ but we want it to conflict with any other call to our addon: from pyramid.config import PHASE0_CONFIG def includeme(config): - config.add_directive(add_auto_route, 'add_auto_route') + config.add_directive('add_auto_route', add_auto_route) def add_auto_route(config, name, view): def register(): -- cgit v1.2.3 From a8fab3816726affaee2a8b91037372ba77cc1487 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 17 Feb 2015 20:15:11 -0500 Subject: add functest for config reentrancy --- pyramid/tests/test_config/test_init.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 2930734fa..4eb3f3385 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1554,6 +1554,35 @@ class TestActionState(unittest.TestCase): (3, g, (8,)), ]) +class Test_reentrant_action_functional(unittest.TestCase): + def _makeConfigurator(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_functional(self): + def add_auto_route(config, name, view): + def register(): + config.add_view(route_name=name, view=view) + config.add_route(name, '/' + name) + config.action( + ('auto route', name), register, order=-30 + ) + config = self._makeConfigurator() + config.add_directive('add_auto_route', add_auto_route) + def my_view(request): + return request.response + config.add_auto_route('foo', my_view) + config.commit() + from pyramid.interfaces import IRoutesMapper + mapper = config.registry.getUtility(IRoutesMapper) + routes = mapper.get_routes() + route = routes[0] + self.assertEqual(len(routes), 1) + self.assertEqual(route.name, 'foo') + self.assertEqual(route.path, '/foo') + + class Test_resolveConflicts(unittest.TestCase): def _callFUT(self, actions): from pyramid.config import resolveConflicts -- cgit v1.2.3 From bae121df8a31fa4303b68d9fcb71283293ad0c79 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:22:07 -0600 Subject: dammit, forgot to revert import --- pyramid/config/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index ea84aa1dc..401def208 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -74,13 +74,7 @@ from pyramid.config.security import SecurityConfiguratorMixin from pyramid.config.settings import SettingsConfiguratorMixin from pyramid.config.testing import TestingConfiguratorMixin from pyramid.config.tweens import TweensConfiguratorMixin -from pyramid.config.util import ( - PredicateList, - not_, - PHASE1_CONFIG, - PHASE2_CONFIG, - PHASE3_CONFIG, -) +from pyramid.config.util import PredicateList, not_ from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin -- cgit v1.2.3 From 4f28c2e2bd59c3fdbfc784d2ba8ef569bbe3b484 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 17 Feb 2015 20:28:38 -0500 Subject: appease coverage --- pyramid/tests/test_config/test_init.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 4eb3f3385..0ed04eb06 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1570,8 +1570,7 @@ class Test_reentrant_action_functional(unittest.TestCase): ) config = self._makeConfigurator() config.add_directive('add_auto_route', add_auto_route) - def my_view(request): - return request.response + def my_view(request): return request.response config.add_auto_route('foo', my_view) config.commit() from pyramid.interfaces import IRoutesMapper -- cgit v1.2.3