From e4c0570d5c67ddf0ad9502169b59475ba0784d82 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Thu, 18 Oct 2018 08:27:04 +0200 Subject: [PATCH] move action-related code into pyramid.config.actions --- src/pyramid/config/__init__.py | 526 --------------------------------------------------------- 1 files changed, 6 insertions(+), 520 deletions(-) diff --git a/src/pyramid/config/__init__.py b/src/pyramid/config/__init__.py index f579035..e4dcc6b 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.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.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 @@ class Configurator( + ActionConfiguratorMixin, TestingConfiguratorMixin, TweensConfiguratorMixin, SecurityConfiguratorMixin, @@ -575,143 +570,6 @@ 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. @@ -1082,378 +940,6 @@ self.end() 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() -- Gitblit v1.9.3