From 4149922e64aecf2a213f8efb120cd2d61fed3eb7 Mon Sep 17 00:00:00 2001 From: Michael Merickel <github@m.merickel.org> Date: Fri, 26 Oct 2018 01:08:57 +0200 Subject: [PATCH] Merge pull request #3397 from mmerickel/refactor-actions --- src/pyramid/config/predicates.py | 257 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 255 insertions(+), 2 deletions(-) diff --git a/src/pyramid/config/predicates.py b/src/pyramid/config/predicates.py index cdbf68c..8f16f74 100644 --- a/src/pyramid/config/predicates.py +++ b/src/pyramid/config/predicates.py @@ -1,3 +1,256 @@ -import zope.deprecation +from hashlib import md5 +from webob.acceptparse import Accept -zope.deprecation.moved('pyramid.predicates', 'Pyramid 1.10') +from pyramid.compat import bytes_, is_nonstr_iter +from pyramid.exceptions import ConfigurationError +from pyramid.interfaces import IPredicateList, PHASE1_CONFIG +from pyramid.predicates import Notted +from pyramid.registry import predvalseq +from pyramid.util import TopologicalSorter + + +MAX_ORDER = 1 << 30 +DEFAULT_PHASH = md5().hexdigest() + + +class PredicateConfiguratorMixin(object): + def get_predlist(self, name): + predlist = self.registry.queryUtility(IPredicateList, name=name) + if predlist is None: + predlist = PredicateList() + self.registry.registerUtility(predlist, IPredicateList, name=name) + return predlist + + def _add_predicate( + self, type, name, factory, weighs_more_than=None, weighs_less_than=None + ): + factory = self.maybe_dotted(factory) + discriminator = ('%s option' % type, name) + intr = self.introspectable( + '%s predicates' % type, + discriminator, + '%s predicate named %s' % (type, name), + '%s predicate' % type, + ) + intr['name'] = name + intr['factory'] = factory + intr['weighs_more_than'] = weighs_more_than + intr['weighs_less_than'] = weighs_less_than + + def register(): + predlist = self.get_predlist(type) + predlist.add( + name, + factory, + weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than, + ) + + self.action( + discriminator, + register, + introspectables=(intr,), + order=PHASE1_CONFIG, + ) # must be registered early + + +class not_(object): + """ + + You can invert the meaning of any predicate value by wrapping it in a call + to :class:`pyramid.config.not_`. + + .. code-block:: python + :linenos: + + from pyramid.config import not_ + + config.add_view( + 'mypackage.views.my_view', + route_name='ok', + request_method=not_('POST') + ) + + The above example will ensure that the view is called if the request method + is *not* ``POST``, at least if no other view is more specific. + + This technique of wrapping a predicate value in ``not_`` can be used + anywhere predicate values are accepted: + + - :meth:`pyramid.config.Configurator.add_view` + + - :meth:`pyramid.config.Configurator.add_route` + + - :meth:`pyramid.config.Configurator.add_subscriber` + + - :meth:`pyramid.view.view_config` + + - :meth:`pyramid.events.subscriber` + + .. versionadded:: 1.5 + """ + + def __init__(self, value): + self.value = value + + +# under = after +# over = before + + +class PredicateList(object): + def __init__(self): + self.sorter = TopologicalSorter() + self.last_added = None + + def add(self, name, factory, weighs_more_than=None, weighs_less_than=None): + # Predicates should be added to a predicate list in (presumed) + # computation expense order. + # if weighs_more_than is None and weighs_less_than is None: + # weighs_more_than = self.last_added or FIRST + # weighs_less_than = LAST + self.last_added = name + self.sorter.add( + name, factory, after=weighs_more_than, before=weighs_less_than + ) + + def names(self): + # Return the list of valid predicate names. + return self.sorter.names + + def make(self, config, **kw): + # Given a configurator and a list of keywords, a predicate list is + # computed. Elsewhere in the code, we evaluate predicates using a + # generator expression. All predicates associated with a view or + # route must evaluate true for the view or route to "match" during a + # request. The fastest predicate should be evaluated first, then the + # next fastest, and so on, as if one returns false, the remainder of + # the predicates won't need to be evaluated. + # + # While we compute predicates, we also compute a predicate hash (aka + # phash) that can be used by a caller to identify identical predicate + # lists. + ordered = self.sorter.sorted() + phash = md5() + weights = [] + preds = [] + for n, (name, predicate_factory) in enumerate(ordered): + vals = kw.pop(name, None) + if vals is None: # XXX should this be a sentinel other than None? + continue + if not isinstance(vals, predvalseq): + vals = (vals,) + for val in vals: + realval = val + notted = False + if isinstance(val, not_): + realval = val.value + notted = True + pred = predicate_factory(realval, config) + if notted: + pred = Notted(pred) + hashes = pred.phash() + if not is_nonstr_iter(hashes): + hashes = [hashes] + for h in hashes: + phash.update(bytes_(h)) + weights.append(1 << n + 1) + preds.append(pred) + if kw: + from difflib import get_close_matches + + closest = [] + names = [name for name, _ in ordered] + for name in kw: + closest.extend(get_close_matches(name, names, 3)) + + raise ConfigurationError( + 'Unknown predicate values: %r (did you mean %s)' + % (kw, ','.join(closest)) + ) + # A "order" is computed for the predicate list. An order is + # a scoring. + # + # Each predicate is associated with a weight value. The weight of a + # predicate symbolizes the relative potential "importance" of the + # predicate to all other predicates. A larger weight indicates + # greater importance. + # + # All weights for a given predicate list are bitwise ORed together + # to create a "score"; this score is then subtracted from + # MAX_ORDER and divided by an integer representing the number of + # predicates+1 to determine the order. + # + # For views, the order represents the ordering in which a "multiview" + # ( a collection of views that share the same context/request/name + # triad but differ in other ways via predicates) will attempt to call + # its set of views. Views with lower orders will be tried first. + # The intent is to a) ensure that views with more predicates are + # always evaluated before views with fewer predicates and b) to + # ensure a stable call ordering of views that share the same number + # of predicates. Views which do not have any predicates get an order + # of MAX_ORDER, meaning that they will be tried very last. + score = 0 + for bit in weights: + score = score | bit + order = (MAX_ORDER - score) / (len(preds) + 1) + return order, preds, phash.hexdigest() + + +def normalize_accept_offer(offer, allow_range=False): + if allow_range and '*' in offer: + return offer.lower() + return str(Accept.parse_offer(offer)) + + +def sort_accept_offers(offers, order=None): + """ + Sort a list of offers by preference. + + For a given ``type/subtype`` category of offers, this algorithm will + always sort offers with params higher than the bare offer. + + :param offers: A list of offers to be sorted. + :param order: A weighted list of offers where items closer to the start of + the list will be a preferred over items closer to the end. + :return: A list of offers sorted first by specificity (higher to lower) + then by ``order``. + + """ + if order is None: + order = [] + + max_weight = len(offers) + + def find_order_index(value, default=None): + return next((i for i, x in enumerate(order) if x == value), default) + + def offer_sort_key(value): + """ + (type_weight, params_weight) + + type_weight: + - index of specific ``type/subtype`` in order list + - ``max_weight * 2`` if no match is found + + params_weight: + - index of specific ``type/subtype;params`` in order list + - ``max_weight`` if not found + - ``max_weight + 1`` if no params at all + + """ + parsed = Accept.parse_offer(value) + + type_w = find_order_index( + parsed.type + '/' + parsed.subtype, max_weight + ) + + if parsed.params: + param_w = find_order_index(value, max_weight) + + else: + param_w = max_weight + 1 + + return (type_w, param_w) + + return sorted(offers, key=offer_sort_key) -- Gitblit v1.9.3