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