From 4149922e64aecf2a213f8efb120cd2d61fed3eb7 Mon Sep 17 00:00:00 2001
From: Michael Merickel <github@m.merickel.org>
Date: Fri, 26 Oct 2018 01:08:57 +0200
Subject: [PATCH] Merge pull request #3397 from mmerickel/refactor-actions

---
 src/pyramid/config/__init__.py |  571 +--------------------------------------------------------
 1 files changed, 11 insertions(+), 560 deletions(-)

diff --git a/src/pyramid/config/__init__.py b/src/pyramid/config/__init__.py
index f579035..00c3e6a 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
 
@@ -12,7 +9,6 @@
 from pyramid.interfaces import (
     IDebugLogger,
     IExceptionResponse,
-    IPredicateList,
     PHASE0_CONFIG,
     PHASE1_CONFIG,
     PHASE2_CONFIG,
@@ -23,21 +19,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,12 +39,15 @@
 
 from pyramid.util import WeakOrderedSet, object_description
 
-from pyramid.config.util import ActionInfo, PredicateList, action_method, not_
+from pyramid.config.actions import action_method, ActionState
+from pyramid.config.predicates import 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
 from pyramid.config.i18n import I18NConfiguratorMixin
+from pyramid.config.predicates import PredicateConfiguratorMixin
 from pyramid.config.rendering import RenderingConfiguratorMixin
 from pyramid.config.routes import RoutesConfiguratorMixin
 from pyramid.config.security import SecurityConfiguratorMixin
@@ -74,8 +69,12 @@
 PHASE2_CONFIG = PHASE2_CONFIG  # api
 PHASE3_CONFIG = PHASE3_CONFIG  # api
 
+ActionState = ActionState  # bw-compat for pyramid_zcml
+
 
 class Configurator(
+    ActionConfiguratorMixin,
+    PredicateConfiguratorMixin,
     TestingConfiguratorMixin,
     TweensConfiguratorMixin,
     SecurityConfiguratorMixin,
@@ -536,182 +535,6 @@
         _get_introspector, _set_introspector, _del_introspector
     )
 
-    def get_predlist(self, name):
-        predlist = self.registry.queryUtility(IPredicateList, name=name)
-        if predlist is None:
-            predlist = PredicateList()
-            self.registry.registerUtility(predlist, IPredicateList, name=name)
-        return predlist
-
-    def _add_predicate(
-        self, type, name, factory, weighs_more_than=None, weighs_less_than=None
-    ):
-        factory = self.maybe_dotted(factory)
-        discriminator = ('%s option' % type, name)
-        intr = self.introspectable(
-            '%s predicates' % type,
-            discriminator,
-            '%s predicate named %s' % (type, name),
-            '%s predicate' % type,
-        )
-        intr['name'] = name
-        intr['factory'] = factory
-        intr['weighs_more_than'] = weighs_more_than
-        intr['weighs_less_than'] = weighs_less_than
-
-        def register():
-            predlist = self.get_predlist(type)
-            predlist.add(
-                name,
-                factory,
-                weighs_more_than=weighs_more_than,
-                weighs_less_than=weighs_less_than,
-            )
-
-        self.action(
-            discriminator,
-            register,
-            introspectables=(intr,),
-            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 +905,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