Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/pyramid/config/predicates.py : 76%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from hashlib import md5
2from webob.acceptparse import Accept
4from pyramid.compat import bytes_, is_nonstr_iter
5from pyramid.exceptions import ConfigurationError
6from pyramid.interfaces import IPredicateList, PHASE1_CONFIG
7from pyramid.predicates import Notted
8from pyramid.registry import predvalseq
9from pyramid.util import TopologicalSorter
12MAX_ORDER = 1 << 30
13DEFAULT_PHASH = md5().hexdigest()
16class 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
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
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 )
49 self.action(
50 discriminator,
51 register,
52 introspectables=(intr,),
53 order=PHASE1_CONFIG,
54 ) # must be registered early
57class not_(object):
58 """
60 You can invert the meaning of any predicate value by wrapping it in a call
61 to :class:`pyramid.config.not_`.
63 .. code-block:: python
64 :linenos:
66 from pyramid.config import not_
68 config.add_view(
69 'mypackage.views.my_view',
70 route_name='ok',
71 request_method=not_('POST')
72 )
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.
77 This technique of wrapping a predicate value in ``not_`` can be used
78 anywhere predicate values are accepted:
80 - :meth:`pyramid.config.Configurator.add_view`
82 - :meth:`pyramid.config.Configurator.add_route`
84 - :meth:`pyramid.config.Configurator.add_subscriber`
86 - :meth:`pyramid.view.view_config`
88 - :meth:`pyramid.events.subscriber`
90 .. versionadded:: 1.5
91 """
93 def __init__(self, value):
94 self.value = value
97# under = after
98# over = before
101class PredicateList(object):
102 def __init__(self):
103 self.sorter = TopologicalSorter()
104 self.last_added = None
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 )
117 def names(self):
118 # Return the list of valid predicate names.
119 return self.sorter.names
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
162 closest = []
163 names = [name for name, _ in ordered]
164 for name in kw:
165 closest.extend(get_close_matches(name, names, 3))
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()
200def 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))
206def sort_accept_offers(offers, order=None):
207 """
208 Sort a list of offers by preference.
210 For a given ``type/subtype`` category of offers, this algorithm will
211 always sort offers with params higher than the bare offer.
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``.
219 """
220 if order is None:
221 order = []
223 max_weight = len(offers)
225 def find_order_index(value, default=None):
226 return next((i for i, x in enumerate(order) if x == value), default)
228 def offer_sort_key(value):
229 """
230 (type_weight, params_weight)
232 type_weight:
233 - index of specific ``type/subtype`` in order list
234 - ``max_weight * 2`` if no match is found
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
241 """
242 parsed = Accept.parse_offer(value)
244 type_w = find_order_index(
245 parsed.type + '/' + parsed.subtype, max_weight
246 )
248 if parsed.params:
249 param_w = find_order_index(value, max_weight)
251 else:
252 param_w = max_weight + 1
254 return (type_w, param_w)
256 return sorted(offers, key=offer_sort_key)