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