summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2018-10-18 01:27:04 -0500
committerMichael Merickel <michael@merickel.org>2018-10-18 01:27:04 -0500
commite4c0570d5c67ddf0ad9502169b59475ba0784d82 (patch)
treea4378dfe09cf33c22fa320a11e73b320c63db1e2 /src
parent41f103af2745c336a3bcdc715e70ef3cb5d1e545 (diff)
downloadpyramid-e4c0570d5c67ddf0ad9502169b59475ba0784d82.tar.gz
pyramid-e4c0570d5c67ddf0ad9502169b59475ba0784d82.tar.bz2
pyramid-e4c0570d5c67ddf0ad9502169b59475ba0784d82.zip
move action-related code into pyramid.config.actions
Diffstat (limited to 'src')
-rw-r--r--src/pyramid/config/__init__.py526
-rw-r--r--src/pyramid/config/actions.py525
2 files changed, 531 insertions, 520 deletions
diff --git a/src/pyramid/config/__init__.py b/src/pyramid/config/__init__.py
index f5790352e..e4dcc6bb7 100644
--- a/src/pyramid/config/__init__.py
+++ b/src/pyramid/config/__init__.py
@@ -1,9 +1,6 @@
import inspect
-import itertools
import logging
-import operator
import os
-import sys
import threading
import venusian
@@ -23,21 +20,17 @@ from pyramid.asset import resolve_asset_spec
from pyramid.authorization import ACLAuthorizationPolicy
-from pyramid.compat import text_, reraise, string_types
+from pyramid.compat import text_, string_types
from pyramid.events import ApplicationCreated
-from pyramid.exceptions import (
- ConfigurationConflictError,
- ConfigurationError,
- ConfigurationExecutionError,
-)
+from pyramid.exceptions import ConfigurationError
from pyramid.httpexceptions import default_exceptionresponse_view
from pyramid.path import caller_package, package_of
-from pyramid.registry import Introspectable, Introspector, Registry, undefer
+from pyramid.registry import Introspectable, Introspector, Registry
from pyramid.router import Router
@@ -47,8 +40,9 @@ from pyramid.threadlocal import manager
from pyramid.util import WeakOrderedSet, object_description
-from pyramid.config.util import ActionInfo, PredicateList, action_method, not_
+from pyramid.config.util import PredicateList, action_method, not_
+from pyramid.config.actions import ActionConfiguratorMixin
from pyramid.config.adapters import AdaptersConfiguratorMixin
from pyramid.config.assets import AssetsConfiguratorMixin
from pyramid.config.factories import FactoriesConfiguratorMixin
@@ -76,6 +70,7 @@ PHASE3_CONFIG = PHASE3_CONFIG # api
class Configurator(
+ ActionConfiguratorMixin,
TestingConfiguratorMixin,
TweensConfiguratorMixin,
SecurityConfiguratorMixin,
@@ -575,143 +570,6 @@ class Configurator(
order=PHASE1_CONFIG,
) # must be registered early
- @property
- def action_info(self):
- info = self.info # usually a ZCML action (ParserInfo) if self.info
- if not info:
- # Try to provide more accurate info for conflict reports
- if self._ainfo:
- info = self._ainfo[0]
- else:
- info = ActionInfo(None, 0, '', '')
- return info
-
- def action(
- self,
- discriminator,
- callable=None,
- args=(),
- kw=None,
- order=0,
- introspectables=(),
- **extra
- ):
- """ Register an action which will be executed when
- :meth:`pyramid.config.Configurator.commit` is called (or executed
- immediately if ``autocommit`` is ``True``).
-
- .. warning:: This method is typically only used by :app:`Pyramid`
- framework extension authors, not by :app:`Pyramid` application
- developers.
-
- The ``discriminator`` uniquely identifies the action. It must be
- given, but it can be ``None``, to indicate that the action never
- conflicts. It must be a hashable value.
-
- The ``callable`` is a callable object which performs the task
- associated with the action when the action is executed. It is
- optional.
-
- ``args`` and ``kw`` are tuple and dict objects respectively, which
- are passed to ``callable`` when this action is executed. Both are
- optional.
-
- ``order`` is a grouping mechanism; an action with a lower order will
- be executed before an action with a higher order (has no effect when
- autocommit is ``True``).
-
- ``introspectables`` is a sequence of :term:`introspectable` objects
- (or the empty sequence if no introspectable objects are associated
- with this action). If this configurator's ``introspection``
- attribute is ``False``, these introspectables will be ignored.
-
- ``extra`` provides a facility for inserting extra keys and values
- into an action dictionary.
- """
- # catch nonhashable discriminators here; most unit tests use
- # autocommit=False, which won't catch unhashable discriminators
- assert hash(discriminator)
-
- if kw is None:
- kw = {}
-
- autocommit = self.autocommit
- action_info = self.action_info
-
- if not self.introspection:
- # if we're not introspecting, ignore any introspectables passed
- # to us
- introspectables = ()
-
- if autocommit:
- # callables can depend on the side effects of resolving a
- # deferred discriminator
- self.begin()
- try:
- undefer(discriminator)
- if callable is not None:
- callable(*args, **kw)
- for introspectable in introspectables:
- introspectable.register(self.introspector, action_info)
- finally:
- self.end()
-
- else:
- action = extra
- action.update(
- dict(
- discriminator=discriminator,
- callable=callable,
- args=args,
- kw=kw,
- order=order,
- info=action_info,
- includepath=self.includepath,
- introspectables=introspectables,
- )
- )
- self.action_state.action(**action)
-
- def _get_action_state(self):
- registry = self.registry
- try:
- state = registry.action_state
- except AttributeError:
- state = ActionState()
- registry.action_state = state
- return state
-
- def _set_action_state(self, state):
- self.registry.action_state = state
-
- action_state = property(_get_action_state, _set_action_state)
-
- _ctx = action_state # bw compat
-
- def commit(self):
- """
- Commit any pending configuration actions. If a configuration
- conflict is detected in the pending configuration actions, this method
- will raise a :exc:`ConfigurationConflictError`; within the traceback
- of this error will be information about the source of the conflict,
- usually including file names and line numbers of the cause of the
- configuration conflicts.
-
- .. warning::
- You should think very carefully before manually invoking
- ``commit()``. Especially not as part of any reusable configuration
- methods. Normally it should only be done by an application author at
- the end of configuration in order to override certain aspects of an
- addon.
-
- """
- self.begin()
- try:
- self.action_state.execute_actions(introspector=self.introspector)
- finally:
- self.end()
- self.action_state = ActionState() # old actions have been processed
-
def include(self, callable, route_prefix=None):
"""Include a configuration callable, to support imperative
application extensibility.
@@ -1084,376 +942,4 @@ class Configurator(
return app
-# this class is licensed under the ZPL (stolen from Zope)
-class ActionState(object):
- def __init__(self):
- # NB "actions" is an API, dep'd upon by pyramid_zcml's load_zcml func
- self.actions = []
- self._seen_files = set()
-
- def processSpec(self, spec):
- """Check whether a callable needs to be processed. The ``spec``
- refers to a unique identifier for the callable.
-
- Return True if processing is needed and False otherwise. If
- the callable needs to be processed, it will be marked as
- processed, assuming that the caller will procces the callable if
- it needs to be processed.
- """
- if spec in self._seen_files:
- return False
- self._seen_files.add(spec)
- return True
-
- def action(
- self,
- discriminator,
- callable=None,
- args=(),
- kw=None,
- order=0,
- includepath=(),
- info=None,
- introspectables=(),
- **extra
- ):
- """Add an action with the given discriminator, callable and arguments
- """
- if kw is None:
- kw = {}
- action = extra
- action.update(
- dict(
- discriminator=discriminator,
- callable=callable,
- args=args,
- kw=kw,
- includepath=includepath,
- info=info,
- order=order,
- introspectables=introspectables,
- )
- )
- self.actions.append(action)
-
- def execute_actions(self, clear=True, introspector=None):
- """Execute the configuration actions
-
- This calls the action callables after resolving conflicts
-
- For example:
-
- >>> output = []
- >>> def f(*a, **k):
- ... output.append(('f', a, k))
- >>> context = ActionState()
- >>> context.actions = [
- ... (1, f, (1,)),
- ... (1, f, (11,), {}, ('x', )),
- ... (2, f, (2,)),
- ... ]
- >>> context.execute_actions()
- >>> output
- [('f', (1,), {}), ('f', (2,), {})]
-
- If the action raises an error, we convert it to a
- ConfigurationExecutionError.
-
- >>> output = []
- >>> def bad():
- ... bad.xxx
- >>> context.actions = [
- ... (1, f, (1,)),
- ... (1, f, (11,), {}, ('x', )),
- ... (2, f, (2,)),
- ... (3, bad, (), {}, (), 'oops')
- ... ]
- >>> try:
- ... v = context.execute_actions()
- ... except ConfigurationExecutionError, v:
- ... pass
- >>> print(v)
- exceptions.AttributeError: 'function' object has no attribute 'xxx'
- in:
- oops
-
- Note that actions executed before the error still have an effect:
-
- >>> 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,)),
- ... ]
- >>> context.execute_actions()
- >>> output
- [('f', (1,), {}), ('g', (8,), {})]
-
- """
- try:
- all_actions = []
- executed_actions = []
- action_iter = iter([])
- conflict_state = ConflictResolverState()
-
- 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)
- action_iter = resolveConflicts(
- self.actions, state=conflict_state
- )
- self.actions = []
-
- action = next(action_iter, None)
- if action is None:
- # we are done!
- break
-
- callable = action['callable']
- args = action['args']
- kw = action['kw']
- info = action['info']
- # we use "get" below in case an action was added via a ZCML
- # directive that did not know about introspectables
- introspectables = action.get('introspectables', ())
-
- try:
- if callable is not None:
- callable(*args, **kw)
- except Exception:
- t, v, tb = sys.exc_info()
- try:
- reraise(
- ConfigurationExecutionError,
- ConfigurationExecutionError(t, v, info),
- tb,
- )
- finally:
- del t, v, tb
-
- if introspector is not None:
- for introspectable in introspectables:
- introspectable.register(introspector, info)
-
- executed_actions.append(action)
-
- self.actions = all_actions
- return executed_actions
-
- finally:
- if clear:
- self.actions = []
-
-
-class ConflictResolverState(object):
- def __init__(self):
- # keep a set of resolved discriminators to test against to ensure
- # that a new action does not conflict with something already executed
- self.resolved_ainfos = {}
-
- # actions left over from a previous iteration
- self.remaining_actions = []
-
- # after executing an action we memoize its order to avoid any new
- # actions sending us backward
- self.min_order = None
-
- # unique tracks the index of the action so we need it to increase
- # monotonically across invocations to resolveConflicts
- self.start = 0
-
-
-# this function is licensed under the ZPL (stolen from Zope)
-def resolveConflicts(actions, state=None):
- """Resolve conflicting actions
-
- Given an actions list, identify and try to resolve conflicting actions.
- Actions conflict if they have the same non-None discriminator.
-
- Conflicting actions can be resolved if the include path of one of
- the actions is a prefix of the includepaths of the other
- conflicting actions and is unequal to the include paths in the
- other conflicting actions.
-
- Actions are resolved on a per-order basis because some discriminators
- cannot be computed until earlier actions have executed. An action in an
- earlier order may execute successfully only to find out later that it was
- overridden by another action with a smaller include path. This will result
- in a conflict as there is no way to revert the original action.
-
- ``state`` may be an instance of ``ConflictResolverState`` that
- can be used to resume execution and resolve the new actions against the
- list of executed actions from a previous call.
-
- """
- if state is None:
- state = ConflictResolverState()
-
- # pick up where we left off last time, but track the new actions as well
- state.remaining_actions.extend(normalize_actions(actions))
- actions = state.remaining_actions
-
- def orderandpos(v):
- n, v = v
- return (v['order'] or 0, n)
-
- def orderonly(v):
- n, v = v
- return v['order'] or 0
-
- sactions = sorted(enumerate(actions, start=state.start), key=orderandpos)
- for order, actiongroup in itertools.groupby(sactions, orderonly):
- # "order" is an integer grouping. Actions in a lower order will be
- # executed before actions in a higher order. All of the actions in
- # one grouping will be executed (its callable, if any will be called)
- # before any of the actions in the next.
- output = []
- unique = {}
-
- # error out if we went backward in order
- if state.min_order is not None and order < state.min_order:
- r = [
- 'Actions were added to order={0} after execution had moved '
- 'on to order={1}. Conflicting actions: '.format(
- order, state.min_order
- )
- ]
- for i, action in actiongroup:
- for line in str(action['info']).rstrip().split('\n'):
- r.append(" " + line)
- raise ConfigurationError('\n'.join(r))
-
- for i, action in actiongroup:
- # Within an order, actions are executed sequentially based on
- # original action ordering ("i").
-
- # "ainfo" is a tuple of (i, action) where "i" is an integer
- # expressing the relative position of this action in the action
- # list being resolved, and "action" is an action dictionary. The
- # purpose of an ainfo is to associate an "i" with a particular
- # action; "i" exists for sorting after conflict resolution.
- ainfo = (i, action)
-
- # wait to defer discriminators until we are on their order because
- # the discriminator may depend on state from a previous order
- discriminator = undefer(action['discriminator'])
- action['discriminator'] = discriminator
-
- if discriminator is None:
- # The discriminator is None, so this action can never conflict.
- # We can add it directly to the result.
- output.append(ainfo)
- continue
-
- L = unique.setdefault(discriminator, [])
- L.append(ainfo)
-
- # Check for conflicts
- conflicts = {}
- for discriminator, ainfos in unique.items():
- # We use (includepath, i) as a sort key because we need to
- # sort the actions by the paths so that the shortest path with a
- # given prefix comes first. The "first" action is the one with the
- # shortest include path. We break sorting ties using "i".
- def bypath(ainfo):
- path, i = ainfo[1]['includepath'], ainfo[0]
- return path, order, i
-
- ainfos.sort(key=bypath)
- ainfo, rest = ainfos[0], ainfos[1:]
- _, action = ainfo
-
- # ensure this new action does not conflict with a previously
- # resolved action from an earlier order / invocation
- prev_ainfo = state.resolved_ainfos.get(discriminator)
- if prev_ainfo is not None:
- _, paction = prev_ainfo
- basepath, baseinfo = paction['includepath'], paction['info']
- includepath = action['includepath']
- # if the new action conflicts with the resolved action then
- # note the conflict, otherwise drop the action as it's
- # effectively overriden by the previous action
- if (
- includepath[: len(basepath)] != basepath
- or includepath == basepath
- ):
- L = conflicts.setdefault(discriminator, [baseinfo])
- L.append(action['info'])
-
- else:
- output.append(ainfo)
-
- basepath, baseinfo = action['includepath'], action['info']
- for _, action in rest:
- includepath = action['includepath']
- # Test whether path is a prefix of opath
- if (
- includepath[: len(basepath)] != basepath
- or includepath == basepath # not a prefix
- ):
- L = conflicts.setdefault(discriminator, [baseinfo])
- L.append(action['info'])
-
- if conflicts:
- raise ConfigurationConflictError(conflicts)
-
- # sort resolved actions by "i" and yield them one by one
- for i, action in sorted(output, key=operator.itemgetter(0)):
- # do not memoize the order until we resolve an action inside it
- state.min_order = action['order']
- state.start = i + 1
- state.remaining_actions.remove(action)
- state.resolved_ainfos[action['discriminator']] = (i, action)
- yield action
-
-
-def normalize_actions(actions):
- """Convert old-style tuple actions to new-style dicts."""
- result = []
- for v in actions:
- if not isinstance(v, dict):
- v = expand_action_tuple(*v)
- result.append(v)
- return result
-
-
-def expand_action_tuple(
- discriminator,
- callable=None,
- args=(),
- kw=None,
- includepath=(),
- info=None,
- order=0,
- introspectables=(),
-):
- if kw is None:
- kw = {}
- return dict(
- discriminator=discriminator,
- callable=callable,
- args=args,
- kw=kw,
- includepath=includepath,
- info=info,
- order=order,
- introspectables=introspectables,
- )
-
-
global_registries = WeakOrderedSet()
diff --git a/src/pyramid/config/actions.py b/src/pyramid/config/actions.py
new file mode 100644
index 000000000..353ed5edf
--- /dev/null
+++ b/src/pyramid/config/actions.py
@@ -0,0 +1,525 @@
+import itertools
+import operator
+import sys
+
+from pyramid.compat import reraise
+
+from pyramid.config.util import ActionInfo
+
+from pyramid.exceptions import (
+ ConfigurationConflictError,
+ ConfigurationError,
+ ConfigurationExecutionError,
+)
+
+from pyramid.registry import undefer
+
+
+class ActionConfiguratorMixin(object):
+ @property
+ def action_info(self):
+ info = self.info # usually a ZCML action (ParserInfo) if self.info
+ if not info:
+ # Try to provide more accurate info for conflict reports
+ if self._ainfo:
+ info = self._ainfo[0]
+ else:
+ info = ActionInfo(None, 0, '', '')
+ return info
+
+ def action(
+ self,
+ discriminator,
+ callable=None,
+ args=(),
+ kw=None,
+ order=0,
+ introspectables=(),
+ **extra
+ ):
+ """ Register an action which will be executed when
+ :meth:`pyramid.config.Configurator.commit` is called (or executed
+ immediately if ``autocommit`` is ``True``).
+
+ .. warning:: This method is typically only used by :app:`Pyramid`
+ framework extension authors, not by :app:`Pyramid` application
+ developers.
+
+ The ``discriminator`` uniquely identifies the action. It must be
+ given, but it can be ``None``, to indicate that the action never
+ conflicts. It must be a hashable value.
+
+ The ``callable`` is a callable object which performs the task
+ associated with the action when the action is executed. It is
+ optional.
+
+ ``args`` and ``kw`` are tuple and dict objects respectively, which
+ are passed to ``callable`` when this action is executed. Both are
+ optional.
+
+ ``order`` is a grouping mechanism; an action with a lower order will
+ be executed before an action with a higher order (has no effect when
+ autocommit is ``True``).
+
+ ``introspectables`` is a sequence of :term:`introspectable` objects
+ (or the empty sequence if no introspectable objects are associated
+ with this action). If this configurator's ``introspection``
+ attribute is ``False``, these introspectables will be ignored.
+
+ ``extra`` provides a facility for inserting extra keys and values
+ into an action dictionary.
+ """
+ # catch nonhashable discriminators here; most unit tests use
+ # autocommit=False, which won't catch unhashable discriminators
+ assert hash(discriminator)
+
+ if kw is None:
+ kw = {}
+
+ autocommit = self.autocommit
+ action_info = self.action_info
+
+ if not self.introspection:
+ # if we're not introspecting, ignore any introspectables passed
+ # to us
+ introspectables = ()
+
+ if autocommit:
+ # callables can depend on the side effects of resolving a
+ # deferred discriminator
+ self.begin()
+ try:
+ undefer(discriminator)
+ if callable is not None:
+ callable(*args, **kw)
+ for introspectable in introspectables:
+ introspectable.register(self.introspector, action_info)
+ finally:
+ self.end()
+
+ else:
+ action = extra
+ action.update(
+ dict(
+ discriminator=discriminator,
+ callable=callable,
+ args=args,
+ kw=kw,
+ order=order,
+ info=action_info,
+ includepath=self.includepath,
+ introspectables=introspectables,
+ )
+ )
+ self.action_state.action(**action)
+
+ def _get_action_state(self):
+ registry = self.registry
+ try:
+ state = registry.action_state
+ except AttributeError:
+ state = ActionState()
+ registry.action_state = state
+ return state
+
+ def _set_action_state(self, state):
+ self.registry.action_state = state
+
+ action_state = property(_get_action_state, _set_action_state)
+
+ _ctx = action_state # bw compat
+
+ def commit(self):
+ """
+ Commit any pending configuration actions. If a configuration
+ conflict is detected in the pending configuration actions, this method
+ will raise a :exc:`ConfigurationConflictError`; within the traceback
+ of this error will be information about the source of the conflict,
+ usually including file names and line numbers of the cause of the
+ configuration conflicts.
+
+ .. warning::
+ You should think very carefully before manually invoking
+ ``commit()``. Especially not as part of any reusable configuration
+ methods. Normally it should only be done by an application author at
+ the end of configuration in order to override certain aspects of an
+ addon.
+
+ """
+ self.begin()
+ try:
+ self.action_state.execute_actions(introspector=self.introspector)
+ finally:
+ self.end()
+ self.action_state = ActionState() # old actions have been processed
+
+# this class is licensed under the ZPL (stolen from Zope)
+class ActionState(object):
+ def __init__(self):
+ # NB "actions" is an API, dep'd upon by pyramid_zcml's load_zcml func
+ self.actions = []
+ self._seen_files = set()
+
+ def processSpec(self, spec):
+ """Check whether a callable needs to be processed. The ``spec``
+ refers to a unique identifier for the callable.
+
+ Return True if processing is needed and False otherwise. If
+ the callable needs to be processed, it will be marked as
+ processed, assuming that the caller will procces the callable if
+ it needs to be processed.
+ """
+ if spec in self._seen_files:
+ return False
+ self._seen_files.add(spec)
+ return True
+
+ def action(
+ self,
+ discriminator,
+ callable=None,
+ args=(),
+ kw=None,
+ order=0,
+ includepath=(),
+ info=None,
+ introspectables=(),
+ **extra
+ ):
+ """Add an action with the given discriminator, callable and arguments
+ """
+ if kw is None:
+ kw = {}
+ action = extra
+ action.update(
+ dict(
+ discriminator=discriminator,
+ callable=callable,
+ args=args,
+ kw=kw,
+ includepath=includepath,
+ info=info,
+ order=order,
+ introspectables=introspectables,
+ )
+ )
+ self.actions.append(action)
+
+ def execute_actions(self, clear=True, introspector=None):
+ """Execute the configuration actions
+
+ This calls the action callables after resolving conflicts
+
+ For example:
+
+ >>> output = []
+ >>> def f(*a, **k):
+ ... output.append(('f', a, k))
+ >>> context = ActionState()
+ >>> context.actions = [
+ ... (1, f, (1,)),
+ ... (1, f, (11,), {}, ('x', )),
+ ... (2, f, (2,)),
+ ... ]
+ >>> context.execute_actions()
+ >>> output
+ [('f', (1,), {}), ('f', (2,), {})]
+
+ If the action raises an error, we convert it to a
+ ConfigurationExecutionError.
+
+ >>> output = []
+ >>> def bad():
+ ... bad.xxx
+ >>> context.actions = [
+ ... (1, f, (1,)),
+ ... (1, f, (11,), {}, ('x', )),
+ ... (2, f, (2,)),
+ ... (3, bad, (), {}, (), 'oops')
+ ... ]
+ >>> try:
+ ... v = context.execute_actions()
+ ... except ConfigurationExecutionError, v:
+ ... pass
+ >>> print(v)
+ exceptions.AttributeError: 'function' object has no attribute 'xxx'
+ in:
+ oops
+
+ Note that actions executed before the error still have an effect:
+
+ >>> 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,)),
+ ... ]
+ >>> context.execute_actions()
+ >>> output
+ [('f', (1,), {}), ('g', (8,), {})]
+
+ """
+ try:
+ all_actions = []
+ executed_actions = []
+ action_iter = iter([])
+ conflict_state = ConflictResolverState()
+
+ 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)
+ action_iter = resolveConflicts(
+ self.actions, state=conflict_state
+ )
+ self.actions = []
+
+ action = next(action_iter, None)
+ if action is None:
+ # we are done!
+ break
+
+ callable = action['callable']
+ args = action['args']
+ kw = action['kw']
+ info = action['info']
+ # we use "get" below in case an action was added via a ZCML
+ # directive that did not know about introspectables
+ introspectables = action.get('introspectables', ())
+
+ try:
+ if callable is not None:
+ callable(*args, **kw)
+ except Exception:
+ t, v, tb = sys.exc_info()
+ try:
+ reraise(
+ ConfigurationExecutionError,
+ ConfigurationExecutionError(t, v, info),
+ tb,
+ )
+ finally:
+ del t, v, tb
+
+ if introspector is not None:
+ for introspectable in introspectables:
+ introspectable.register(introspector, info)
+
+ executed_actions.append(action)
+
+ self.actions = all_actions
+ return executed_actions
+
+ finally:
+ if clear:
+ self.actions = []
+
+
+class ConflictResolverState(object):
+ def __init__(self):
+ # keep a set of resolved discriminators to test against to ensure
+ # that a new action does not conflict with something already executed
+ self.resolved_ainfos = {}
+
+ # actions left over from a previous iteration
+ self.remaining_actions = []
+
+ # after executing an action we memoize its order to avoid any new
+ # actions sending us backward
+ self.min_order = None
+
+ # unique tracks the index of the action so we need it to increase
+ # monotonically across invocations to resolveConflicts
+ self.start = 0
+
+
+# this function is licensed under the ZPL (stolen from Zope)
+def resolveConflicts(actions, state=None):
+ """Resolve conflicting actions
+
+ Given an actions list, identify and try to resolve conflicting actions.
+ Actions conflict if they have the same non-None discriminator.
+
+ Conflicting actions can be resolved if the include path of one of
+ the actions is a prefix of the includepaths of the other
+ conflicting actions and is unequal to the include paths in the
+ other conflicting actions.
+
+ Actions are resolved on a per-order basis because some discriminators
+ cannot be computed until earlier actions have executed. An action in an
+ earlier order may execute successfully only to find out later that it was
+ overridden by another action with a smaller include path. This will result
+ in a conflict as there is no way to revert the original action.
+
+ ``state`` may be an instance of ``ConflictResolverState`` that
+ can be used to resume execution and resolve the new actions against the
+ list of executed actions from a previous call.
+
+ """
+ if state is None:
+ state = ConflictResolverState()
+
+ # pick up where we left off last time, but track the new actions as well
+ state.remaining_actions.extend(normalize_actions(actions))
+ actions = state.remaining_actions
+
+ def orderandpos(v):
+ n, v = v
+ return (v['order'] or 0, n)
+
+ def orderonly(v):
+ n, v = v
+ return v['order'] or 0
+
+ sactions = sorted(enumerate(actions, start=state.start), key=orderandpos)
+ for order, actiongroup in itertools.groupby(sactions, orderonly):
+ # "order" is an integer grouping. Actions in a lower order will be
+ # executed before actions in a higher order. All of the actions in
+ # one grouping will be executed (its callable, if any will be called)
+ # before any of the actions in the next.
+ output = []
+ unique = {}
+
+ # error out if we went backward in order
+ if state.min_order is not None and order < state.min_order:
+ r = [
+ 'Actions were added to order={0} after execution had moved '
+ 'on to order={1}. Conflicting actions: '.format(
+ order, state.min_order
+ )
+ ]
+ for i, action in actiongroup:
+ for line in str(action['info']).rstrip().split('\n'):
+ r.append(" " + line)
+ raise ConfigurationError('\n'.join(r))
+
+ for i, action in actiongroup:
+ # Within an order, actions are executed sequentially based on
+ # original action ordering ("i").
+
+ # "ainfo" is a tuple of (i, action) where "i" is an integer
+ # expressing the relative position of this action in the action
+ # list being resolved, and "action" is an action dictionary. The
+ # purpose of an ainfo is to associate an "i" with a particular
+ # action; "i" exists for sorting after conflict resolution.
+ ainfo = (i, action)
+
+ # wait to defer discriminators until we are on their order because
+ # the discriminator may depend on state from a previous order
+ discriminator = undefer(action['discriminator'])
+ action['discriminator'] = discriminator
+
+ if discriminator is None:
+ # The discriminator is None, so this action can never conflict.
+ # We can add it directly to the result.
+ output.append(ainfo)
+ continue
+
+ L = unique.setdefault(discriminator, [])
+ L.append(ainfo)
+
+ # Check for conflicts
+ conflicts = {}
+ for discriminator, ainfos in unique.items():
+ # We use (includepath, i) as a sort key because we need to
+ # sort the actions by the paths so that the shortest path with a
+ # given prefix comes first. The "first" action is the one with the
+ # shortest include path. We break sorting ties using "i".
+ def bypath(ainfo):
+ path, i = ainfo[1]['includepath'], ainfo[0]
+ return path, order, i
+
+ ainfos.sort(key=bypath)
+ ainfo, rest = ainfos[0], ainfos[1:]
+ _, action = ainfo
+
+ # ensure this new action does not conflict with a previously
+ # resolved action from an earlier order / invocation
+ prev_ainfo = state.resolved_ainfos.get(discriminator)
+ if prev_ainfo is not None:
+ _, paction = prev_ainfo
+ basepath, baseinfo = paction['includepath'], paction['info']
+ includepath = action['includepath']
+ # if the new action conflicts with the resolved action then
+ # note the conflict, otherwise drop the action as it's
+ # effectively overriden by the previous action
+ if (
+ includepath[: len(basepath)] != basepath
+ or includepath == basepath
+ ):
+ L = conflicts.setdefault(discriminator, [baseinfo])
+ L.append(action['info'])
+
+ else:
+ output.append(ainfo)
+
+ basepath, baseinfo = action['includepath'], action['info']
+ for _, action in rest:
+ includepath = action['includepath']
+ # Test whether path is a prefix of opath
+ if (
+ includepath[: len(basepath)] != basepath
+ or includepath == basepath # not a prefix
+ ):
+ L = conflicts.setdefault(discriminator, [baseinfo])
+ L.append(action['info'])
+
+ if conflicts:
+ raise ConfigurationConflictError(conflicts)
+
+ # sort resolved actions by "i" and yield them one by one
+ for i, action in sorted(output, key=operator.itemgetter(0)):
+ # do not memoize the order until we resolve an action inside it
+ state.min_order = action['order']
+ state.start = i + 1
+ state.remaining_actions.remove(action)
+ state.resolved_ainfos[action['discriminator']] = (i, action)
+ yield action
+
+
+def normalize_actions(actions):
+ """Convert old-style tuple actions to new-style dicts."""
+ result = []
+ for v in actions:
+ if not isinstance(v, dict):
+ v = expand_action_tuple(*v)
+ result.append(v)
+ return result
+
+
+def expand_action_tuple(
+ discriminator,
+ callable=None,
+ args=(),
+ kw=None,
+ includepath=(),
+ info=None,
+ order=0,
+ introspectables=(),
+):
+ if kw is None:
+ kw = {}
+ return dict(
+ discriminator=discriminator,
+ callable=callable,
+ args=args,
+ kw=kw,
+ includepath=includepath,
+ info=info,
+ order=order,
+ introspectables=introspectables,
+ )