Michael Merickel
2018-10-15 a54bc1ccac17625991e26eb5d4577f893803c683
commit | author | age
52fde9 1 import functools
d8d3a9 2 from hashlib import md5
52fde9 3 import traceback
f2294f 4 from webob.acceptparse import Accept
52fde9 5 from zope.interface import implementer
5bf23f 6
0c29cf 7 from pyramid.compat import bytes_, is_nonstr_iter
52fde9 8 from pyramid.interfaces import IActionInfo
ee117e 9
5bf23f 10 from pyramid.exceptions import ConfigurationError
52fde9 11 from pyramid.predicates import Notted
d98612 12 from pyramid.registry import predvalseq
0c29cf 13 from pyramid.util import TopologicalSorter, takes_one_arg
d8d3a9 14
52fde9 15 TopologicalSorter = TopologicalSorter  # support bw-compat imports
MM 16 takes_one_arg = takes_one_arg  # support bw-compat imports
0c29cf 17
52fde9 18
MM 19 @implementer(IActionInfo)
20 class ActionInfo(object):
21     def __init__(self, file, line, function, src):
22         self.file = file
23         self.line = line
24         self.function = function
25         self.src = src
26
27     def __str__(self):
28         srclines = self.src.split('\n')
29         src = '\n'.join('    %s' % x for x in srclines)
30         return 'Line %s of file %s:\n%s' % (self.line, self.file, src)
31
0c29cf 32
52fde9 33 def action_method(wrapped):
MM 34     """ Wrapper to provide the right conflict info report data when a method
35     that calls Configurator.action calls another that does the same.  Not a
36     documented API but used by some external systems."""
0c29cf 37
52fde9 38     def wrapper(self, *arg, **kw):
MM 39         if self._ainfo is None:
40             self._ainfo = []
41         info = kw.pop('_info', None)
42         # backframes for outer decorators to actionmethods
43         backframes = kw.pop('_backframes', 0) + 2
44         if is_nonstr_iter(info) and len(info) == 4:
45             # _info permitted as extract_stack tuple
46             info = ActionInfo(*info)
47         if info is None:
48             try:
49                 f = traceback.extract_stack(limit=4)
50
51                 # Work around a Python 3.5 issue whereby it would insert an
52                 # extra stack frame. This should no longer be necessary in
53                 # Python 3.5.1
54                 last_frame = ActionInfo(*f[-1])
0c29cf 55                 if last_frame.function == 'extract_stack':  # pragma: no cover
52fde9 56                     f.pop()
MM 57                 info = ActionInfo(*f[-backframes])
0c29cf 58             except Exception:  # pragma: no cover
52fde9 59                 info = ActionInfo(None, 0, '', '')
MM 60         self._ainfo.append(info)
61         try:
62             result = wrapped(self, *arg, **kw)
63         finally:
64             self._ainfo.pop()
65         return result
66
67     if hasattr(wrapped, '__name__'):
68         functools.update_wrapper(wrapper, wrapped)
69     wrapper.__docobj__ = wrapped
70     return wrapper
71
5bf23f 72
CM 73 MAX_ORDER = 1 << 30
74 DEFAULT_PHASH = md5().hexdigest()
52fde9 75
561591 76
32333e 77 class not_(object):
49f7c3 78     """
CM 79
80     You can invert the meaning of any predicate value by wrapping it in a call
81     to :class:`pyramid.config.not_`.
82
83     .. code-block:: python
84        :linenos:
85
86        from pyramid.config import not_
87
88        config.add_view(
89            'mypackage.views.my_view',
08422e 90            route_name='ok',
49f7c3 91            request_method=not_('POST')
CM 92            )
93
94     The above example will ensure that the view is called if the request method
95     is *not* ``POST``, at least if no other view is more specific.
96
97     This technique of wrapping a predicate value in ``not_`` can be used
98     anywhere predicate values are accepted:
99
100     - :meth:`pyramid.config.Configurator.add_view`
101
102     - :meth:`pyramid.config.Configurator.add_route`
103
104     - :meth:`pyramid.config.Configurator.add_subscriber`
105
106     - :meth:`pyramid.view.view_config`
107
108     - :meth:`pyramid.events.subscriber`
109
110     .. versionadded:: 1.5
111     """
0c29cf 112
32333e 113     def __init__(self, value):
CM 114         self.value = value
115
116
fc3f23 117 # under = after
CM 118 # over = before
a00621 119
08422e 120
0c29cf 121 class PredicateList(object):
a00621 122     def __init__(self):
CM 123         self.sorter = TopologicalSorter()
9c8ec5 124         self.last_added = None
a00621 125
CM 126     def add(self, name, factory, weighs_more_than=None, weighs_less_than=None):
9c8ec5 127         # Predicates should be added to a predicate list in (presumed)
CM 128         # computation expense order.
a54bc1 129         # if weighs_more_than is None and weighs_less_than is None:
MM 130         #     weighs_more_than = self.last_added or FIRST
131         #     weighs_less_than = LAST
9c8ec5 132         self.last_added = name
0ccdc2 133         self.sorter.add(
0c29cf 134             name, factory, after=weighs_more_than, before=weighs_less_than
MM 135         )
a00621 136
51ee34 137     def names(self):
BJR 138         # Return the list of valid predicate names.
139         return self.sorter.names
140
9c8ec5 141     def make(self, config, **kw):
CM 142         # Given a configurator and a list of keywords, a predicate list is
143         # computed.  Elsewhere in the code, we evaluate predicates using a
144         # generator expression.  All predicates associated with a view or
145         # route must evaluate true for the view or route to "match" during a
146         # request.  The fastest predicate should be evaluated first, then the
147         # next fastest, and so on, as if one returns false, the remainder of
148         # the predicates won't need to be evaluated.
149         #
150         # While we compute predicates, we also compute a predicate hash (aka
151         # phash) that can be used by a caller to identify identical predicate
152         # lists.
a00621 153         ordered = self.sorter.sorted()
CM 154         phash = md5()
155         weights = []
9c8ec5 156         preds = []
CM 157         for n, (name, predicate_factory) in enumerate(ordered):
a00621 158             vals = kw.pop(name, None)
0c29cf 159             if vals is None:  # XXX should this be a sentinel other than None?
a00621 160                 continue
9c8ec5 161             if not isinstance(vals, predvalseq):
a00621 162                 vals = (vals,)
CM 163             for val in vals:
32333e 164                 realval = val
CM 165                 notted = False
166                 if isinstance(val, not_):
167                     realval = val.value
168                     notted = True
169                 pred = predicate_factory(realval, config)
170                 if notted:
171                     pred = Notted(pred)
9c8ec5 172                 hashes = pred.phash()
a00621 173                 if not is_nonstr_iter(hashes):
CM 174                     hashes = [hashes]
175                 for h in hashes:
176                     phash.update(bytes_(h))
25c64c 177                 weights.append(1 << n + 1)
9c8ec5 178                 preds.append(pred)
a00621 179         if kw:
96dd80 180             from difflib import get_close_matches
0c29cf 181
08422e 182             closest = []
0c29cf 183             names = [name for name, _ in ordered]
08422e 184             for name in kw:
FL 185                 closest.extend(get_close_matches(name, names, 3))
186
187             raise ConfigurationError(
188                 'Unknown predicate values: %r (did you mean %s)'
189                 % (kw, ','.join(closest))
190             )
9c8ec5 191         # A "order" is computed for the predicate list.  An order is
CM 192         # a scoring.
193         #
194         # Each predicate is associated with a weight value.  The weight of a
195         # predicate symbolizes the relative potential "importance" of the
196         # predicate to all other predicates.  A larger weight indicates
197         # greater importance.
198         #
199         # All weights for a given predicate list are bitwise ORed together
200         # to create a "score"; this score is then subtracted from
201         # MAX_ORDER and divided by an integer representing the number of
202         # predicates+1 to determine the order.
203         #
204         # For views, the order represents the ordering in which a "multiview"
205         # ( a collection of views that share the same context/request/name
206         # triad but differ in other ways via predicates) will attempt to call
207         # its set of views.  Views with lower orders will be tried first.
208         # The intent is to a) ensure that views with more predicates are
209         # always evaluated before views with fewer predicates and b) to
210         # ensure a stable call ordering of views that share the same number
211         # of predicates.  Views which do not have any predicates get an order
212         # of MAX_ORDER, meaning that they will be tried very last.
a00621 213         score = 0
CM 214         for bit in weights:
215             score = score | bit
9c8ec5 216         order = (MAX_ORDER - score) / (len(preds) + 1)
CM 217         return order, preds, phash.hexdigest()
f2294f 218
MM 219
19eef8 220 def normalize_accept_offer(offer, allow_range=False):
MM 221     if allow_range and '*' in offer:
222         return offer.lower()
223     return str(Accept.parse_offer(offer))
4a9f4f 224
MM 225
f2294f 226 def sort_accept_offers(offers, order=None):
MM 227     """
4a9f4f 228     Sort a list of offers by preference.
f2294f 229
4a9f4f 230     For a given ``type/subtype`` category of offers, this algorithm will
MM 231     always sort offers with params higher than the bare offer.
f2294f 232
MM 233     :param offers: A list of offers to be sorted.
234     :param order: A weighted list of offers where items closer to the start of
235                   the list will be a preferred over items closer to the end.
236     :return: A list of offers sorted first by specificity (higher to lower)
237              then by ``order``.
238
239     """
240     if order is None:
241         order = []
242
243     max_weight = len(offers)
244
245     def find_order_index(value, default=None):
246         return next((i for i, x in enumerate(order) if x == value), default)
247
248     def offer_sort_key(value):
249         """
4a9f4f 250         (type_weight, params_weight)
f2294f 251
MM 252         type_weight:
4a9f4f 253             - index of specific ``type/subtype`` in order list
f2294f 254             - ``max_weight * 2`` if no match is found
MM 255
256         params_weight:
257             - index of specific ``type/subtype;params`` in order list
258             - ``max_weight`` if not found
259             - ``max_weight + 1`` if no params at all
260
261         """
262         parsed = Accept.parse_offer(value)
263
4a9f4f 264         type_w = find_order_index(
0c29cf 265             parsed.type + '/' + parsed.subtype, max_weight
4a9f4f 266         )
f2294f 267
MM 268         if parsed.params:
269             param_w = find_order_index(value, max_weight)
270
271         else:
272             param_w = max_weight + 1
273
4a9f4f 274         return (type_w, param_w)
f2294f 275
MM 276     return sorted(offers, key=offer_sort_key)