Michael Merickel
2018-10-19 d579f2104de139e0b0fc5d6c81aabb2f826e5e54
move predicate-related code into pyramid.config.predicates
1 files deleted
16 files modified
1 files renamed
777 ■■■■ changed files
src/pyramid/config/__init__.py 45 ●●●●● patch | view | raw | blame | history
src/pyramid/config/actions.py 64 ●●●●● patch | view | raw | blame | history
src/pyramid/config/adapters.py 2 ●●● patch | view | raw | blame | history
src/pyramid/config/assets.py 2 ●●● patch | view | raw | blame | history
src/pyramid/config/factories.py 2 ●●● patch | view | raw | blame | history
src/pyramid/config/i18n.py 2 ●●● patch | view | raw | blame | history
src/pyramid/config/predicates.py 257 ●●●●● patch | view | raw | blame | history
src/pyramid/config/rendering.py 2 ●●● patch | view | raw | blame | history
src/pyramid/config/routes.py 10 ●●●●● patch | view | raw | blame | history
src/pyramid/config/security.py 2 ●●● patch | view | raw | blame | history
src/pyramid/config/testing.py 2 ●●● patch | view | raw | blame | history
src/pyramid/config/tweens.py 2 ●●● patch | view | raw | blame | history
src/pyramid/config/util.py 276 ●●●●● patch | view | raw | blame | history
src/pyramid/config/views.py 4 ●●●● patch | view | raw | blame | history
tests/test_config/test_actions.py 37 ●●●●● patch | view | raw | blame | history
tests/test_config/test_predicates.py 62 ●●●● patch | view | raw | blame | history
tests/test_config/test_views.py 4 ●●●● patch | view | raw | blame | history
tests/test_viewderivers.py 2 ●●● patch | view | raw | blame | history
src/pyramid/config/__init__.py
@@ -9,7 +9,6 @@
from pyramid.interfaces import (
    IDebugLogger,
    IExceptionResponse,
    IPredicateList,
    PHASE0_CONFIG,
    PHASE1_CONFIG,
    PHASE2_CONFIG,
@@ -40,13 +39,15 @@
from pyramid.util import WeakOrderedSet, object_description
from pyramid.config.util import PredicateList, action_method, not_
from pyramid.config.actions import action_method
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
@@ -71,6 +72,7 @@
class Configurator(
    ActionConfiguratorMixin,
    PredicateConfiguratorMixin,
    TestingConfiguratorMixin,
    TweensConfiguratorMixin,
    SecurityConfiguratorMixin,
@@ -530,45 +532,6 @@
    introspector = property(
        _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
    def include(self, callable, route_prefix=None):
        """Include a configuration callable, to support imperative
src/pyramid/config/actions.py
@@ -1,18 +1,19 @@
import functools
import itertools
import operator
import sys
import traceback
from zope.interface import implementer
from pyramid.compat import reraise
from pyramid.config.util import ActionInfo
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):
@@ -152,6 +153,7 @@
        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):
@@ -523,3 +525,57 @@
        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
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):
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):
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):
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):
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)
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),
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):
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):
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):
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):
src/pyramid/config/util.py
File was deleted
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,
tests/test_config/test_actions.py
@@ -50,7 +50,7 @@
        self.assertEqual(config.action('discrim', kw={'a': 1}), None)
    def test_action_autocommit_with_introspectables(self):
        from pyramid.config.util import ActionInfo
        from pyramid.config.actions import ActionInfo
        config = self._makeOne(autocommit=True)
        intr = DummyIntrospectable()
@@ -1029,6 +1029,41 @@
        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:
tests/test_config/test_predicates.py
File was renamed from tests/test_config/test_util.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)
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
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