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) |