From d579f2104de139e0b0fc5d6c81aabb2f826e5e54 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 18 Oct 2018 20:44:42 -0500 Subject: move predicate-related code into pyramid.config.predicates --- src/pyramid/config/__init__.py | 45 +-- src/pyramid/config/actions.py | 64 ++++- src/pyramid/config/adapters.py | 2 +- src/pyramid/config/assets.py | 2 +- src/pyramid/config/factories.py | 2 +- src/pyramid/config/i18n.py | 2 +- src/pyramid/config/predicates.py | 257 ++++++++++++++++- src/pyramid/config/rendering.py | 2 +- src/pyramid/config/routes.py | 10 +- src/pyramid/config/security.py | 2 +- src/pyramid/config/testing.py | 2 +- src/pyramid/config/tweens.py | 2 +- src/pyramid/config/util.py | 276 ------------------ src/pyramid/config/views.py | 4 +- tests/test_config/test_actions.py | 37 ++- tests/test_config/test_predicates.py | 480 ++++++++++++++++++++++++++++++++ tests/test_config/test_util.py | 526 ----------------------------------- tests/test_config/test_views.py | 4 +- tests/test_viewderivers.py | 2 +- 19 files changed, 851 insertions(+), 870 deletions(-) delete mode 100644 src/pyramid/config/util.py create mode 100644 tests/test_config/test_predicates.py delete mode 100644 tests/test_config/test_util.py diff --git a/src/pyramid/config/__init__.py b/src/pyramid/config/__init__.py index e4dcc6bb7..5cff20754 100644 --- a/src/pyramid/config/__init__.py +++ b/src/pyramid/config/__init__.py @@ -9,7 +9,6 @@ from webob.exc import WSGIHTTPException as WebobWSGIHTTPException from pyramid.interfaces import ( IDebugLogger, IExceptionResponse, - IPredicateList, PHASE0_CONFIG, PHASE1_CONFIG, PHASE2_CONFIG, @@ -40,13 +39,15 @@ from pyramid.threadlocal import manager 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 @@ PHASE3_CONFIG = PHASE3_CONFIG # api class Configurator( ActionConfiguratorMixin, + PredicateConfiguratorMixin, TestingConfiguratorMixin, TweensConfiguratorMixin, SecurityConfiguratorMixin, @@ -531,45 +533,6 @@ class Configurator( _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 application extensibility. diff --git a/src/pyramid/config/actions.py b/src/pyramid/config/actions.py index 353ed5edf..9c1227d4a 100644 --- a/src/pyramid/config/actions.py +++ b/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): @@ -153,6 +154,7 @@ class ActionConfiguratorMixin(object): 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): @@ -523,3 +525,57 @@ def expand_action_tuple( 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 e5668c40e..54c239ab3 100644 --- a/src/pyramid/config/adapters.py +++ b/src/pyramid/config/adapters.py @@ -8,7 +8,7 @@ from pyramid.interfaces import IResponse, ITraverser, IResourceURL 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 fd8b2ee49..e505fd204 100644 --- a/src/pyramid/config/assets.py +++ b/src/pyramid/config/assets.py @@ -9,7 +9,7 @@ from pyramid.interfaces import IPackageOverrides, PHASE1_CONFIG 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 2ec1558a6..16211989f 100644 --- a/src/pyramid/config/factories.py +++ b/src/pyramid/config/factories.py @@ -15,7 +15,7 @@ from pyramid.traversal import DefaultRootFactory 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 6e7334448..92c324ff7 100644 --- a/src/pyramid/config/i18n.py +++ b/src/pyramid/config/i18n.py @@ -3,7 +3,7 @@ from pyramid.interfaces import ILocaleNegotiator, ITranslationDirectories 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 cdbf68ca4..8f16f74af 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 948199636..7e5b767d9 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 7a76e9e68..a14662370 100644 --- a/src/pyramid/config/routes.py +++ b/src/pyramid/config/routes.py @@ -10,18 +10,14 @@ from pyramid.interfaces import ( ) 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 3b55c41d7..08e7cb81a 100644 --- a/src/pyramid/config/security.py +++ b/src/pyramid/config/security.py @@ -14,7 +14,7 @@ from pyramid.csrf import LegacySessionCSRFStoragePolicy 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 1655df52c..bba5054e6 100644 --- a/src/pyramid/config/testing.py +++ b/src/pyramid/config/testing.py @@ -11,7 +11,7 @@ from pyramid.renderers import RendererHelper 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 b74a57adf..7fc786a97 100644 --- a/src/pyramid/config/tweens.py +++ b/src/pyramid/config/tweens.py @@ -10,7 +10,7 @@ from pyramid.tweens import MAIN, INGRESS, EXCVIEW 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 8723b7721..000000000 --- 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 cc5b48ecb..bd1b693ba 100644 --- a/src/pyramid/config/views.py +++ b/src/pyramid/config/views.py @@ -74,8 +74,8 @@ from pyramid.viewderivers import ( 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 index ba8b8c124..098f73585 100644 --- a/tests/test_config/test_actions.py +++ b/tests/test_config/test_actions.py @@ -50,7 +50,7 @@ class ConfiguratorTests(unittest.TestCase): 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 @@ class Test_resolveConflicts(unittest.TestCase): 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: diff --git a/tests/test_config/test_predicates.py b/tests/test_config/test_predicates.py new file mode 100644 index 000000000..079652b39 --- /dev/null +++ b/tests/test_config/test_predicates.py @@ -0,0 +1,480 @@ +import unittest + +from pyramid.compat import text_ + + +class TestPredicateList(unittest.TestCase): + def _makeOne(self): + from pyramid.config.predicates import PredicateList + from pyramid import predicates + + inst = PredicateList() + for name, factory in ( + ('xhr', predicates.XHRPredicate), + ('request_method', predicates.RequestMethodPredicate), + ('path_info', predicates.PathInfoPredicate), + ('request_param', predicates.RequestParamPredicate), + ('header', predicates.HeaderPredicate), + ('accept', predicates.AcceptPredicate), + ('containment', predicates.ContainmentPredicate), + ('request_type', predicates.RequestTypePredicate), + ('match_param', predicates.MatchParamPredicate), + ('custom', predicates.CustomPredicate), + ('traverse', predicates.TraversePredicate), + ): + inst.add(name, factory) + return inst + + def _callFUT(self, **kw): + inst = self._makeOne() + config = DummyConfigurator() + return inst.make(config, **kw) + + def test_ordering_xhr_and_request_method_trump_only_containment(self): + order1, _, _ = self._callFUT(xhr=True, request_method='GET') + order2, _, _ = self._callFUT(containment=True) + self.assertTrue(order1 < order2) + + def test_ordering_number_of_predicates(self): + from pyramid.config.predicates import predvalseq + + order1, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + accept='accept', + containment='containment', + request_type='request_type', + custom=predvalseq([DummyCustomPredicate()]), + ) + order2, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + accept='accept', + containment='containment', + request_type='request_type', + custom=predvalseq([DummyCustomPredicate()]), + ) + order3, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + accept='accept', + containment='containment', + request_type='request_type', + ) + order4, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + accept='accept', + containment='containment', + ) + order5, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + accept='accept', + ) + order6, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + ) + order7, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + ) + order8, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + ) + order9, _, _ = self._callFUT( + xhr='xhr', request_method='request_method', path_info='path_info' + ) + order10, _, _ = self._callFUT( + xhr='xhr', request_method='request_method' + ) + order11, _, _ = self._callFUT(xhr='xhr') + order12, _, _ = self._callFUT() + self.assertEqual(order1, order2) + self.assertTrue(order3 > order2) + self.assertTrue(order4 > order3) + self.assertTrue(order5 > order4) + self.assertTrue(order6 > order5) + self.assertTrue(order7 > order6) + self.assertTrue(order8 > order7) + self.assertTrue(order9 > order8) + self.assertTrue(order10 > order9) + self.assertTrue(order11 > order10) + self.assertTrue(order12 > order10) + + def test_ordering_importance_of_predicates(self): + from pyramid.config.predicates import predvalseq + + order1, _, _ = self._callFUT(xhr='xhr') + order2, _, _ = self._callFUT(request_method='request_method') + order3, _, _ = self._callFUT(path_info='path_info') + order4, _, _ = self._callFUT(request_param='param') + order5, _, _ = self._callFUT(header='header') + order6, _, _ = self._callFUT(accept='accept') + order7, _, _ = self._callFUT(containment='containment') + order8, _, _ = self._callFUT(request_type='request_type') + order9, _, _ = self._callFUT(match_param='foo=bar') + order10, _, _ = self._callFUT( + custom=predvalseq([DummyCustomPredicate()]) + ) + self.assertTrue(order1 > order2) + self.assertTrue(order2 > order3) + self.assertTrue(order3 > order4) + self.assertTrue(order4 > order5) + self.assertTrue(order5 > order6) + self.assertTrue(order6 > order7) + self.assertTrue(order7 > order8) + self.assertTrue(order8 > order9) + self.assertTrue(order9 > order10) + + def test_ordering_importance_and_number(self): + from pyramid.config.predicates import predvalseq + + order1, _, _ = self._callFUT( + xhr='xhr', request_method='request_method' + ) + order2, _, _ = self._callFUT( + custom=predvalseq([DummyCustomPredicate()]) + ) + self.assertTrue(order1 < order2) + + order1, _, _ = self._callFUT( + xhr='xhr', request_method='request_method' + ) + order2, _, _ = self._callFUT( + request_method='request_method', + custom=predvalseq([DummyCustomPredicate()]), + ) + self.assertTrue(order1 > order2) + + order1, _, _ = self._callFUT( + xhr='xhr', request_method='request_method', path_info='path_info' + ) + order2, _, _ = self._callFUT( + request_method='request_method', + custom=predvalseq([DummyCustomPredicate()]), + ) + self.assertTrue(order1 < order2) + + order1, _, _ = self._callFUT( + xhr='xhr', request_method='request_method', path_info='path_info' + ) + order2, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + custom=predvalseq([DummyCustomPredicate()]), + ) + self.assertTrue(order1 > order2) + + def test_different_custom_predicates_with_same_hash(self): + from pyramid.config.predicates import predvalseq + + class PredicateWithHash(object): + def __hash__(self): + return 1 + + a = PredicateWithHash() + b = PredicateWithHash() + _, _, a_phash = self._callFUT(custom=predvalseq([a])) + _, _, b_phash = self._callFUT(custom=predvalseq([b])) + self.assertEqual(a_phash, b_phash) + + def test_traverse_has_remainder_already(self): + order, predicates, phash = self._callFUT(traverse='/1/:a/:b') + self.assertEqual(len(predicates), 1) + pred = predicates[0] + info = {'traverse': 'abc'} + request = DummyRequest() + result = pred(info, request) + self.assertEqual(result, True) + self.assertEqual(info, {'traverse': 'abc'}) + + def test_traverse_matches(self): + order, predicates, phash = self._callFUT(traverse='/1/:a/:b') + self.assertEqual(len(predicates), 1) + pred = predicates[0] + info = {'match': {'a': 'a', 'b': 'b'}} + request = DummyRequest() + result = pred(info, request) + self.assertEqual(result, True) + self.assertEqual( + info, {'match': {'a': 'a', 'b': 'b', 'traverse': ('1', 'a', 'b')}} + ) + + def test_traverse_matches_with_highorder_chars(self): + order, predicates, phash = self._callFUT( + traverse=text_(b'/La Pe\xc3\xb1a/{x}', 'utf-8') + ) + self.assertEqual(len(predicates), 1) + pred = predicates[0] + info = {'match': {'x': text_(b'Qu\xc3\xa9bec', 'utf-8')}} + request = DummyRequest() + result = pred(info, request) + self.assertEqual(result, True) + self.assertEqual( + info['match']['traverse'], + ( + text_(b'La Pe\xc3\xb1a', 'utf-8'), + text_(b'Qu\xc3\xa9bec', 'utf-8'), + ), + ) + + def test_custom_predicates_can_affect_traversal(self): + from pyramid.config.predicates import predvalseq + + def custom(info, request): + m = info['match'] + m['dummy'] = 'foo' + return True + + _, predicates, _ = self._callFUT( + custom=predvalseq([custom]), traverse='/1/:dummy/:a' + ) + self.assertEqual(len(predicates), 2) + info = {'match': {'a': 'a'}} + request = DummyRequest() + self.assertTrue(all([p(info, request) for p in predicates])) + self.assertEqual( + info, + { + 'match': { + 'a': 'a', + 'dummy': 'foo', + 'traverse': ('1', 'foo', 'a'), + } + }, + ) + + def test_predicate_text_is_correct(self): + from pyramid.config.predicates import predvalseq + + _, predicates, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + header='header', + accept='accept', + containment='containment', + request_type='request_type', + custom=predvalseq( + [ + DummyCustomPredicate(), + DummyCustomPredicate.classmethod_predicate, + DummyCustomPredicate.classmethod_predicate_no_text, + ] + ), + match_param='foo=bar', + ) + self.assertEqual(predicates[0].text(), 'xhr = True') + self.assertEqual( + predicates[1].text(), "request_method = request_method" + ) + self.assertEqual(predicates[2].text(), 'path_info = path_info') + self.assertEqual(predicates[3].text(), 'request_param param') + self.assertEqual(predicates[4].text(), 'header header') + self.assertEqual(predicates[5].text(), 'accept = accept') + self.assertEqual(predicates[6].text(), 'containment = containment') + self.assertEqual(predicates[7].text(), 'request_type = request_type') + self.assertEqual(predicates[8].text(), "match_param foo=bar") + self.assertEqual(predicates[9].text(), 'custom predicate') + self.assertEqual(predicates[10].text(), 'classmethod predicate') + self.assertTrue(predicates[11].text().startswith('custom predicate')) + + def test_match_param_from_string(self): + _, predicates, _ = self._callFUT(match_param='foo=bar') + request = DummyRequest() + request.matchdict = {'foo': 'bar', 'baz': 'bum'} + self.assertTrue(predicates[0](Dummy(), request)) + + def test_match_param_from_string_fails(self): + _, predicates, _ = self._callFUT(match_param='foo=bar') + request = DummyRequest() + request.matchdict = {'foo': 'bum', 'baz': 'bum'} + self.assertFalse(predicates[0](Dummy(), request)) + + def test_match_param_from_dict(self): + _, predicates, _ = self._callFUT(match_param=('foo=bar', 'baz=bum')) + request = DummyRequest() + request.matchdict = {'foo': 'bar', 'baz': 'bum'} + self.assertTrue(predicates[0](Dummy(), request)) + + def test_match_param_from_dict_fails(self): + _, predicates, _ = self._callFUT(match_param=('foo=bar', 'baz=bum')) + request = DummyRequest() + request.matchdict = {'foo': 'bar', 'baz': 'foo'} + self.assertFalse(predicates[0](Dummy(), request)) + + def test_request_method_sequence(self): + _, predicates, _ = self._callFUT(request_method=('GET', 'HEAD')) + request = DummyRequest() + request.method = 'HEAD' + self.assertTrue(predicates[0](Dummy(), request)) + request.method = 'GET' + self.assertTrue(predicates[0](Dummy(), request)) + request.method = 'POST' + self.assertFalse(predicates[0](Dummy(), request)) + + def test_request_method_ordering_hashes_same(self): + hash1, _, __ = self._callFUT(request_method=('GET', 'HEAD')) + hash2, _, __ = self._callFUT(request_method=('HEAD', 'GET')) + self.assertEqual(hash1, hash2) + hash1, _, __ = self._callFUT(request_method=('GET',)) + hash2, _, __ = self._callFUT(request_method='GET') + self.assertEqual(hash1, hash2) + + def test_unknown_predicate(self): + from pyramid.exceptions import ConfigurationError + + self.assertRaises(ConfigurationError, self._callFUT, unknown=1) + + def test_predicate_close_matches(self): + from pyramid.exceptions import ConfigurationError + + with self.assertRaises(ConfigurationError) as context: + self._callFUT(method='GET') + expected_msg = ( + "Unknown predicate values: {'method': 'GET'} " + "(did you mean request_method)" + ) + self.assertEqual(context.exception.args[0], expected_msg) + + def test_notted(self): + from pyramid.config import not_ + from pyramid.testing import DummyRequest + + request = DummyRequest() + _, predicates, _ = self._callFUT( + xhr='xhr', request_method=not_('POST'), header=not_('header') + ) + self.assertEqual(predicates[0].text(), 'xhr = True') + self.assertEqual(predicates[1].text(), "!request_method = POST") + self.assertEqual(predicates[2].text(), '!header header') + self.assertEqual(predicates[1](None, request), True) + self.assertEqual(predicates[2](None, request), True) + + +class Test_sort_accept_offers(unittest.TestCase): + def _callFUT(self, offers, order=None): + from pyramid.config.predicates import sort_accept_offers + + return sort_accept_offers(offers, order) + + def test_default_specificities(self): + result = self._callFUT(['text/html', 'text/html;charset=utf8']) + self.assertEqual(result, ['text/html;charset=utf8', 'text/html']) + + def test_specific_type_order(self): + result = self._callFUT( + [ + 'text/html', + 'application/json', + 'text/html;charset=utf8', + 'text/plain', + ], + ['application/json', 'text/html'], + ) + self.assertEqual( + result, + [ + 'application/json', + 'text/html;charset=utf8', + 'text/html', + 'text/plain', + ], + ) + + def test_params_order(self): + result = self._callFUT( + [ + 'text/html;charset=utf8', + 'text/html;charset=latin1', + 'text/html;foo=bar', + ], + ['text/html;charset=latin1', 'text/html;charset=utf8'], + ) + self.assertEqual( + result, + [ + 'text/html;charset=latin1', + 'text/html;charset=utf8', + 'text/html;foo=bar', + ], + ) + + def test_params_inherit_type_prefs(self): + result = self._callFUT( + ['text/html;charset=utf8', 'text/plain;charset=latin1'], + ['text/plain', 'text/html'], + ) + self.assertEqual( + result, ['text/plain;charset=latin1', 'text/html;charset=utf8'] + ) + + +class DummyCustomPredicate(object): + def __init__(self): + self.__text__ = 'custom predicate' + + def classmethod_predicate(*args): # pragma: no cover + pass + + classmethod_predicate.__text__ = 'classmethod predicate' + classmethod_predicate = classmethod(classmethod_predicate) + + @classmethod + def classmethod_predicate_no_text(*args): + pass # pragma: no cover + + +class Dummy(object): + def __init__(self, **kw): + self.__dict__.update(**kw) + + +class DummyRequest: + subpath = () + matchdict = None + + def __init__(self, environ=None): + if environ is None: + environ = {} + self.environ = environ + self.params = {} + self.cookies = {} + + +class DummyConfigurator(object): + def maybe_dotted(self, thing): + return thing diff --git a/tests/test_config/test_util.py b/tests/test_config/test_util.py deleted file mode 100644 index 50d143b2d..000000000 --- a/tests/test_config/test_util.py +++ /dev/null @@ -1,526 +0,0 @@ -import unittest - -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 import predicates - - inst = PredicateList() - for name, factory in ( - ('xhr', predicates.XHRPredicate), - ('request_method', predicates.RequestMethodPredicate), - ('path_info', predicates.PathInfoPredicate), - ('request_param', predicates.RequestParamPredicate), - ('header', predicates.HeaderPredicate), - ('accept', predicates.AcceptPredicate), - ('containment', predicates.ContainmentPredicate), - ('request_type', predicates.RequestTypePredicate), - ('match_param', predicates.MatchParamPredicate), - ('custom', predicates.CustomPredicate), - ('traverse', predicates.TraversePredicate), - ): - inst.add(name, factory) - return inst - - def _callFUT(self, **kw): - inst = self._makeOne() - config = DummyConfigurator() - return inst.make(config, **kw) - - def test_ordering_xhr_and_request_method_trump_only_containment(self): - order1, _, _ = self._callFUT(xhr=True, request_method='GET') - order2, _, _ = self._callFUT(containment=True) - self.assertTrue(order1 < order2) - - def test_ordering_number_of_predicates(self): - from pyramid.config.util import predvalseq - - order1, _, _ = self._callFUT( - xhr='xhr', - request_method='request_method', - path_info='path_info', - request_param='param', - match_param='foo=bar', - header='header', - accept='accept', - containment='containment', - request_type='request_type', - custom=predvalseq([DummyCustomPredicate()]), - ) - order2, _, _ = self._callFUT( - xhr='xhr', - request_method='request_method', - path_info='path_info', - request_param='param', - match_param='foo=bar', - header='header', - accept='accept', - containment='containment', - request_type='request_type', - custom=predvalseq([DummyCustomPredicate()]), - ) - order3, _, _ = self._callFUT( - xhr='xhr', - request_method='request_method', - path_info='path_info', - request_param='param', - match_param='foo=bar', - header='header', - accept='accept', - containment='containment', - request_type='request_type', - ) - order4, _, _ = self._callFUT( - xhr='xhr', - request_method='request_method', - path_info='path_info', - request_param='param', - match_param='foo=bar', - header='header', - accept='accept', - containment='containment', - ) - order5, _, _ = self._callFUT( - xhr='xhr', - request_method='request_method', - path_info='path_info', - request_param='param', - match_param='foo=bar', - header='header', - accept='accept', - ) - order6, _, _ = self._callFUT( - xhr='xhr', - request_method='request_method', - path_info='path_info', - request_param='param', - match_param='foo=bar', - header='header', - ) - order7, _, _ = self._callFUT( - xhr='xhr', - request_method='request_method', - path_info='path_info', - request_param='param', - match_param='foo=bar', - ) - order8, _, _ = self._callFUT( - xhr='xhr', - request_method='request_method', - path_info='path_info', - request_param='param', - ) - order9, _, _ = self._callFUT( - xhr='xhr', request_method='request_method', path_info='path_info' - ) - order10, _, _ = self._callFUT( - xhr='xhr', request_method='request_method' - ) - order11, _, _ = self._callFUT(xhr='xhr') - order12, _, _ = self._callFUT() - self.assertEqual(order1, order2) - self.assertTrue(order3 > order2) - self.assertTrue(order4 > order3) - self.assertTrue(order5 > order4) - self.assertTrue(order6 > order5) - self.assertTrue(order7 > order6) - self.assertTrue(order8 > order7) - self.assertTrue(order9 > order8) - self.assertTrue(order10 > order9) - self.assertTrue(order11 > order10) - self.assertTrue(order12 > order10) - - def test_ordering_importance_of_predicates(self): - from pyramid.config.util import predvalseq - - order1, _, _ = self._callFUT(xhr='xhr') - order2, _, _ = self._callFUT(request_method='request_method') - order3, _, _ = self._callFUT(path_info='path_info') - order4, _, _ = self._callFUT(request_param='param') - order5, _, _ = self._callFUT(header='header') - order6, _, _ = self._callFUT(accept='accept') - order7, _, _ = self._callFUT(containment='containment') - order8, _, _ = self._callFUT(request_type='request_type') - order9, _, _ = self._callFUT(match_param='foo=bar') - order10, _, _ = self._callFUT( - custom=predvalseq([DummyCustomPredicate()]) - ) - self.assertTrue(order1 > order2) - self.assertTrue(order2 > order3) - self.assertTrue(order3 > order4) - self.assertTrue(order4 > order5) - self.assertTrue(order5 > order6) - self.assertTrue(order6 > order7) - self.assertTrue(order7 > order8) - self.assertTrue(order8 > order9) - self.assertTrue(order9 > order10) - - def test_ordering_importance_and_number(self): - from pyramid.config.util import predvalseq - - order1, _, _ = self._callFUT( - xhr='xhr', request_method='request_method' - ) - order2, _, _ = self._callFUT( - custom=predvalseq([DummyCustomPredicate()]) - ) - self.assertTrue(order1 < order2) - - order1, _, _ = self._callFUT( - xhr='xhr', request_method='request_method' - ) - order2, _, _ = self._callFUT( - request_method='request_method', - custom=predvalseq([DummyCustomPredicate()]), - ) - self.assertTrue(order1 > order2) - - order1, _, _ = self._callFUT( - xhr='xhr', request_method='request_method', path_info='path_info' - ) - order2, _, _ = self._callFUT( - request_method='request_method', - custom=predvalseq([DummyCustomPredicate()]), - ) - self.assertTrue(order1 < order2) - - order1, _, _ = self._callFUT( - xhr='xhr', request_method='request_method', path_info='path_info' - ) - order2, _, _ = self._callFUT( - xhr='xhr', - request_method='request_method', - custom=predvalseq([DummyCustomPredicate()]), - ) - self.assertTrue(order1 > order2) - - def test_different_custom_predicates_with_same_hash(self): - from pyramid.config.util import predvalseq - - class PredicateWithHash(object): - def __hash__(self): - return 1 - - a = PredicateWithHash() - b = PredicateWithHash() - _, _, a_phash = self._callFUT(custom=predvalseq([a])) - _, _, b_phash = self._callFUT(custom=predvalseq([b])) - self.assertEqual(a_phash, b_phash) - - def test_traverse_has_remainder_already(self): - order, predicates, phash = self._callFUT(traverse='/1/:a/:b') - self.assertEqual(len(predicates), 1) - pred = predicates[0] - info = {'traverse': 'abc'} - request = DummyRequest() - result = pred(info, request) - self.assertEqual(result, True) - self.assertEqual(info, {'traverse': 'abc'}) - - def test_traverse_matches(self): - order, predicates, phash = self._callFUT(traverse='/1/:a/:b') - self.assertEqual(len(predicates), 1) - pred = predicates[0] - info = {'match': {'a': 'a', 'b': 'b'}} - request = DummyRequest() - result = pred(info, request) - self.assertEqual(result, True) - self.assertEqual( - info, {'match': {'a': 'a', 'b': 'b', 'traverse': ('1', 'a', 'b')}} - ) - - def test_traverse_matches_with_highorder_chars(self): - order, predicates, phash = self._callFUT( - traverse=text_(b'/La Pe\xc3\xb1a/{x}', 'utf-8') - ) - self.assertEqual(len(predicates), 1) - pred = predicates[0] - info = {'match': {'x': text_(b'Qu\xc3\xa9bec', 'utf-8')}} - request = DummyRequest() - result = pred(info, request) - self.assertEqual(result, True) - self.assertEqual( - info['match']['traverse'], - ( - text_(b'La Pe\xc3\xb1a', 'utf-8'), - text_(b'Qu\xc3\xa9bec', 'utf-8'), - ), - ) - - def test_custom_predicates_can_affect_traversal(self): - from pyramid.config.util import predvalseq - - def custom(info, request): - m = info['match'] - m['dummy'] = 'foo' - return True - - _, predicates, _ = self._callFUT( - custom=predvalseq([custom]), traverse='/1/:dummy/:a' - ) - self.assertEqual(len(predicates), 2) - info = {'match': {'a': 'a'}} - request = DummyRequest() - self.assertTrue(all([p(info, request) for p in predicates])) - self.assertEqual( - info, - { - 'match': { - 'a': 'a', - 'dummy': 'foo', - 'traverse': ('1', 'foo', 'a'), - } - }, - ) - - def test_predicate_text_is_correct(self): - from pyramid.config.util import predvalseq - - _, predicates, _ = self._callFUT( - xhr='xhr', - request_method='request_method', - path_info='path_info', - request_param='param', - header='header', - accept='accept', - containment='containment', - request_type='request_type', - custom=predvalseq( - [ - DummyCustomPredicate(), - DummyCustomPredicate.classmethod_predicate, - DummyCustomPredicate.classmethod_predicate_no_text, - ] - ), - match_param='foo=bar', - ) - self.assertEqual(predicates[0].text(), 'xhr = True') - self.assertEqual( - predicates[1].text(), "request_method = request_method" - ) - self.assertEqual(predicates[2].text(), 'path_info = path_info') - self.assertEqual(predicates[3].text(), 'request_param param') - self.assertEqual(predicates[4].text(), 'header header') - self.assertEqual(predicates[5].text(), 'accept = accept') - self.assertEqual(predicates[6].text(), 'containment = containment') - self.assertEqual(predicates[7].text(), 'request_type = request_type') - self.assertEqual(predicates[8].text(), "match_param foo=bar") - self.assertEqual(predicates[9].text(), 'custom predicate') - self.assertEqual(predicates[10].text(), 'classmethod predicate') - self.assertTrue(predicates[11].text().startswith('custom predicate')) - - def test_match_param_from_string(self): - _, predicates, _ = self._callFUT(match_param='foo=bar') - request = DummyRequest() - request.matchdict = {'foo': 'bar', 'baz': 'bum'} - self.assertTrue(predicates[0](Dummy(), request)) - - def test_match_param_from_string_fails(self): - _, predicates, _ = self._callFUT(match_param='foo=bar') - request = DummyRequest() - request.matchdict = {'foo': 'bum', 'baz': 'bum'} - self.assertFalse(predicates[0](Dummy(), request)) - - def test_match_param_from_dict(self): - _, predicates, _ = self._callFUT(match_param=('foo=bar', 'baz=bum')) - request = DummyRequest() - request.matchdict = {'foo': 'bar', 'baz': 'bum'} - self.assertTrue(predicates[0](Dummy(), request)) - - def test_match_param_from_dict_fails(self): - _, predicates, _ = self._callFUT(match_param=('foo=bar', 'baz=bum')) - request = DummyRequest() - request.matchdict = {'foo': 'bar', 'baz': 'foo'} - self.assertFalse(predicates[0](Dummy(), request)) - - def test_request_method_sequence(self): - _, predicates, _ = self._callFUT(request_method=('GET', 'HEAD')) - request = DummyRequest() - request.method = 'HEAD' - self.assertTrue(predicates[0](Dummy(), request)) - request.method = 'GET' - self.assertTrue(predicates[0](Dummy(), request)) - request.method = 'POST' - self.assertFalse(predicates[0](Dummy(), request)) - - def test_request_method_ordering_hashes_same(self): - hash1, _, __ = self._callFUT(request_method=('GET', 'HEAD')) - hash2, _, __ = self._callFUT(request_method=('HEAD', 'GET')) - self.assertEqual(hash1, hash2) - hash1, _, __ = self._callFUT(request_method=('GET',)) - hash2, _, __ = self._callFUT(request_method='GET') - self.assertEqual(hash1, hash2) - - def test_unknown_predicate(self): - from pyramid.exceptions import ConfigurationError - - self.assertRaises(ConfigurationError, self._callFUT, unknown=1) - - def test_predicate_close_matches(self): - from pyramid.exceptions import ConfigurationError - - with self.assertRaises(ConfigurationError) as context: - self._callFUT(method='GET') - expected_msg = ( - "Unknown predicate values: {'method': 'GET'} " - "(did you mean request_method)" - ) - self.assertEqual(context.exception.args[0], expected_msg) - - def test_notted(self): - from pyramid.config import not_ - from pyramid.testing import DummyRequest - - request = DummyRequest() - _, predicates, _ = self._callFUT( - xhr='xhr', request_method=not_('POST'), header=not_('header') - ) - self.assertEqual(predicates[0].text(), 'xhr = True') - self.assertEqual(predicates[1].text(), "!request_method = POST") - self.assertEqual(predicates[2].text(), '!header header') - self.assertEqual(predicates[1](None, request), True) - 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 - - return sort_accept_offers(offers, order) - - def test_default_specificities(self): - result = self._callFUT(['text/html', 'text/html;charset=utf8']) - self.assertEqual(result, ['text/html;charset=utf8', 'text/html']) - - def test_specific_type_order(self): - result = self._callFUT( - [ - 'text/html', - 'application/json', - 'text/html;charset=utf8', - 'text/plain', - ], - ['application/json', 'text/html'], - ) - self.assertEqual( - result, - [ - 'application/json', - 'text/html;charset=utf8', - 'text/html', - 'text/plain', - ], - ) - - def test_params_order(self): - result = self._callFUT( - [ - 'text/html;charset=utf8', - 'text/html;charset=latin1', - 'text/html;foo=bar', - ], - ['text/html;charset=latin1', 'text/html;charset=utf8'], - ) - self.assertEqual( - result, - [ - 'text/html;charset=latin1', - 'text/html;charset=utf8', - 'text/html;foo=bar', - ], - ) - - def test_params_inherit_type_prefs(self): - result = self._callFUT( - ['text/html;charset=utf8', 'text/plain;charset=latin1'], - ['text/plain', 'text/html'], - ) - self.assertEqual( - result, ['text/plain;charset=latin1', 'text/html;charset=utf8'] - ) - - -class DummyCustomPredicate(object): - def __init__(self): - self.__text__ = 'custom predicate' - - def classmethod_predicate(*args): # pragma: no cover - pass - - classmethod_predicate.__text__ = 'classmethod predicate' - classmethod_predicate = classmethod(classmethod_predicate) - - @classmethod - def classmethod_predicate_no_text(*args): - pass # pragma: no cover - - -class Dummy(object): - def __init__(self, **kw): - self.__dict__.update(**kw) - - -class DummyRequest: - subpath = () - matchdict = None - - def __init__(self, environ=None): - if environ is None: - environ = {} - self.environ = environ - self.params = {} - self.cookies = {} - - -class DummyConfigurator(object): - def maybe_dotted(self, thing): - return thing diff --git a/tests/test_config/test_views.py b/tests/test_config/test_views.py index 977944fdd..5e722c9a0 100644 --- a/tests/test_config/test_views.py +++ b/tests/test_config/test_views.py @@ -664,7 +664,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): 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 @@ class TestViewsConfigurationMixin(unittest.TestCase): 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 a1455ed92..f01cb490e 100644 --- a/tests/test_viewderivers.py +++ b/tests/test_viewderivers.py @@ -1265,7 +1265,7 @@ class TestDeriveView(unittest.TestCase): 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 -- cgit v1.2.3