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