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 --- tests/test_config/test_testing.py | 2 src/pyramid/config/__init__.py | 571 -------- tests/test_config/test_init.py | 1038 --------------- src/pyramid/config/tweens.py | 2 src/pyramid/config/adapters.py | 2 src/pyramid/config/rendering.py | 2 tests/test_config/test_views.py | 4 src/pyramid/config/testing.py | 2 tests/test_viewderivers.py | 2 src/pyramid/config/assets.py | 2 /dev/null | 276 ---- src/pyramid/config/actions.py | 581 ++++++++ src/pyramid/config/predicates.py | 257 +++ tests/test_config/test_predicates.py | 62 src/pyramid/config/views.py | 4 src/pyramid/config/i18n.py | 2 src/pyramid/config/routes.py | 10 src/pyramid/config/factories.py | 2 tests/test_config/test_actions.py | 1090 ++++++++++++++++ src/pyramid/config/security.py | 2 20 files changed, 1,962 insertions(+), 1,951 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() diff --git a/src/pyramid/config/actions.py b/src/pyramid/config/actions.py new file mode 100644 index 0000000..9c1227d --- /dev/null +++ b/src/pyramid/config/actions.py @@ -0,0 +1,581 @@ +import functools +import itertools +import operator +import sys +import traceback +from zope.interface import implementer + +from pyramid.compat import reraise +from pyramid.exceptions import ( + ConfigurationConflictError, + ConfigurationError, + ConfigurationExecutionError, +) +from pyramid.interfaces import IActionInfo +from pyramid.registry import undefer +from pyramid.util import is_nonstr_iter + + +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, + ) + + +@implementer(IActionInfo) +class ActionInfo(object): + def __init__(self, file, line, function, src): + self.file = file + self.line = line + self.function = function + self.src = src + + def __str__(self): + srclines = self.src.split('\n') + src = '\n'.join(' %s' % x for x in srclines) + return 'Line %s of file %s:\n%s' % (self.line, self.file, src) + + +def action_method(wrapped): + """ Wrapper to provide the right conflict info report data when a method + that calls Configurator.action calls another that does the same. Not a + documented API but used by some external systems.""" + + def wrapper(self, *arg, **kw): + if self._ainfo is None: + self._ainfo = [] + info = kw.pop('_info', None) + # backframes for outer decorators to actionmethods + backframes = kw.pop('_backframes', 0) + 2 + if is_nonstr_iter(info) and len(info) == 4: + # _info permitted as extract_stack tuple + info = ActionInfo(*info) + if info is None: + try: + f = traceback.extract_stack(limit=4) + + # Work around a Python 3.5 issue whereby it would insert an + # extra stack frame. This should no longer be necessary in + # Python 3.5.1 + last_frame = ActionInfo(*f[-1]) + if last_frame.function == 'extract_stack': # pragma: no cover + f.pop() + info = ActionInfo(*f[-backframes]) + except Exception: # pragma: no cover + info = ActionInfo(None, 0, '', '') + self._ainfo.append(info) + try: + result = wrapped(self, *arg, **kw) + finally: + self._ainfo.pop() + return result + + if hasattr(wrapped, '__name__'): + functools.update_wrapper(wrapper, wrapped) + wrapper.__docobj__ = wrapped + return wrapper diff --git a/src/pyramid/config/adapters.py b/src/pyramid/config/adapters.py index e5668c4..54c239a 100644 --- a/src/pyramid/config/adapters.py +++ b/src/pyramid/config/adapters.py @@ -8,7 +8,7 @@ from pyramid.util import takes_one_arg -from pyramid.config.util import action_method +from pyramid.config.actions import action_method class AdaptersConfiguratorMixin(object): diff --git a/src/pyramid/config/assets.py b/src/pyramid/config/assets.py index fd8b2ee..e505fd2 100644 --- a/src/pyramid/config/assets.py +++ b/src/pyramid/config/assets.py @@ -9,7 +9,7 @@ from pyramid.exceptions import ConfigurationError from pyramid.threadlocal import get_current_registry -from pyramid.config.util import action_method +from pyramid.config.actions import action_method class OverrideProvider(pkg_resources.DefaultProvider): diff --git a/src/pyramid/config/factories.py b/src/pyramid/config/factories.py index 2ec1558..1621198 100644 --- a/src/pyramid/config/factories.py +++ b/src/pyramid/config/factories.py @@ -15,7 +15,7 @@ from pyramid.util import get_callable_name, InstancePropertyHelper -from pyramid.config.util import action_method +from pyramid.config.actions import action_method class FactoriesConfiguratorMixin(object): diff --git a/src/pyramid/config/i18n.py b/src/pyramid/config/i18n.py index 6e73344..92c324f 100644 --- a/src/pyramid/config/i18n.py +++ b/src/pyramid/config/i18n.py @@ -3,7 +3,7 @@ from pyramid.exceptions import ConfigurationError from pyramid.path import AssetResolver -from pyramid.config.util import action_method +from pyramid.config.actions import action_method class I18NConfiguratorMixin(object): diff --git a/src/pyramid/config/predicates.py b/src/pyramid/config/predicates.py index cdbf68c..8f16f74 100644 --- a/src/pyramid/config/predicates.py +++ b/src/pyramid/config/predicates.py @@ -1,3 +1,256 @@ -import zope.deprecation +from hashlib import md5 +from webob.acceptparse import Accept -zope.deprecation.moved('pyramid.predicates', 'Pyramid 1.10') +from pyramid.compat import bytes_, is_nonstr_iter +from pyramid.exceptions import ConfigurationError +from pyramid.interfaces import IPredicateList, PHASE1_CONFIG +from pyramid.predicates import Notted +from pyramid.registry import predvalseq +from pyramid.util import TopologicalSorter + + +MAX_ORDER = 1 << 30 +DEFAULT_PHASH = md5().hexdigest() + + +class PredicateConfiguratorMixin(object): + 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 + + +class not_(object): + """ + + You can invert the meaning of any predicate value by wrapping it in a call + to :class:`pyramid.config.not_`. + + .. code-block:: python + :linenos: + + from pyramid.config import not_ + + config.add_view( + 'mypackage.views.my_view', + route_name='ok', + request_method=not_('POST') + ) + + The above example will ensure that the view is called if the request method + is *not* ``POST``, at least if no other view is more specific. + + This technique of wrapping a predicate value in ``not_`` can be used + anywhere predicate values are accepted: + + - :meth:`pyramid.config.Configurator.add_view` + + - :meth:`pyramid.config.Configurator.add_route` + + - :meth:`pyramid.config.Configurator.add_subscriber` + + - :meth:`pyramid.view.view_config` + + - :meth:`pyramid.events.subscriber` + + .. versionadded:: 1.5 + """ + + def __init__(self, value): + self.value = value + + +# under = after +# over = before + + +class PredicateList(object): + def __init__(self): + self.sorter = TopologicalSorter() + self.last_added = None + + def add(self, name, factory, weighs_more_than=None, weighs_less_than=None): + # Predicates should be added to a predicate list in (presumed) + # computation expense order. + # if weighs_more_than is None and weighs_less_than is None: + # weighs_more_than = self.last_added or FIRST + # weighs_less_than = LAST + self.last_added = name + self.sorter.add( + name, factory, after=weighs_more_than, before=weighs_less_than + ) + + def names(self): + # Return the list of valid predicate names. + return self.sorter.names + + def make(self, config, **kw): + # Given a configurator and a list of keywords, a predicate list is + # computed. Elsewhere in the code, we evaluate predicates using a + # generator expression. All predicates associated with a view or + # route must evaluate true for the view or route to "match" during a + # request. The fastest predicate should be evaluated first, then the + # next fastest, and so on, as if one returns false, the remainder of + # the predicates won't need to be evaluated. + # + # While we compute predicates, we also compute a predicate hash (aka + # phash) that can be used by a caller to identify identical predicate + # lists. + ordered = self.sorter.sorted() + phash = md5() + weights = [] + preds = [] + for n, (name, predicate_factory) in enumerate(ordered): + vals = kw.pop(name, None) + if vals is None: # XXX should this be a sentinel other than None? + continue + if not isinstance(vals, predvalseq): + vals = (vals,) + for val in vals: + realval = val + notted = False + if isinstance(val, not_): + realval = val.value + notted = True + pred = predicate_factory(realval, config) + if notted: + pred = Notted(pred) + hashes = pred.phash() + if not is_nonstr_iter(hashes): + hashes = [hashes] + for h in hashes: + phash.update(bytes_(h)) + weights.append(1 << n + 1) + preds.append(pred) + if kw: + from difflib import get_close_matches + + closest = [] + names = [name for name, _ in ordered] + for name in kw: + closest.extend(get_close_matches(name, names, 3)) + + raise ConfigurationError( + 'Unknown predicate values: %r (did you mean %s)' + % (kw, ','.join(closest)) + ) + # A "order" is computed for the predicate list. An order is + # a scoring. + # + # Each predicate is associated with a weight value. The weight of a + # predicate symbolizes the relative potential "importance" of the + # predicate to all other predicates. A larger weight indicates + # greater importance. + # + # All weights for a given predicate list are bitwise ORed together + # to create a "score"; this score is then subtracted from + # MAX_ORDER and divided by an integer representing the number of + # predicates+1 to determine the order. + # + # For views, the order represents the ordering in which a "multiview" + # ( a collection of views that share the same context/request/name + # triad but differ in other ways via predicates) will attempt to call + # its set of views. Views with lower orders will be tried first. + # The intent is to a) ensure that views with more predicates are + # always evaluated before views with fewer predicates and b) to + # ensure a stable call ordering of views that share the same number + # of predicates. Views which do not have any predicates get an order + # of MAX_ORDER, meaning that they will be tried very last. + score = 0 + for bit in weights: + score = score | bit + order = (MAX_ORDER - score) / (len(preds) + 1) + return order, preds, phash.hexdigest() + + +def normalize_accept_offer(offer, allow_range=False): + if allow_range and '*' in offer: + return offer.lower() + return str(Accept.parse_offer(offer)) + + +def sort_accept_offers(offers, order=None): + """ + Sort a list of offers by preference. + + For a given ``type/subtype`` category of offers, this algorithm will + always sort offers with params higher than the bare offer. + + :param offers: A list of offers to be sorted. + :param order: A weighted list of offers where items closer to the start of + the list will be a preferred over items closer to the end. + :return: A list of offers sorted first by specificity (higher to lower) + then by ``order``. + + """ + if order is None: + order = [] + + max_weight = len(offers) + + def find_order_index(value, default=None): + return next((i for i, x in enumerate(order) if x == value), default) + + def offer_sort_key(value): + """ + (type_weight, params_weight) + + type_weight: + - index of specific ``type/subtype`` in order list + - ``max_weight * 2`` if no match is found + + params_weight: + - index of specific ``type/subtype;params`` in order list + - ``max_weight`` if not found + - ``max_weight + 1`` if no params at all + + """ + parsed = Accept.parse_offer(value) + + type_w = find_order_index( + parsed.type + '/' + parsed.subtype, max_weight + ) + + if parsed.params: + param_w = find_order_index(value, max_weight) + + else: + param_w = max_weight + 1 + + return (type_w, param_w) + + return sorted(offers, key=offer_sort_key) diff --git a/src/pyramid/config/rendering.py b/src/pyramid/config/rendering.py index 9481996..7e5b767 100644 --- a/src/pyramid/config/rendering.py +++ b/src/pyramid/config/rendering.py @@ -1,7 +1,7 @@ from pyramid.interfaces import IRendererFactory, PHASE1_CONFIG from pyramid import renderers -from pyramid.config.util import action_method +from pyramid.config.actions import action_method DEFAULT_RENDERERS = ( ('json', renderers.json_renderer_factory), diff --git a/src/pyramid/config/routes.py b/src/pyramid/config/routes.py index 7a76e9e..a146623 100644 --- a/src/pyramid/config/routes.py +++ b/src/pyramid/config/routes.py @@ -10,18 +10,14 @@ ) from pyramid.exceptions import ConfigurationError +import pyramid.predicates from pyramid.request import route_request_iface from pyramid.urldispatch import RoutesMapper from pyramid.util import as_sorted_tuple, is_nonstr_iter -import pyramid.predicates - -from pyramid.config.util import ( - action_method, - normalize_accept_offer, - predvalseq, -) +from pyramid.config.actions import action_method +from pyramid.config.predicates import normalize_accept_offer, predvalseq class RoutesConfiguratorMixin(object): diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py index 3b55c41..08e7cb8 100644 --- a/src/pyramid/config/security.py +++ b/src/pyramid/config/security.py @@ -14,7 +14,7 @@ from pyramid.exceptions import ConfigurationError from pyramid.util import as_sorted_tuple -from pyramid.config.util import action_method +from pyramid.config.actions import action_method class SecurityConfiguratorMixin(object): diff --git a/src/pyramid/config/testing.py b/src/pyramid/config/testing.py index 1655df5..bba5054 100644 --- a/src/pyramid/config/testing.py +++ b/src/pyramid/config/testing.py @@ -11,7 +11,7 @@ from pyramid.traversal import decode_path_info, split_path_info -from pyramid.config.util import action_method +from pyramid.config.actions import action_method class TestingConfiguratorMixin(object): diff --git a/src/pyramid/config/tweens.py b/src/pyramid/config/tweens.py index b74a57a..7fc786a 100644 --- a/src/pyramid/config/tweens.py +++ b/src/pyramid/config/tweens.py @@ -10,7 +10,7 @@ from pyramid.util import is_string_or_iterable, TopologicalSorter -from pyramid.config.util import action_method +from pyramid.config.actions import action_method class TweensConfiguratorMixin(object): diff --git a/src/pyramid/config/util.py b/src/pyramid/config/util.py deleted file mode 100644 index 8723b77..0000000 --- a/src/pyramid/config/util.py +++ /dev/null @@ -1,276 +0,0 @@ -import functools -from hashlib import md5 -import traceback -from webob.acceptparse import Accept -from zope.interface import implementer - -from pyramid.compat import bytes_, is_nonstr_iter -from pyramid.interfaces import IActionInfo - -from pyramid.exceptions import ConfigurationError -from pyramid.predicates import Notted -from pyramid.registry import predvalseq -from pyramid.util import TopologicalSorter, takes_one_arg - -TopologicalSorter = TopologicalSorter # support bw-compat imports -takes_one_arg = takes_one_arg # support bw-compat imports - - -@implementer(IActionInfo) -class ActionInfo(object): - def __init__(self, file, line, function, src): - self.file = file - self.line = line - self.function = function - self.src = src - - def __str__(self): - srclines = self.src.split('\n') - src = '\n'.join(' %s' % x for x in srclines) - return 'Line %s of file %s:\n%s' % (self.line, self.file, src) - - -def action_method(wrapped): - """ Wrapper to provide the right conflict info report data when a method - that calls Configurator.action calls another that does the same. Not a - documented API but used by some external systems.""" - - def wrapper(self, *arg, **kw): - if self._ainfo is None: - self._ainfo = [] - info = kw.pop('_info', None) - # backframes for outer decorators to actionmethods - backframes = kw.pop('_backframes', 0) + 2 - if is_nonstr_iter(info) and len(info) == 4: - # _info permitted as extract_stack tuple - info = ActionInfo(*info) - if info is None: - try: - f = traceback.extract_stack(limit=4) - - # Work around a Python 3.5 issue whereby it would insert an - # extra stack frame. This should no longer be necessary in - # Python 3.5.1 - last_frame = ActionInfo(*f[-1]) - if last_frame.function == 'extract_stack': # pragma: no cover - f.pop() - info = ActionInfo(*f[-backframes]) - except Exception: # pragma: no cover - info = ActionInfo(None, 0, '', '') - self._ainfo.append(info) - try: - result = wrapped(self, *arg, **kw) - finally: - self._ainfo.pop() - return result - - if hasattr(wrapped, '__name__'): - functools.update_wrapper(wrapper, wrapped) - wrapper.__docobj__ = wrapped - return wrapper - - -MAX_ORDER = 1 << 30 -DEFAULT_PHASH = md5().hexdigest() - - -class not_(object): - """ - - You can invert the meaning of any predicate value by wrapping it in a call - to :class:`pyramid.config.not_`. - - .. code-block:: python - :linenos: - - from pyramid.config import not_ - - config.add_view( - 'mypackage.views.my_view', - route_name='ok', - request_method=not_('POST') - ) - - The above example will ensure that the view is called if the request method - is *not* ``POST``, at least if no other view is more specific. - - This technique of wrapping a predicate value in ``not_`` can be used - anywhere predicate values are accepted: - - - :meth:`pyramid.config.Configurator.add_view` - - - :meth:`pyramid.config.Configurator.add_route` - - - :meth:`pyramid.config.Configurator.add_subscriber` - - - :meth:`pyramid.view.view_config` - - - :meth:`pyramid.events.subscriber` - - .. versionadded:: 1.5 - """ - - def __init__(self, value): - self.value = value - - -# under = after -# over = before - - -class PredicateList(object): - def __init__(self): - self.sorter = TopologicalSorter() - self.last_added = None - - def add(self, name, factory, weighs_more_than=None, weighs_less_than=None): - # Predicates should be added to a predicate list in (presumed) - # computation expense order. - # if weighs_more_than is None and weighs_less_than is None: - # weighs_more_than = self.last_added or FIRST - # weighs_less_than = LAST - self.last_added = name - self.sorter.add( - name, factory, after=weighs_more_than, before=weighs_less_than - ) - - def names(self): - # Return the list of valid predicate names. - return self.sorter.names - - def make(self, config, **kw): - # Given a configurator and a list of keywords, a predicate list is - # computed. Elsewhere in the code, we evaluate predicates using a - # generator expression. All predicates associated with a view or - # route must evaluate true for the view or route to "match" during a - # request. The fastest predicate should be evaluated first, then the - # next fastest, and so on, as if one returns false, the remainder of - # the predicates won't need to be evaluated. - # - # While we compute predicates, we also compute a predicate hash (aka - # phash) that can be used by a caller to identify identical predicate - # lists. - ordered = self.sorter.sorted() - phash = md5() - weights = [] - preds = [] - for n, (name, predicate_factory) in enumerate(ordered): - vals = kw.pop(name, None) - if vals is None: # XXX should this be a sentinel other than None? - continue - if not isinstance(vals, predvalseq): - vals = (vals,) - for val in vals: - realval = val - notted = False - if isinstance(val, not_): - realval = val.value - notted = True - pred = predicate_factory(realval, config) - if notted: - pred = Notted(pred) - hashes = pred.phash() - if not is_nonstr_iter(hashes): - hashes = [hashes] - for h in hashes: - phash.update(bytes_(h)) - weights.append(1 << n + 1) - preds.append(pred) - if kw: - from difflib import get_close_matches - - closest = [] - names = [name for name, _ in ordered] - for name in kw: - closest.extend(get_close_matches(name, names, 3)) - - raise ConfigurationError( - 'Unknown predicate values: %r (did you mean %s)' - % (kw, ','.join(closest)) - ) - # A "order" is computed for the predicate list. An order is - # a scoring. - # - # Each predicate is associated with a weight value. The weight of a - # predicate symbolizes the relative potential "importance" of the - # predicate to all other predicates. A larger weight indicates - # greater importance. - # - # All weights for a given predicate list are bitwise ORed together - # to create a "score"; this score is then subtracted from - # MAX_ORDER and divided by an integer representing the number of - # predicates+1 to determine the order. - # - # For views, the order represents the ordering in which a "multiview" - # ( a collection of views that share the same context/request/name - # triad but differ in other ways via predicates) will attempt to call - # its set of views. Views with lower orders will be tried first. - # The intent is to a) ensure that views with more predicates are - # always evaluated before views with fewer predicates and b) to - # ensure a stable call ordering of views that share the same number - # of predicates. Views which do not have any predicates get an order - # of MAX_ORDER, meaning that they will be tried very last. - score = 0 - for bit in weights: - score = score | bit - order = (MAX_ORDER - score) / (len(preds) + 1) - return order, preds, phash.hexdigest() - - -def normalize_accept_offer(offer, allow_range=False): - if allow_range and '*' in offer: - return offer.lower() - return str(Accept.parse_offer(offer)) - - -def sort_accept_offers(offers, order=None): - """ - Sort a list of offers by preference. - - For a given ``type/subtype`` category of offers, this algorithm will - always sort offers with params higher than the bare offer. - - :param offers: A list of offers to be sorted. - :param order: A weighted list of offers where items closer to the start of - the list will be a preferred over items closer to the end. - :return: A list of offers sorted first by specificity (higher to lower) - then by ``order``. - - """ - if order is None: - order = [] - - max_weight = len(offers) - - def find_order_index(value, default=None): - return next((i for i, x in enumerate(order) if x == value), default) - - def offer_sort_key(value): - """ - (type_weight, params_weight) - - type_weight: - - index of specific ``type/subtype`` in order list - - ``max_weight * 2`` if no match is found - - params_weight: - - index of specific ``type/subtype;params`` in order list - - ``max_weight`` if not found - - ``max_weight + 1`` if no params at all - - """ - parsed = Accept.parse_offer(value) - - type_w = find_order_index( - parsed.type + '/' + parsed.subtype, max_weight - ) - - if parsed.params: - param_w = find_order_index(value, max_weight) - - else: - param_w = max_weight + 1 - - return (type_w, param_w) - - return sorted(offers, key=offer_sort_key) diff --git a/src/pyramid/config/views.py b/src/pyramid/config/views.py index cc5b48e..bd1b693 100644 --- a/src/pyramid/config/views.py +++ b/src/pyramid/config/views.py @@ -74,8 +74,8 @@ wraps_view, ) -from pyramid.config.util import ( - action_method, +from pyramid.config.actions import action_method +from pyramid.config.predicates import ( DEFAULT_PHASH, MAX_ORDER, normalize_accept_offer, diff --git a/tests/test_config/test_actions.py b/tests/test_config/test_actions.py new file mode 100644 index 0000000..a72d0d7 --- /dev/null +++ b/tests/test_config/test_actions.py @@ -0,0 +1,1090 @@ +import unittest + +from pyramid.exceptions import ConfigurationConflictError +from pyramid.exceptions import ConfigurationExecutionError + +from pyramid.interfaces import IRequest + + +class ActionConfiguratorMixinTests(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + + config = Configurator(*arg, **kw) + return config + + def _getViewCallable( + self, + config, + ctx_iface=None, + request_iface=None, + name='', + exception_view=False, + ): + from zope.interface import Interface + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + + if exception_view: # pragma: no cover + classifier = IExceptionViewClassifier + else: + classifier = IViewClassifier + if ctx_iface is None: + ctx_iface = Interface + if request_iface is None: + request_iface = IRequest + return config.registry.adapters.lookup( + (classifier, request_iface, ctx_iface), + IView, + name=name, + default=None, + ) + + def test_action_branching_kw_is_None(self): + config = self._makeOne(autocommit=True) + self.assertEqual(config.action('discrim'), None) + + def test_action_branching_kw_is_not_None(self): + config = self._makeOne(autocommit=True) + self.assertEqual(config.action('discrim', kw={'a': 1}), None) + + def test_action_autocommit_with_introspectables(self): + from pyramid.config.actions import ActionInfo + + config = self._makeOne(autocommit=True) + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual(len(intr.registered), 1) + self.assertEqual(intr.registered[0][0], config.introspector) + self.assertEqual(intr.registered[0][1].__class__, ActionInfo) + + def test_action_autocommit_with_introspectables_introspection_off(self): + config = self._makeOne(autocommit=True) + config.introspection = False + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual(len(intr.registered), 0) + + def test_action_branching_nonautocommit_with_config_info(self): + config = self._makeOne(autocommit=False) + config.info = 'abc' + state = DummyActionState() + state.autocommit = False + config.action_state = state + config.action('discrim', kw={'a': 1}) + self.assertEqual( + state.actions, + [ + ( + (), + { + 'args': (), + 'callable': None, + 'discriminator': 'discrim', + 'includepath': (), + 'info': 'abc', + 'introspectables': (), + 'kw': {'a': 1}, + 'order': 0, + }, + ) + ], + ) + + def test_action_branching_nonautocommit_without_config_info(self): + config = self._makeOne(autocommit=False) + config.info = '' + config._ainfo = ['z'] + state = DummyActionState() + config.action_state = state + state.autocommit = False + config.action('discrim', kw={'a': 1}) + self.assertEqual( + state.actions, + [ + ( + (), + { + 'args': (), + 'callable': None, + 'discriminator': 'discrim', + 'includepath': (), + 'info': 'z', + 'introspectables': (), + 'kw': {'a': 1}, + 'order': 0, + }, + ) + ], + ) + + def test_action_branching_nonautocommit_with_introspectables(self): + config = self._makeOne(autocommit=False) + config.info = '' + config._ainfo = [] + state = DummyActionState() + config.action_state = state + state.autocommit = False + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual(state.actions[0][1]['introspectables'], (intr,)) + + def test_action_nonautocommit_with_introspectables_introspection_off(self): + config = self._makeOne(autocommit=False) + config.info = '' + config._ainfo = [] + config.introspection = False + state = DummyActionState() + config.action_state = state + state.autocommit = False + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual(state.actions[0][1]['introspectables'], ()) + + def test_commit_conflict_simple(self): + config = self._makeOne() + + def view1(request): # pragma: no cover + pass + + def view2(request): # pragma: no cover + pass + + config.add_view(view1) + config.add_view(view2) + self.assertRaises(ConfigurationConflictError, config.commit) + + def test_commit_conflict_resolved_with_include(self): + config = self._makeOne() + + def view1(request): # pragma: no cover + pass + + def view2(request): # pragma: no cover + pass + + def includeme(config): + config.add_view(view2) + + config.add_view(view1) + config.include(includeme) + config.commit() + registeredview = self._getViewCallable(config) + self.assertEqual(registeredview.__name__, 'view1') + + def test_commit_conflict_with_two_includes(self): + config = self._makeOne() + + def view1(request): # pragma: no cover + pass + + def view2(request): # pragma: no cover + pass + + def includeme1(config): + config.add_view(view1) + + def includeme2(config): + config.add_view(view2) + + config.include(includeme1) + config.include(includeme2) + try: + config.commit() + except ConfigurationConflictError as why: + c1, c2 = _conflictFunctions(why) + self.assertEqual(c1, 'includeme1') + self.assertEqual(c2, 'includeme2') + else: # pragma: no cover + raise AssertionError + + def test_commit_conflict_resolved_with_two_includes_and_local(self): + config = self._makeOne() + + def view1(request): # pragma: no cover + pass + + def view2(request): # pragma: no cover + pass + + def view3(request): # pragma: no cover + pass + + def includeme1(config): + config.add_view(view1) + + def includeme2(config): + config.add_view(view2) + + config.include(includeme1) + config.include(includeme2) + config.add_view(view3) + config.commit() + registeredview = self._getViewCallable(config) + self.assertEqual(registeredview.__name__, 'view3') + + def test_autocommit_no_conflicts(self): + from pyramid.renderers import null_renderer + + config = self._makeOne(autocommit=True) + + def view1(request): # pragma: no cover + pass + + def view2(request): # pragma: no cover + pass + + def view3(request): # pragma: no cover + pass + + config.add_view(view1, renderer=null_renderer) + config.add_view(view2, renderer=null_renderer) + config.add_view(view3, renderer=null_renderer) + config.commit() + registeredview = self._getViewCallable(config) + self.assertEqual(registeredview.__name__, 'view3') + + def test_conflict_set_notfound_view(self): + config = self._makeOne() + + def view1(request): # pragma: no cover + pass + + def view2(request): # pragma: no cover + pass + + config.set_notfound_view(view1) + config.set_notfound_view(view2) + try: + config.commit() + except ConfigurationConflictError as why: + c1, c2 = _conflictFunctions(why) + self.assertEqual(c1, 'test_conflict_set_notfound_view') + self.assertEqual(c2, 'test_conflict_set_notfound_view') + else: # pragma: no cover + raise AssertionError + + def test_conflict_set_forbidden_view(self): + config = self._makeOne() + + def view1(request): # pragma: no cover + pass + + def view2(request): # pragma: no cover + pass + + config.set_forbidden_view(view1) + config.set_forbidden_view(view2) + try: + config.commit() + except ConfigurationConflictError as why: + c1, c2 = _conflictFunctions(why) + self.assertEqual(c1, 'test_conflict_set_forbidden_view') + self.assertEqual(c2, 'test_conflict_set_forbidden_view') + else: # pragma: no cover + raise AssertionError + + +class TestActionState(unittest.TestCase): + def _makeOne(self): + from pyramid.config.actions import ActionState + + return ActionState() + + def test_it(self): + c = self._makeOne() + self.assertEqual(c.actions, []) + + def test_action_simple(self): + from . import dummyfactory as f + + c = self._makeOne() + c.actions = [] + c.action(1, f, (1,), {'x': 1}) + self.assertEqual( + c.actions, + [ + { + 'args': (1,), + 'callable': f, + 'discriminator': 1, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {'x': 1}, + 'order': 0, + } + ], + ) + c.action(None) + self.assertEqual( + c.actions, + [ + { + 'args': (1,), + 'callable': f, + 'discriminator': 1, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {'x': 1}, + 'order': 0, + }, + { + 'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {}, + 'order': 0, + }, + ], + ) + + def test_action_with_includepath(self): + c = self._makeOne() + c.actions = [] + c.action(None, includepath=('abc',)) + self.assertEqual( + c.actions, + [ + { + 'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': ('abc',), + 'info': None, + 'introspectables': (), + 'kw': {}, + 'order': 0, + } + ], + ) + + def test_action_with_info(self): + c = self._makeOne() + c.action(None, info='abc') + self.assertEqual( + c.actions, + [ + { + 'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': 'abc', + 'introspectables': (), + 'kw': {}, + 'order': 0, + } + ], + ) + + def test_action_with_includepath_and_info(self): + c = self._makeOne() + c.action(None, includepath=('spec',), info='bleh') + self.assertEqual( + c.actions, + [ + { + 'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': ('spec',), + 'info': 'bleh', + 'introspectables': (), + 'kw': {}, + 'order': 0, + } + ], + ) + + def test_action_with_order(self): + c = self._makeOne() + c.actions = [] + c.action(None, order=99999) + self.assertEqual( + c.actions, + [ + { + 'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {}, + 'order': 99999, + } + ], + ) + + def test_action_with_introspectables(self): + c = self._makeOne() + c.actions = [] + intr = DummyIntrospectable() + c.action(None, introspectables=(intr,)) + self.assertEqual( + c.actions, + [ + { + 'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': None, + 'introspectables': (intr,), + 'kw': {}, + 'order': 0, + } + ], + ) + + def test_processSpec(self): + c = self._makeOne() + self.assertTrue(c.processSpec('spec')) + self.assertFalse(c.processSpec('spec')) + + def test_execute_actions_tuples(self): + output = [] + + def f(*a, **k): + output.append((a, k)) + + c = self._makeOne() + c.actions = [ + (1, f, (1,)), + (1, f, (11,), {}, ('x',)), + (2, f, (2,)), + (None, None), + ] + c.execute_actions() + self.assertEqual(output, [((1,), {}), ((2,), {})]) + + def test_execute_actions_dicts(self): + output = [] + + def f(*a, **k): + output.append((a, k)) + + c = self._makeOne() + c.actions = [ + { + 'discriminator': 1, + 'callable': f, + 'args': (1,), + 'kw': {}, + 'order': 0, + 'includepath': (), + 'info': None, + 'introspectables': (), + }, + { + 'discriminator': 1, + 'callable': f, + 'args': (11,), + 'kw': {}, + 'includepath': ('x',), + 'order': 0, + 'info': None, + 'introspectables': (), + }, + { + 'discriminator': 2, + 'callable': f, + 'args': (2,), + 'kw': {}, + 'order': 0, + 'includepath': (), + 'info': None, + 'introspectables': (), + }, + { + 'discriminator': None, + 'callable': None, + 'args': (), + 'kw': {}, + 'order': 0, + 'includepath': (), + 'info': None, + 'introspectables': (), + }, + ] + c.execute_actions() + self.assertEqual(output, [((1,), {}), ((2,), {})]) + + def test_execute_actions_with_introspectables(self): + output = [] + + def f(*a, **k): + output.append((a, k)) + + c = self._makeOne() + intr = DummyIntrospectable() + c.actions = [ + { + 'discriminator': 1, + 'callable': f, + 'args': (1,), + 'kw': {}, + 'order': 0, + 'includepath': (), + 'info': None, + 'introspectables': (intr,), + } + ] + introspector = object() + c.execute_actions(introspector=introspector) + self.assertEqual(output, [((1,), {})]) + self.assertEqual(intr.registered, [(introspector, None)]) + + def test_execute_actions_with_introspectable_no_callable(self): + c = self._makeOne() + intr = DummyIntrospectable() + c.actions = [ + { + 'discriminator': 1, + 'callable': None, + 'args': (1,), + 'kw': {}, + 'order': 0, + 'includepath': (), + 'info': None, + 'introspectables': (intr,), + } + ] + introspector = object() + c.execute_actions(introspector=introspector) + self.assertEqual(intr.registered, [(introspector, None)]) + + def test_execute_actions_error(self): + output = [] + + def f(*a, **k): + output.append(('f', a, k)) + + def bad(): + raise NotImplementedError + + c = self._makeOne() + c.actions = [ + (1, f, (1,)), + (1, f, (11,), {}, ('x',)), + (2, f, (2,)), + (3, bad, (), {}, (), 'oops'), + ] + 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_with_deferred_discriminator(self): + # see https://github.com/Pylons/pyramid/issues/2697 + from pyramid.registry import Deferred + + output = [] + c = self._makeOne() + + def f(*a, **k): + output.append(('f', a, k)) + c.actions.append((4, g, (4,), {}, (), None, 2)) + + def g(*a, **k): + output.append(('g', a, k)) + + def h(*a, **k): + output.append(('h', a, k)) + + def discrim(): + self.assertEqual(output, [('f', (1,), {}), ('g', (2,), {})]) + return 3 + + d = Deferred(discrim) + c.actions = [ + (d, h, (3,), {}, (), None, 1), # order 1 + (1, f, (1,)), # order 0 + (2, g, (2,)), # order 0 + ] + c.execute_actions() + self.assertEqual( + output, + [ + ('f', (1,), {}), + ('g', (2,), {}), + ('h', (3,), {}), + ('g', (4,), {}), + ], + ) + + 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): # pragma: no cover + 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,))]) + + def test_executing_conflicting_action_across_orders(self): + from pyramid.exceptions import ConfigurationConflictError + + c = self._makeOne() + + def f(*a, **k): + pass + + def g(*a, **k): # pragma: no cover + pass + + c.actions = [(1, f, (1,), {}, (), None, -1), (1, g, (2,))] + self.assertRaises(ConfigurationConflictError, c.execute_actions) + + def test_executing_conflicting_action_across_reentrant_orders(self): + from pyramid.exceptions import ConfigurationConflictError + + c = self._makeOne() + + def f(*a, **k): + c.actions.append((1, g, (8,))) + + def g(*a, **k): # pragma: no cover + pass + + c.actions = [(1, f, (1,), {}, (), None, -1)] + self.assertRaises(ConfigurationConflictError, c.execute_actions) + + +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): # pragma: no cover + 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') + + def test_deferred_discriminator(self): + # see https://github.com/Pylons/pyramid/issues/2697 + from pyramid.config import PHASE0_CONFIG + + config = self._makeConfigurator() + + def deriver(view, info): + return view + + deriver.options = ('foo',) + config.add_view_deriver(deriver, 'foo_view') + # add_view uses a deferred discriminator and will fail if executed + # prior to add_view_deriver executing its action + config.add_view(lambda r: r.response, name='', foo=1) + + def dummy_action(): + # trigger a re-entrant action + config.action(None, lambda: None) + + config.action(None, dummy_action, order=PHASE0_CONFIG) + config.commit() + + +class Test_resolveConflicts(unittest.TestCase): + def _callFUT(self, actions): + from pyramid.config.actions import resolveConflicts + + return resolveConflicts(actions) + + def test_it_success_tuples(self): + from . import dummyfactory as f + + result = self._callFUT( + [ + (None, f), + (1, f, (1,), {}, (), 'first'), + (1, f, (2,), {}, ('x',), 'second'), + (1, f, (3,), {}, ('y',), 'third'), + (4, f, (4,), {}, ('y',), 'should be last', 99999), + (3, f, (3,), {}, ('y',)), + (None, f, (5,), {}, ('y',)), + ] + ) + result = list(result) + self.assertEqual( + result, + [ + { + 'info': None, + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0, + }, + { + 'info': 'first', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 0, + }, + { + 'info': None, + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': ('y',), + 'order': 0, + }, + { + 'info': None, + 'args': (5,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': ('y',), + 'order': 0, + }, + { + 'info': 'should be last', + 'args': (4,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 4, + 'includepath': ('y',), + 'order': 99999, + }, + ], + ) + + def test_it_success_dicts(self): + from . import dummyfactory as f + + result = self._callFUT( + [ + (None, f), + (1, f, (1,), {}, (), 'first'), + (1, f, (2,), {}, ('x',), 'second'), + (1, f, (3,), {}, ('y',), 'third'), + (4, f, (4,), {}, ('y',), 'should be last', 99999), + (3, f, (3,), {}, ('y',)), + (None, f, (5,), {}, ('y',)), + ] + ) + result = list(result) + self.assertEqual( + result, + [ + { + 'info': None, + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0, + }, + { + 'info': 'first', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 0, + }, + { + 'info': None, + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': ('y',), + 'order': 0, + }, + { + 'info': None, + 'args': (5,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': ('y',), + 'order': 0, + }, + { + 'info': 'should be last', + 'args': (4,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 4, + 'includepath': ('y',), + 'order': 99999, + }, + ], + ) + + def test_it_conflict(self): + from . import dummyfactory as f + + result = self._callFUT( + [ + (None, f), + (1, f, (2,), {}, ('x',), 'eek'), # will conflict + (1, f, (3,), {}, ('y',), 'ack'), # will conflict + (4, f, (4,), {}, ('y',)), + (3, f, (3,), {}, ('y',)), + (None, f, (5,), {}, ('y',)), + ] + ) + self.assertRaises(ConfigurationConflictError, list, result) + + def test_it_with_actions_grouped_by_order(self): + from . import dummyfactory as f + + result = self._callFUT( + [ + (None, f), # X + (1, f, (1,), {}, (), 'third', 10), # X + (1, f, (2,), {}, ('x',), 'fourth', 10), + (1, f, (3,), {}, ('y',), 'fifth', 10), + (2, f, (1,), {}, (), 'sixth', 10), # X + (3, f, (1,), {}, (), 'seventh', 10), # X + (5, f, (4,), {}, ('y',), 'eighth', 99999), # X + (4, f, (3,), {}, (), 'first', 5), # X + (4, f, (5,), {}, ('y',), 'second', 5), + ] + ) + result = list(result) + self.assertEqual(len(result), 6) + # resolved actions should be grouped by (order, i) + self.assertEqual( + result, + [ + { + 'info': None, + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0, + }, + { + 'info': 'first', + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 4, + 'includepath': (), + 'order': 5, + }, + { + 'info': 'third', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 10, + }, + { + 'info': 'sixth', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 2, + 'includepath': (), + 'order': 10, + }, + { + 'info': 'seventh', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': (), + 'order': 10, + }, + { + 'info': 'eighth', + 'args': (4,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 5, + 'includepath': ('y',), + 'order': 99999, + }, + ], + ) + + def test_override_success_across_orders(self): + from . import dummyfactory as f + + result = self._callFUT( + [ + (1, f, (2,), {}, ('x',), 'eek', 0), + (1, f, (3,), {}, ('x', 'y'), 'ack', 10), + ] + ) + result = list(result) + self.assertEqual( + result, + [ + { + 'info': 'eek', + 'args': (2,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': ('x',), + 'order': 0, + } + ], + ) + + def test_conflicts_across_orders(self): + from . import dummyfactory as f + + result = self._callFUT( + [ + (1, f, (2,), {}, ('x', 'y'), 'eek', 0), + (1, f, (3,), {}, ('x'), 'ack', 10), + ] + ) + self.assertRaises(ConfigurationConflictError, list, result) + + +class TestActionInfo(unittest.TestCase): + def _getTargetClass(self): + from pyramid.config.actions import ActionInfo + + return ActionInfo + + def _makeOne(self, filename, lineno, function, linerepr): + return self._getTargetClass()(filename, lineno, function, linerepr) + + def test_class_conforms(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IActionInfo + + verifyClass(IActionInfo, self._getTargetClass()) + + def test_instance_conforms(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IActionInfo + + verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f')) + + def test_ctor(self): + inst = self._makeOne('filename', 10, 'function', 'src') + self.assertEqual(inst.file, 'filename') + self.assertEqual(inst.line, 10) + self.assertEqual(inst.function, 'function') + self.assertEqual(inst.src, 'src') + + def test___str__(self): + inst = self._makeOne('filename', 0, 'function', ' linerepr ') + self.assertEqual( + str(inst), "Line 0 of file filename:\n linerepr " + ) + + +def _conflictFunctions(e): + conflicts = e._conflicts.values() + for conflict in conflicts: + for confinst in conflict: + yield confinst.function + + +class DummyActionState(object): + autocommit = False + info = '' + + def __init__(self): + self.actions = [] + + def action(self, *arg, **kw): + self.actions.append((arg, kw)) + + +class DummyIntrospectable(object): + def __init__(self): + self.registered = [] + + def register(self, introspector, action_info): + self.registered.append((introspector, action_info)) diff --git a/tests/test_config/test_init.py b/tests/test_config/test_init.py index 2c92b60..811672f 100644 --- a/tests/test_config/test_init.py +++ b/tests/test_config/test_init.py @@ -1,7 +1,5 @@ import os import unittest -from zope.interface import Interface -from zope.interface import implementer from pyramid.compat import im_func from pyramid.testing import skip_on @@ -10,7 +8,6 @@ from . import dummy_include from . import dummy_extend from . import dummy_extend2 -from . import IDummy from . import DummyContext from pyramid.exceptions import ConfigurationExecutionError @@ -34,8 +31,6 @@ name='', exception_view=False, ): - from zope.interface import Interface - from pyramid.interfaces import IRequest from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IExceptionViewClassifier @@ -44,10 +39,6 @@ classifier = IExceptionViewClassifier else: classifier = IViewClassifier - if ctx_iface is None: - ctx_iface = Interface - if request_iface is None: - request_iface = IRequest return config.registry.adapters.lookup( (classifier, request_iface, ctx_iface), IView, @@ -299,7 +290,6 @@ def test_ctor_httpexception_view_default(self): from pyramid.interfaces import IExceptionResponse from pyramid.httpexceptions import default_exceptionresponse_view - from pyramid.interfaces import IRequest config = self._makeOne() view = self._getViewCallable( @@ -309,7 +299,6 @@ def test_ctor_exceptionresponse_view_None(self): from pyramid.interfaces import IExceptionResponse - from pyramid.interfaces import IRequest config = self._makeOne(exceptionresponse_view=None) view = self._getViewCallable( @@ -319,7 +308,6 @@ def test_ctor_exceptionresponse_view_custom(self): from pyramid.interfaces import IExceptionResponse - from pyramid.interfaces import IRequest def exceptionresponse_view(context, request): # pragma: no cover pass @@ -548,7 +536,6 @@ def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from pyramid.renderers import null_renderer from zope.interface import implementedBy - from pyramid.interfaces import IRequest from pyramid.httpexceptions import HTTPNotFound from pyramid.registry import Registry @@ -975,110 +962,8 @@ config.include(include) self.assertTrue(stack[0] is config.registry) - def test_action_branching_kw_is_None(self): - config = self._makeOne(autocommit=True) - self.assertEqual(config.action('discrim'), None) - - def test_action_branching_kw_is_not_None(self): - config = self._makeOne(autocommit=True) - self.assertEqual(config.action('discrim', kw={'a': 1}), None) - - def test_action_autocommit_with_introspectables(self): - from pyramid.config.util import ActionInfo - - config = self._makeOne(autocommit=True) - intr = DummyIntrospectable() - config.action('discrim', introspectables=(intr,)) - self.assertEqual(len(intr.registered), 1) - self.assertEqual(intr.registered[0][0], config.introspector) - self.assertEqual(intr.registered[0][1].__class__, ActionInfo) - - def test_action_autocommit_with_introspectables_introspection_off(self): - config = self._makeOne(autocommit=True) - config.introspection = False - intr = DummyIntrospectable() - config.action('discrim', introspectables=(intr,)) - self.assertEqual(len(intr.registered), 0) - - def test_action_branching_nonautocommit_with_config_info(self): - config = self._makeOne(autocommit=False) - config.info = 'abc' - state = DummyActionState() - state.autocommit = False - config.action_state = state - config.action('discrim', kw={'a': 1}) - self.assertEqual( - state.actions, - [ - ( - (), - { - 'args': (), - 'callable': None, - 'discriminator': 'discrim', - 'includepath': (), - 'info': 'abc', - 'introspectables': (), - 'kw': {'a': 1}, - 'order': 0, - }, - ) - ], - ) - - def test_action_branching_nonautocommit_without_config_info(self): - config = self._makeOne(autocommit=False) - config.info = '' - config._ainfo = ['z'] - state = DummyActionState() - config.action_state = state - state.autocommit = False - config.action('discrim', kw={'a': 1}) - self.assertEqual( - state.actions, - [ - ( - (), - { - 'args': (), - 'callable': None, - 'discriminator': 'discrim', - 'includepath': (), - 'info': 'z', - 'introspectables': (), - 'kw': {'a': 1}, - 'order': 0, - }, - ) - ], - ) - - def test_action_branching_nonautocommit_with_introspectables(self): - config = self._makeOne(autocommit=False) - config.info = '' - config._ainfo = [] - state = DummyActionState() - config.action_state = state - state.autocommit = False - intr = DummyIntrospectable() - config.action('discrim', introspectables=(intr,)) - self.assertEqual(state.actions[0][1]['introspectables'], (intr,)) - - def test_action_nonautocommit_with_introspectables_introspection_off(self): - config = self._makeOne(autocommit=False) - config.info = '' - config._ainfo = [] - config.introspection = False - state = DummyActionState() - config.action_state = state - state.autocommit = False - intr = DummyIntrospectable() - config.action('discrim', introspectables=(intr,)) - self.assertEqual(state.actions[0][1]['introspectables'], ()) - def test_scan_integration(self): from zope.interface import alsoProvides - from pyramid.interfaces import IRequest from pyramid.view import render_view_to_response import tests.test_config.pkgs.scannable as package @@ -1185,7 +1070,6 @@ def test_scan_integration_with_ignore(self): from zope.interface import alsoProvides - from pyramid.interfaces import IRequest from pyramid.view import render_view_to_response import tests.test_config.pkgs.scannable as package @@ -1207,7 +1091,6 @@ def test_scan_integration_dottedname_package(self): from zope.interface import alsoProvides - from pyramid.interfaces import IRequest from pyramid.view import render_view_to_response config = self._makeOne(autocommit=True) @@ -1305,149 +1188,6 @@ self.assertNotEqual(sm, '123') finally: getSiteManager.reset() - - def test_commit_conflict_simple(self): - config = self._makeOne() - - def view1(request): # pragma: no cover - pass - - def view2(request): # pragma: no cover - pass - - config.add_view(view1) - config.add_view(view2) - self.assertRaises(ConfigurationConflictError, config.commit) - - def test_commit_conflict_resolved_with_include(self): - config = self._makeOne() - - def view1(request): # pragma: no cover - pass - - def view2(request): # pragma: no cover - pass - - def includeme(config): - config.add_view(view2) - - config.add_view(view1) - config.include(includeme) - config.commit() - registeredview = self._getViewCallable(config) - self.assertEqual(registeredview.__name__, 'view1') - - def test_commit_conflict_with_two_includes(self): - config = self._makeOne() - - def view1(request): # pragma: no cover - pass - - def view2(request): # pragma: no cover - pass - - def includeme1(config): - config.add_view(view1) - - def includeme2(config): - config.add_view(view2) - - config.include(includeme1) - config.include(includeme2) - try: - config.commit() - except ConfigurationConflictError as why: - c1, c2 = _conflictFunctions(why) - self.assertEqual(c1, 'includeme1') - self.assertEqual(c2, 'includeme2') - else: # pragma: no cover - raise AssertionError - - def test_commit_conflict_resolved_with_two_includes_and_local(self): - config = self._makeOne() - - def view1(request): # pragma: no cover - pass - - def view2(request): # pragma: no cover - pass - - def view3(request): # pragma: no cover - pass - - def includeme1(config): - config.add_view(view1) - - def includeme2(config): - config.add_view(view2) - - config.include(includeme1) - config.include(includeme2) - config.add_view(view3) - config.commit() - registeredview = self._getViewCallable(config) - self.assertEqual(registeredview.__name__, 'view3') - - def test_autocommit_no_conflicts(self): - from pyramid.renderers import null_renderer - - config = self._makeOne(autocommit=True) - - def view1(request): # pragma: no cover - pass - - def view2(request): # pragma: no cover - pass - - def view3(request): # pragma: no cover - pass - - config.add_view(view1, renderer=null_renderer) - config.add_view(view2, renderer=null_renderer) - config.add_view(view3, renderer=null_renderer) - config.commit() - registeredview = self._getViewCallable(config) - self.assertEqual(registeredview.__name__, 'view3') - - def test_conflict_set_notfound_view(self): - config = self._makeOne() - - def view1(request): # pragma: no cover - pass - - def view2(request): # pragma: no cover - pass - - config.set_notfound_view(view1) - config.set_notfound_view(view2) - try: - config.commit() - except ConfigurationConflictError as why: - c1, c2 = _conflictFunctions(why) - self.assertEqual(c1, 'test_conflict_set_notfound_view') - self.assertEqual(c2, 'test_conflict_set_notfound_view') - else: # pragma: no cover - raise AssertionError - - def test_conflict_set_forbidden_view(self): - config = self._makeOne() - - def view1(request): # pragma: no cover - pass - - def view2(request): # pragma: no cover - pass - - config.set_forbidden_view(view1) - config.set_forbidden_view(view2) - try: - config.commit() - except ConfigurationConflictError as why: - c1, c2 = _conflictFunctions(why) - self.assertEqual(c1, 'test_conflict_set_forbidden_view') - self.assertEqual(c2, 'test_conflict_set_forbidden_view') - else: # pragma: no cover - raise AssertionError def test___getattr__missing_when_directives_exist(self): config = self._makeOne() @@ -1624,749 +1364,6 @@ ) -class TestActionState(unittest.TestCase): - def _makeOne(self): - from pyramid.config import ActionState - - return ActionState() - - def test_it(self): - c = self._makeOne() - self.assertEqual(c.actions, []) - - def test_action_simple(self): - from . import dummyfactory as f - - c = self._makeOne() - c.actions = [] - c.action(1, f, (1,), {'x': 1}) - self.assertEqual( - c.actions, - [ - { - 'args': (1,), - 'callable': f, - 'discriminator': 1, - 'includepath': (), - 'info': None, - 'introspectables': (), - 'kw': {'x': 1}, - 'order': 0, - } - ], - ) - c.action(None) - self.assertEqual( - c.actions, - [ - { - 'args': (1,), - 'callable': f, - 'discriminator': 1, - 'includepath': (), - 'info': None, - 'introspectables': (), - 'kw': {'x': 1}, - 'order': 0, - }, - { - 'args': (), - 'callable': None, - 'discriminator': None, - 'includepath': (), - 'info': None, - 'introspectables': (), - 'kw': {}, - 'order': 0, - }, - ], - ) - - def test_action_with_includepath(self): - c = self._makeOne() - c.actions = [] - c.action(None, includepath=('abc',)) - self.assertEqual( - c.actions, - [ - { - 'args': (), - 'callable': None, - 'discriminator': None, - 'includepath': ('abc',), - 'info': None, - 'introspectables': (), - 'kw': {}, - 'order': 0, - } - ], - ) - - def test_action_with_info(self): - c = self._makeOne() - c.action(None, info='abc') - self.assertEqual( - c.actions, - [ - { - 'args': (), - 'callable': None, - 'discriminator': None, - 'includepath': (), - 'info': 'abc', - 'introspectables': (), - 'kw': {}, - 'order': 0, - } - ], - ) - - def test_action_with_includepath_and_info(self): - c = self._makeOne() - c.action(None, includepath=('spec',), info='bleh') - self.assertEqual( - c.actions, - [ - { - 'args': (), - 'callable': None, - 'discriminator': None, - 'includepath': ('spec',), - 'info': 'bleh', - 'introspectables': (), - 'kw': {}, - 'order': 0, - } - ], - ) - - def test_action_with_order(self): - c = self._makeOne() - c.actions = [] - c.action(None, order=99999) - self.assertEqual( - c.actions, - [ - { - 'args': (), - 'callable': None, - 'discriminator': None, - 'includepath': (), - 'info': None, - 'introspectables': (), - 'kw': {}, - 'order': 99999, - } - ], - ) - - def test_action_with_introspectables(self): - c = self._makeOne() - c.actions = [] - intr = DummyIntrospectable() - c.action(None, introspectables=(intr,)) - self.assertEqual( - c.actions, - [ - { - 'args': (), - 'callable': None, - 'discriminator': None, - 'includepath': (), - 'info': None, - 'introspectables': (intr,), - 'kw': {}, - 'order': 0, - } - ], - ) - - def test_processSpec(self): - c = self._makeOne() - self.assertTrue(c.processSpec('spec')) - self.assertFalse(c.processSpec('spec')) - - def test_execute_actions_tuples(self): - output = [] - - def f(*a, **k): - output.append((a, k)) - - c = self._makeOne() - c.actions = [ - (1, f, (1,)), - (1, f, (11,), {}, ('x',)), - (2, f, (2,)), - (None, None), - ] - c.execute_actions() - self.assertEqual(output, [((1,), {}), ((2,), {})]) - - def test_execute_actions_dicts(self): - output = [] - - def f(*a, **k): - output.append((a, k)) - - c = self._makeOne() - c.actions = [ - { - 'discriminator': 1, - 'callable': f, - 'args': (1,), - 'kw': {}, - 'order': 0, - 'includepath': (), - 'info': None, - 'introspectables': (), - }, - { - 'discriminator': 1, - 'callable': f, - 'args': (11,), - 'kw': {}, - 'includepath': ('x',), - 'order': 0, - 'info': None, - 'introspectables': (), - }, - { - 'discriminator': 2, - 'callable': f, - 'args': (2,), - 'kw': {}, - 'order': 0, - 'includepath': (), - 'info': None, - 'introspectables': (), - }, - { - 'discriminator': None, - 'callable': None, - 'args': (), - 'kw': {}, - 'order': 0, - 'includepath': (), - 'info': None, - 'introspectables': (), - }, - ] - c.execute_actions() - self.assertEqual(output, [((1,), {}), ((2,), {})]) - - def test_execute_actions_with_introspectables(self): - output = [] - - def f(*a, **k): - output.append((a, k)) - - c = self._makeOne() - intr = DummyIntrospectable() - c.actions = [ - { - 'discriminator': 1, - 'callable': f, - 'args': (1,), - 'kw': {}, - 'order': 0, - 'includepath': (), - 'info': None, - 'introspectables': (intr,), - } - ] - introspector = object() - c.execute_actions(introspector=introspector) - self.assertEqual(output, [((1,), {})]) - self.assertEqual(intr.registered, [(introspector, None)]) - - def test_execute_actions_with_introspectable_no_callable(self): - c = self._makeOne() - intr = DummyIntrospectable() - c.actions = [ - { - 'discriminator': 1, - 'callable': None, - 'args': (1,), - 'kw': {}, - 'order': 0, - 'includepath': (), - 'info': None, - 'introspectables': (intr,), - } - ] - introspector = object() - c.execute_actions(introspector=introspector) - self.assertEqual(intr.registered, [(introspector, None)]) - - def test_execute_actions_error(self): - output = [] - - def f(*a, **k): - output.append(('f', a, k)) - - def bad(): - raise NotImplementedError - - c = self._makeOne() - c.actions = [ - (1, f, (1,)), - (1, f, (11,), {}, ('x',)), - (2, f, (2,)), - (3, bad, (), {}, (), 'oops'), - ] - 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_with_deferred_discriminator(self): - # see https://github.com/Pylons/pyramid/issues/2697 - from pyramid.registry import Deferred - - output = [] - c = self._makeOne() - - def f(*a, **k): - output.append(('f', a, k)) - c.actions.append((4, g, (4,), {}, (), None, 2)) - - def g(*a, **k): - output.append(('g', a, k)) - - def h(*a, **k): - output.append(('h', a, k)) - - def discrim(): - self.assertEqual(output, [('f', (1,), {}), ('g', (2,), {})]) - return 3 - - d = Deferred(discrim) - c.actions = [ - (d, h, (3,), {}, (), None, 1), # order 1 - (1, f, (1,)), # order 0 - (2, g, (2,)), # order 0 - ] - c.execute_actions() - self.assertEqual( - output, - [ - ('f', (1,), {}), - ('g', (2,), {}), - ('h', (3,), {}), - ('g', (4,), {}), - ], - ) - - 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): # pragma: no cover - 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,))]) - - def test_executing_conflicting_action_across_orders(self): - from pyramid.exceptions import ConfigurationConflictError - - c = self._makeOne() - - def f(*a, **k): - pass - - def g(*a, **k): # pragma: no cover - pass - - c.actions = [(1, f, (1,), {}, (), None, -1), (1, g, (2,))] - self.assertRaises(ConfigurationConflictError, c.execute_actions) - - def test_executing_conflicting_action_across_reentrant_orders(self): - from pyramid.exceptions import ConfigurationConflictError - - c = self._makeOne() - - def f(*a, **k): - c.actions.append((1, g, (8,))) - - def g(*a, **k): # pragma: no cover - pass - - c.actions = [(1, f, (1,), {}, (), None, -1)] - self.assertRaises(ConfigurationConflictError, c.execute_actions) - - -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): # pragma: no cover - 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') - - def test_deferred_discriminator(self): - # see https://github.com/Pylons/pyramid/issues/2697 - from pyramid.config import PHASE0_CONFIG - - config = self._makeConfigurator() - - def deriver(view, info): - return view - - deriver.options = ('foo',) - config.add_view_deriver(deriver, 'foo_view') - # add_view uses a deferred discriminator and will fail if executed - # prior to add_view_deriver executing its action - config.add_view(lambda r: r.response, name='', foo=1) - - def dummy_action(): - # trigger a re-entrant action - config.action(None, lambda: None) - - config.action(None, dummy_action, order=PHASE0_CONFIG) - config.commit() - - -class Test_resolveConflicts(unittest.TestCase): - def _callFUT(self, actions): - from pyramid.config import resolveConflicts - - return resolveConflicts(actions) - - def test_it_success_tuples(self): - from . import dummyfactory as f - - result = self._callFUT( - [ - (None, f), - (1, f, (1,), {}, (), 'first'), - (1, f, (2,), {}, ('x',), 'second'), - (1, f, (3,), {}, ('y',), 'third'), - (4, f, (4,), {}, ('y',), 'should be last', 99999), - (3, f, (3,), {}, ('y',)), - (None, f, (5,), {}, ('y',)), - ] - ) - result = list(result) - self.assertEqual( - result, - [ - { - 'info': None, - 'args': (), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': None, - 'includepath': (), - 'order': 0, - }, - { - 'info': 'first', - 'args': (1,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 1, - 'includepath': (), - 'order': 0, - }, - { - 'info': None, - 'args': (3,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 3, - 'includepath': ('y',), - 'order': 0, - }, - { - 'info': None, - 'args': (5,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': None, - 'includepath': ('y',), - 'order': 0, - }, - { - 'info': 'should be last', - 'args': (4,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 4, - 'includepath': ('y',), - 'order': 99999, - }, - ], - ) - - def test_it_success_dicts(self): - from . import dummyfactory as f - - result = self._callFUT( - [ - (None, f), - (1, f, (1,), {}, (), 'first'), - (1, f, (2,), {}, ('x',), 'second'), - (1, f, (3,), {}, ('y',), 'third'), - (4, f, (4,), {}, ('y',), 'should be last', 99999), - (3, f, (3,), {}, ('y',)), - (None, f, (5,), {}, ('y',)), - ] - ) - result = list(result) - self.assertEqual( - result, - [ - { - 'info': None, - 'args': (), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': None, - 'includepath': (), - 'order': 0, - }, - { - 'info': 'first', - 'args': (1,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 1, - 'includepath': (), - 'order': 0, - }, - { - 'info': None, - 'args': (3,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 3, - 'includepath': ('y',), - 'order': 0, - }, - { - 'info': None, - 'args': (5,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': None, - 'includepath': ('y',), - 'order': 0, - }, - { - 'info': 'should be last', - 'args': (4,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 4, - 'includepath': ('y',), - 'order': 99999, - }, - ], - ) - - def test_it_conflict(self): - from . import dummyfactory as f - - result = self._callFUT( - [ - (None, f), - (1, f, (2,), {}, ('x',), 'eek'), # will conflict - (1, f, (3,), {}, ('y',), 'ack'), # will conflict - (4, f, (4,), {}, ('y',)), - (3, f, (3,), {}, ('y',)), - (None, f, (5,), {}, ('y',)), - ] - ) - self.assertRaises(ConfigurationConflictError, list, result) - - def test_it_with_actions_grouped_by_order(self): - from . import dummyfactory as f - - result = self._callFUT( - [ - (None, f), # X - (1, f, (1,), {}, (), 'third', 10), # X - (1, f, (2,), {}, ('x',), 'fourth', 10), - (1, f, (3,), {}, ('y',), 'fifth', 10), - (2, f, (1,), {}, (), 'sixth', 10), # X - (3, f, (1,), {}, (), 'seventh', 10), # X - (5, f, (4,), {}, ('y',), 'eighth', 99999), # X - (4, f, (3,), {}, (), 'first', 5), # X - (4, f, (5,), {}, ('y',), 'second', 5), - ] - ) - result = list(result) - self.assertEqual(len(result), 6) - # resolved actions should be grouped by (order, i) - self.assertEqual( - result, - [ - { - 'info': None, - 'args': (), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': None, - 'includepath': (), - 'order': 0, - }, - { - 'info': 'first', - 'args': (3,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 4, - 'includepath': (), - 'order': 5, - }, - { - 'info': 'third', - 'args': (1,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 1, - 'includepath': (), - 'order': 10, - }, - { - 'info': 'sixth', - 'args': (1,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 2, - 'includepath': (), - 'order': 10, - }, - { - 'info': 'seventh', - 'args': (1,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 3, - 'includepath': (), - 'order': 10, - }, - { - 'info': 'eighth', - 'args': (4,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 5, - 'includepath': ('y',), - 'order': 99999, - }, - ], - ) - - def test_override_success_across_orders(self): - from . import dummyfactory as f - - result = self._callFUT( - [ - (1, f, (2,), {}, ('x',), 'eek', 0), - (1, f, (3,), {}, ('x', 'y'), 'ack', 10), - ] - ) - result = list(result) - self.assertEqual( - result, - [ - { - 'info': 'eek', - 'args': (2,), - 'callable': f, - 'introspectables': (), - 'kw': {}, - 'discriminator': 1, - 'includepath': ('x',), - 'order': 0, - } - ], - ) - - def test_conflicts_across_orders(self): - from . import dummyfactory as f - - result = self._callFUT( - [ - (1, f, (2,), {}, ('x', 'y'), 'eek', 0), - (1, f, (3,), {}, ('x'), 'ack', 10), - ] - ) - self.assertRaises(ConfigurationConflictError, list, result) - - class TestGlobalRegistriesIntegration(unittest.TestCase): def setUp(self): from pyramid.config import global_registries @@ -2430,11 +1427,6 @@ self.popped = True -@implementer(IDummy) -class DummyEvent: - pass - - class DummyRegistry(object): def __init__(self, adaptation=None, util=None): self.utilities = [] @@ -2457,36 +1449,6 @@ def queryUtility(self, *arg, **kw): return self.util - - -class IOther(Interface): - pass - - -def _conflictFunctions(e): - conflicts = e._conflicts.values() - for conflict in conflicts: - for confinst in conflict: - yield confinst.function - - -class DummyActionState(object): - autocommit = False - info = '' - - def __init__(self): - self.actions = [] - - def action(self, *arg, **kw): - self.actions.append((arg, kw)) - - -class DummyIntrospectable(object): - def __init__(self): - self.registered = [] - - def register(self, introspector, action_info): - self.registered.append((introspector, action_info)) class DummyPredicate(object): diff --git a/tests/test_config/test_util.py b/tests/test_config/test_predicates.py similarity index 89% rename from tests/test_config/test_util.py rename to tests/test_config/test_predicates.py index 50d143b..079652b 100644 --- a/tests/test_config/test_util.py +++ b/tests/test_config/test_predicates.py @@ -3,44 +3,9 @@ from pyramid.compat import text_ -class TestActionInfo(unittest.TestCase): - def _getTargetClass(self): - from pyramid.config.util import ActionInfo - - return ActionInfo - - def _makeOne(self, filename, lineno, function, linerepr): - return self._getTargetClass()(filename, lineno, function, linerepr) - - def test_class_conforms(self): - from zope.interface.verify import verifyClass - from pyramid.interfaces import IActionInfo - - verifyClass(IActionInfo, self._getTargetClass()) - - def test_instance_conforms(self): - from zope.interface.verify import verifyObject - from pyramid.interfaces import IActionInfo - - verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f')) - - def test_ctor(self): - inst = self._makeOne('filename', 10, 'function', 'src') - self.assertEqual(inst.file, 'filename') - self.assertEqual(inst.line, 10) - self.assertEqual(inst.function, 'function') - self.assertEqual(inst.src, 'src') - - def test___str__(self): - inst = self._makeOne('filename', 0, 'function', ' linerepr ') - self.assertEqual( - str(inst), "Line 0 of file filename:\n linerepr " - ) - - class TestPredicateList(unittest.TestCase): def _makeOne(self): - from pyramid.config.util import PredicateList + from pyramid.config.predicates import PredicateList from pyramid import predicates inst = PredicateList() @@ -71,7 +36,7 @@ self.assertTrue(order1 < order2) def test_ordering_number_of_predicates(self): - from pyramid.config.util import predvalseq + from pyramid.config.predicates import predvalseq order1, _, _ = self._callFUT( xhr='xhr', @@ -169,7 +134,7 @@ self.assertTrue(order12 > order10) def test_ordering_importance_of_predicates(self): - from pyramid.config.util import predvalseq + from pyramid.config.predicates import predvalseq order1, _, _ = self._callFUT(xhr='xhr') order2, _, _ = self._callFUT(request_method='request_method') @@ -194,7 +159,7 @@ self.assertTrue(order9 > order10) def test_ordering_importance_and_number(self): - from pyramid.config.util import predvalseq + from pyramid.config.predicates import predvalseq order1, _, _ = self._callFUT( xhr='xhr', request_method='request_method' @@ -233,7 +198,7 @@ self.assertTrue(order1 > order2) def test_different_custom_predicates_with_same_hash(self): - from pyramid.config.util import predvalseq + from pyramid.config.predicates import predvalseq class PredicateWithHash(object): def __hash__(self): @@ -286,7 +251,7 @@ ) def test_custom_predicates_can_affect_traversal(self): - from pyramid.config.util import predvalseq + from pyramid.config.predicates import predvalseq def custom(info, request): m = info['match'] @@ -312,7 +277,7 @@ ) def test_predicate_text_is_correct(self): - from pyramid.config.util import predvalseq + from pyramid.config.predicates import predvalseq _, predicates, _ = self._callFUT( xhr='xhr', @@ -420,20 +385,9 @@ self.assertEqual(predicates[2](None, request), True) -class TestDeprecatedPredicates(unittest.TestCase): - def test_it(self): - import warnings - - with warnings.catch_warnings(record=True) as w: - warnings.filterwarnings('always') - from pyramid.config.predicates import XHRPredicate # noqa: F401 - - self.assertEqual(len(w), 1) - - class Test_sort_accept_offers(unittest.TestCase): def _callFUT(self, offers, order=None): - from pyramid.config.util import sort_accept_offers + from pyramid.config.predicates import sort_accept_offers return sort_accept_offers(offers, order) diff --git a/tests/test_config/test_testing.py b/tests/test_config/test_testing.py index ce66596..ede31e1 100644 --- a/tests/test_config/test_testing.py +++ b/tests/test_config/test_testing.py @@ -104,7 +104,7 @@ def test_testing_add_subscriber_dottedname(self): config = self._makeOne(autocommit=True) - L = config.testing_add_subscriber('tests.test_config.test_init.IDummy') + L = config.testing_add_subscriber('tests.test_config.IDummy') event = DummyEvent() config.registry.notify(event) self.assertEqual(len(L), 1) diff --git a/tests/test_config/test_views.py b/tests/test_config/test_views.py index 977944f..5e722c9 100644 --- a/tests/test_config/test_views.py +++ b/tests/test_config/test_views.py @@ -664,7 +664,7 @@ self.assertEqual(wrapper(None, request), 'OK') def test_add_view_default_phash_overrides_default_phash(self): - from pyramid.config.util import DEFAULT_PHASH + from pyramid.config.predicates import DEFAULT_PHASH from pyramid.renderers import null_renderer from zope.interface import Interface from pyramid.interfaces import IRequest @@ -690,7 +690,7 @@ self.assertEqual(wrapper(None, request), 'OK') def test_add_view_exc_default_phash_overrides_default_phash(self): - from pyramid.config.util import DEFAULT_PHASH + from pyramid.config.predicates import DEFAULT_PHASH from pyramid.renderers import null_renderer from zope.interface import implementedBy from pyramid.interfaces import IRequest diff --git a/tests/test_viewderivers.py b/tests/test_viewderivers.py index a1455ed..f01cb49 100644 --- a/tests/test_viewderivers.py +++ b/tests/test_viewderivers.py @@ -1265,7 +1265,7 @@ self.assertEqual(result(None, None), response) def test_attr_wrapped_view_branching_default_phash(self): - from pyramid.config.util import DEFAULT_PHASH + from pyramid.config.predicates import DEFAULT_PHASH def view(context, request): # pragma: no cover pass -- Gitblit v1.9.3