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