summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/pyramid/config/__init__.py45
-rw-r--r--src/pyramid/config/actions.py64
-rw-r--r--src/pyramid/config/adapters.py2
-rw-r--r--src/pyramid/config/assets.py2
-rw-r--r--src/pyramid/config/factories.py2
-rw-r--r--src/pyramid/config/i18n.py2
-rw-r--r--src/pyramid/config/predicates.py257
-rw-r--r--src/pyramid/config/rendering.py2
-rw-r--r--src/pyramid/config/routes.py10
-rw-r--r--src/pyramid/config/security.py2
-rw-r--r--src/pyramid/config/testing.py2
-rw-r--r--src/pyramid/config/tweens.py2
-rw-r--r--src/pyramid/config/util.py276
-rw-r--r--src/pyramid/config/views.py4
-rw-r--r--tests/test_config/test_actions.py37
-rw-r--r--tests/test_config/test_predicates.py (renamed from tests/test_config/test_util.py)62
-rw-r--r--tests/test_config/test_views.py4
-rw-r--r--tests/test_viewderivers.py2
18 files changed, 379 insertions, 398 deletions
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_util.py b/tests/test_config/test_predicates.py
index 50d143b2d..079652b39 100644
--- a/tests/test_config/test_util.py
+++ b/tests/test_config/test_predicates.py
@@ -3,44 +3,9 @@ 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.config.predicates import PredicateList
from pyramid import predicates
inst = PredicateList()
@@ -71,7 +36,7 @@ class TestPredicateList(unittest.TestCase):
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 @@ class TestPredicateList(unittest.TestCase):
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 @@ class TestPredicateList(unittest.TestCase):
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 @@ class TestPredicateList(unittest.TestCase):
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 @@ class TestPredicateList(unittest.TestCase):
)
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 @@ class TestPredicateList(unittest.TestCase):
)
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 @@ class TestPredicateList(unittest.TestCase):
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_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