commit | author | age
|
d579f2
|
1 |
import functools |
e4c057
|
2 |
import itertools |
MM |
3 |
import operator |
|
4 |
import sys |
d579f2
|
5 |
import traceback |
MM |
6 |
from zope.interface import implementer |
e4c057
|
7 |
|
MM |
8 |
from pyramid.compat import reraise |
|
9 |
from pyramid.exceptions import ( |
|
10 |
ConfigurationConflictError, |
|
11 |
ConfigurationError, |
|
12 |
ConfigurationExecutionError, |
|
13 |
) |
d579f2
|
14 |
from pyramid.interfaces import IActionInfo |
e4c057
|
15 |
from pyramid.registry import undefer |
d579f2
|
16 |
from pyramid.util import is_nonstr_iter |
e4c057
|
17 |
|
MM |
18 |
|
|
19 |
class ActionConfiguratorMixin(object): |
|
20 |
@property |
|
21 |
def action_info(self): |
|
22 |
info = self.info # usually a ZCML action (ParserInfo) if self.info |
|
23 |
if not info: |
|
24 |
# Try to provide more accurate info for conflict reports |
|
25 |
if self._ainfo: |
|
26 |
info = self._ainfo[0] |
|
27 |
else: |
|
28 |
info = ActionInfo(None, 0, '', '') |
|
29 |
return info |
|
30 |
|
|
31 |
def action( |
|
32 |
self, |
|
33 |
discriminator, |
|
34 |
callable=None, |
|
35 |
args=(), |
|
36 |
kw=None, |
|
37 |
order=0, |
|
38 |
introspectables=(), |
|
39 |
**extra |
|
40 |
): |
|
41 |
""" Register an action which will be executed when |
|
42 |
:meth:`pyramid.config.Configurator.commit` is called (or executed |
|
43 |
immediately if ``autocommit`` is ``True``). |
|
44 |
|
|
45 |
.. warning:: This method is typically only used by :app:`Pyramid` |
|
46 |
framework extension authors, not by :app:`Pyramid` application |
|
47 |
developers. |
|
48 |
|
|
49 |
The ``discriminator`` uniquely identifies the action. It must be |
|
50 |
given, but it can be ``None``, to indicate that the action never |
|
51 |
conflicts. It must be a hashable value. |
|
52 |
|
|
53 |
The ``callable`` is a callable object which performs the task |
|
54 |
associated with the action when the action is executed. It is |
|
55 |
optional. |
|
56 |
|
|
57 |
``args`` and ``kw`` are tuple and dict objects respectively, which |
|
58 |
are passed to ``callable`` when this action is executed. Both are |
|
59 |
optional. |
|
60 |
|
|
61 |
``order`` is a grouping mechanism; an action with a lower order will |
|
62 |
be executed before an action with a higher order (has no effect when |
|
63 |
autocommit is ``True``). |
|
64 |
|
|
65 |
``introspectables`` is a sequence of :term:`introspectable` objects |
|
66 |
(or the empty sequence if no introspectable objects are associated |
|
67 |
with this action). If this configurator's ``introspection`` |
|
68 |
attribute is ``False``, these introspectables will be ignored. |
|
69 |
|
|
70 |
``extra`` provides a facility for inserting extra keys and values |
|
71 |
into an action dictionary. |
|
72 |
""" |
|
73 |
# catch nonhashable discriminators here; most unit tests use |
|
74 |
# autocommit=False, which won't catch unhashable discriminators |
|
75 |
assert hash(discriminator) |
|
76 |
|
|
77 |
if kw is None: |
|
78 |
kw = {} |
|
79 |
|
|
80 |
autocommit = self.autocommit |
|
81 |
action_info = self.action_info |
|
82 |
|
|
83 |
if not self.introspection: |
|
84 |
# if we're not introspecting, ignore any introspectables passed |
|
85 |
# to us |
|
86 |
introspectables = () |
|
87 |
|
|
88 |
if autocommit: |
|
89 |
# callables can depend on the side effects of resolving a |
|
90 |
# deferred discriminator |
|
91 |
self.begin() |
|
92 |
try: |
|
93 |
undefer(discriminator) |
|
94 |
if callable is not None: |
|
95 |
callable(*args, **kw) |
|
96 |
for introspectable in introspectables: |
|
97 |
introspectable.register(self.introspector, action_info) |
|
98 |
finally: |
|
99 |
self.end() |
|
100 |
|
|
101 |
else: |
|
102 |
action = extra |
|
103 |
action.update( |
|
104 |
dict( |
|
105 |
discriminator=discriminator, |
|
106 |
callable=callable, |
|
107 |
args=args, |
|
108 |
kw=kw, |
|
109 |
order=order, |
|
110 |
info=action_info, |
|
111 |
includepath=self.includepath, |
|
112 |
introspectables=introspectables, |
|
113 |
) |
|
114 |
) |
|
115 |
self.action_state.action(**action) |
|
116 |
|
|
117 |
def _get_action_state(self): |
|
118 |
registry = self.registry |
|
119 |
try: |
|
120 |
state = registry.action_state |
|
121 |
except AttributeError: |
|
122 |
state = ActionState() |
|
123 |
registry.action_state = state |
|
124 |
return state |
|
125 |
|
|
126 |
def _set_action_state(self, state): |
|
127 |
self.registry.action_state = state |
|
128 |
|
|
129 |
action_state = property(_get_action_state, _set_action_state) |
|
130 |
|
|
131 |
_ctx = action_state # bw compat |
|
132 |
|
|
133 |
def commit(self): |
|
134 |
""" |
|
135 |
Commit any pending configuration actions. If a configuration |
|
136 |
conflict is detected in the pending configuration actions, this method |
|
137 |
will raise a :exc:`ConfigurationConflictError`; within the traceback |
|
138 |
of this error will be information about the source of the conflict, |
|
139 |
usually including file names and line numbers of the cause of the |
|
140 |
configuration conflicts. |
|
141 |
|
|
142 |
.. warning:: |
|
143 |
You should think very carefully before manually invoking |
|
144 |
``commit()``. Especially not as part of any reusable configuration |
|
145 |
methods. Normally it should only be done by an application author at |
|
146 |
the end of configuration in order to override certain aspects of an |
|
147 |
addon. |
|
148 |
|
|
149 |
""" |
|
150 |
self.begin() |
|
151 |
try: |
|
152 |
self.action_state.execute_actions(introspector=self.introspector) |
|
153 |
finally: |
|
154 |
self.end() |
|
155 |
self.action_state = ActionState() # old actions have been processed |
d579f2
|
156 |
|
e4c057
|
157 |
|
MM |
158 |
# this class is licensed under the ZPL (stolen from Zope) |
|
159 |
class ActionState(object): |
|
160 |
def __init__(self): |
|
161 |
# NB "actions" is an API, dep'd upon by pyramid_zcml's load_zcml func |
|
162 |
self.actions = [] |
|
163 |
self._seen_files = set() |
|
164 |
|
|
165 |
def processSpec(self, spec): |
|
166 |
"""Check whether a callable needs to be processed. The ``spec`` |
|
167 |
refers to a unique identifier for the callable. |
|
168 |
|
|
169 |
Return True if processing is needed and False otherwise. If |
|
170 |
the callable needs to be processed, it will be marked as |
|
171 |
processed, assuming that the caller will procces the callable if |
|
172 |
it needs to be processed. |
|
173 |
""" |
|
174 |
if spec in self._seen_files: |
|
175 |
return False |
|
176 |
self._seen_files.add(spec) |
|
177 |
return True |
|
178 |
|
|
179 |
def action( |
|
180 |
self, |
|
181 |
discriminator, |
|
182 |
callable=None, |
|
183 |
args=(), |
|
184 |
kw=None, |
|
185 |
order=0, |
|
186 |
includepath=(), |
|
187 |
info=None, |
|
188 |
introspectables=(), |
|
189 |
**extra |
|
190 |
): |
|
191 |
"""Add an action with the given discriminator, callable and arguments |
|
192 |
""" |
|
193 |
if kw is None: |
|
194 |
kw = {} |
|
195 |
action = extra |
|
196 |
action.update( |
|
197 |
dict( |
|
198 |
discriminator=discriminator, |
|
199 |
callable=callable, |
|
200 |
args=args, |
|
201 |
kw=kw, |
|
202 |
includepath=includepath, |
|
203 |
info=info, |
|
204 |
order=order, |
|
205 |
introspectables=introspectables, |
|
206 |
) |
|
207 |
) |
|
208 |
self.actions.append(action) |
|
209 |
|
|
210 |
def execute_actions(self, clear=True, introspector=None): |
|
211 |
"""Execute the configuration actions |
|
212 |
|
|
213 |
This calls the action callables after resolving conflicts |
|
214 |
|
|
215 |
For example: |
|
216 |
|
|
217 |
>>> output = [] |
|
218 |
>>> def f(*a, **k): |
|
219 |
... output.append(('f', a, k)) |
|
220 |
>>> context = ActionState() |
|
221 |
>>> context.actions = [ |
|
222 |
... (1, f, (1,)), |
|
223 |
... (1, f, (11,), {}, ('x', )), |
|
224 |
... (2, f, (2,)), |
|
225 |
... ] |
|
226 |
>>> context.execute_actions() |
|
227 |
>>> output |
|
228 |
[('f', (1,), {}), ('f', (2,), {})] |
|
229 |
|
|
230 |
If the action raises an error, we convert it to a |
|
231 |
ConfigurationExecutionError. |
|
232 |
|
|
233 |
>>> output = [] |
|
234 |
>>> def bad(): |
|
235 |
... bad.xxx |
|
236 |
>>> context.actions = [ |
|
237 |
... (1, f, (1,)), |
|
238 |
... (1, f, (11,), {}, ('x', )), |
|
239 |
... (2, f, (2,)), |
|
240 |
... (3, bad, (), {}, (), 'oops') |
|
241 |
... ] |
|
242 |
>>> try: |
|
243 |
... v = context.execute_actions() |
|
244 |
... except ConfigurationExecutionError, v: |
|
245 |
... pass |
|
246 |
>>> print(v) |
|
247 |
exceptions.AttributeError: 'function' object has no attribute 'xxx' |
|
248 |
in: |
|
249 |
oops |
|
250 |
|
|
251 |
Note that actions executed before the error still have an effect: |
|
252 |
|
|
253 |
>>> output |
|
254 |
[('f', (1,), {}), ('f', (2,), {})] |
|
255 |
|
|
256 |
The execution is re-entrant such that actions may be added by other |
|
257 |
actions with the one caveat that the order of any added actions must |
|
258 |
be equal to or larger than the current action. |
|
259 |
|
|
260 |
>>> output = [] |
|
261 |
>>> def f(*a, **k): |
|
262 |
... output.append(('f', a, k)) |
|
263 |
... context.actions.append((3, g, (8,), {})) |
|
264 |
>>> def g(*a, **k): |
|
265 |
... output.append(('g', a, k)) |
|
266 |
>>> context.actions = [ |
|
267 |
... (1, f, (1,)), |
|
268 |
... ] |
|
269 |
>>> context.execute_actions() |
|
270 |
>>> output |
|
271 |
[('f', (1,), {}), ('g', (8,), {})] |
|
272 |
|
|
273 |
""" |
|
274 |
try: |
|
275 |
all_actions = [] |
|
276 |
executed_actions = [] |
|
277 |
action_iter = iter([]) |
|
278 |
conflict_state = ConflictResolverState() |
|
279 |
|
|
280 |
while True: |
|
281 |
# We clear the actions list prior to execution so if there |
|
282 |
# are some new actions then we add them to the mix and resolve |
|
283 |
# conflicts again. This orders the new actions as well as |
|
284 |
# ensures that the previously executed actions have no new |
|
285 |
# conflicts. |
|
286 |
if self.actions: |
|
287 |
all_actions.extend(self.actions) |
|
288 |
action_iter = resolveConflicts( |
|
289 |
self.actions, state=conflict_state |
|
290 |
) |
|
291 |
self.actions = [] |
|
292 |
|
|
293 |
action = next(action_iter, None) |
|
294 |
if action is None: |
|
295 |
# we are done! |
|
296 |
break |
|
297 |
|
|
298 |
callable = action['callable'] |
|
299 |
args = action['args'] |
|
300 |
kw = action['kw'] |
|
301 |
info = action['info'] |
|
302 |
# we use "get" below in case an action was added via a ZCML |
|
303 |
# directive that did not know about introspectables |
|
304 |
introspectables = action.get('introspectables', ()) |
|
305 |
|
|
306 |
try: |
|
307 |
if callable is not None: |
|
308 |
callable(*args, **kw) |
|
309 |
except Exception: |
|
310 |
t, v, tb = sys.exc_info() |
|
311 |
try: |
|
312 |
reraise( |
|
313 |
ConfigurationExecutionError, |
|
314 |
ConfigurationExecutionError(t, v, info), |
|
315 |
tb, |
|
316 |
) |
|
317 |
finally: |
|
318 |
del t, v, tb |
|
319 |
|
|
320 |
if introspector is not None: |
|
321 |
for introspectable in introspectables: |
|
322 |
introspectable.register(introspector, info) |
|
323 |
|
|
324 |
executed_actions.append(action) |
|
325 |
|
|
326 |
self.actions = all_actions |
|
327 |
return executed_actions |
|
328 |
|
|
329 |
finally: |
|
330 |
if clear: |
|
331 |
self.actions = [] |
|
332 |
|
|
333 |
|
|
334 |
class ConflictResolverState(object): |
|
335 |
def __init__(self): |
|
336 |
# keep a set of resolved discriminators to test against to ensure |
|
337 |
# that a new action does not conflict with something already executed |
|
338 |
self.resolved_ainfos = {} |
|
339 |
|
|
340 |
# actions left over from a previous iteration |
|
341 |
self.remaining_actions = [] |
|
342 |
|
|
343 |
# after executing an action we memoize its order to avoid any new |
|
344 |
# actions sending us backward |
|
345 |
self.min_order = None |
|
346 |
|
|
347 |
# unique tracks the index of the action so we need it to increase |
|
348 |
# monotonically across invocations to resolveConflicts |
|
349 |
self.start = 0 |
|
350 |
|
|
351 |
|
|
352 |
# this function is licensed under the ZPL (stolen from Zope) |
|
353 |
def resolveConflicts(actions, state=None): |
|
354 |
"""Resolve conflicting actions |
|
355 |
|
|
356 |
Given an actions list, identify and try to resolve conflicting actions. |
|
357 |
Actions conflict if they have the same non-None discriminator. |
|
358 |
|
|
359 |
Conflicting actions can be resolved if the include path of one of |
|
360 |
the actions is a prefix of the includepaths of the other |
|
361 |
conflicting actions and is unequal to the include paths in the |
|
362 |
other conflicting actions. |
|
363 |
|
|
364 |
Actions are resolved on a per-order basis because some discriminators |
|
365 |
cannot be computed until earlier actions have executed. An action in an |
|
366 |
earlier order may execute successfully only to find out later that it was |
|
367 |
overridden by another action with a smaller include path. This will result |
|
368 |
in a conflict as there is no way to revert the original action. |
|
369 |
|
|
370 |
``state`` may be an instance of ``ConflictResolverState`` that |
|
371 |
can be used to resume execution and resolve the new actions against the |
|
372 |
list of executed actions from a previous call. |
|
373 |
|
|
374 |
""" |
|
375 |
if state is None: |
|
376 |
state = ConflictResolverState() |
|
377 |
|
|
378 |
# pick up where we left off last time, but track the new actions as well |
|
379 |
state.remaining_actions.extend(normalize_actions(actions)) |
|
380 |
actions = state.remaining_actions |
|
381 |
|
|
382 |
def orderandpos(v): |
|
383 |
n, v = v |
|
384 |
return (v['order'] or 0, n) |
|
385 |
|
|
386 |
def orderonly(v): |
|
387 |
n, v = v |
|
388 |
return v['order'] or 0 |
|
389 |
|
|
390 |
sactions = sorted(enumerate(actions, start=state.start), key=orderandpos) |
|
391 |
for order, actiongroup in itertools.groupby(sactions, orderonly): |
|
392 |
# "order" is an integer grouping. Actions in a lower order will be |
|
393 |
# executed before actions in a higher order. All of the actions in |
|
394 |
# one grouping will be executed (its callable, if any will be called) |
|
395 |
# before any of the actions in the next. |
|
396 |
output = [] |
|
397 |
unique = {} |
|
398 |
|
|
399 |
# error out if we went backward in order |
|
400 |
if state.min_order is not None and order < state.min_order: |
|
401 |
r = [ |
|
402 |
'Actions were added to order={0} after execution had moved ' |
|
403 |
'on to order={1}. Conflicting actions: '.format( |
|
404 |
order, state.min_order |
|
405 |
) |
|
406 |
] |
|
407 |
for i, action in actiongroup: |
|
408 |
for line in str(action['info']).rstrip().split('\n'): |
|
409 |
r.append(" " + line) |
|
410 |
raise ConfigurationError('\n'.join(r)) |
|
411 |
|
|
412 |
for i, action in actiongroup: |
|
413 |
# Within an order, actions are executed sequentially based on |
|
414 |
# original action ordering ("i"). |
|
415 |
|
|
416 |
# "ainfo" is a tuple of (i, action) where "i" is an integer |
|
417 |
# expressing the relative position of this action in the action |
|
418 |
# list being resolved, and "action" is an action dictionary. The |
|
419 |
# purpose of an ainfo is to associate an "i" with a particular |
|
420 |
# action; "i" exists for sorting after conflict resolution. |
|
421 |
ainfo = (i, action) |
|
422 |
|
|
423 |
# wait to defer discriminators until we are on their order because |
|
424 |
# the discriminator may depend on state from a previous order |
|
425 |
discriminator = undefer(action['discriminator']) |
|
426 |
action['discriminator'] = discriminator |
|
427 |
|
|
428 |
if discriminator is None: |
|
429 |
# The discriminator is None, so this action can never conflict. |
|
430 |
# We can add it directly to the result. |
|
431 |
output.append(ainfo) |
|
432 |
continue |
|
433 |
|
|
434 |
L = unique.setdefault(discriminator, []) |
|
435 |
L.append(ainfo) |
|
436 |
|
|
437 |
# Check for conflicts |
|
438 |
conflicts = {} |
|
439 |
for discriminator, ainfos in unique.items(): |
|
440 |
# We use (includepath, i) as a sort key because we need to |
|
441 |
# sort the actions by the paths so that the shortest path with a |
|
442 |
# given prefix comes first. The "first" action is the one with the |
|
443 |
# shortest include path. We break sorting ties using "i". |
|
444 |
def bypath(ainfo): |
|
445 |
path, i = ainfo[1]['includepath'], ainfo[0] |
|
446 |
return path, order, i |
|
447 |
|
|
448 |
ainfos.sort(key=bypath) |
|
449 |
ainfo, rest = ainfos[0], ainfos[1:] |
|
450 |
_, action = ainfo |
|
451 |
|
|
452 |
# ensure this new action does not conflict with a previously |
|
453 |
# resolved action from an earlier order / invocation |
|
454 |
prev_ainfo = state.resolved_ainfos.get(discriminator) |
|
455 |
if prev_ainfo is not None: |
|
456 |
_, paction = prev_ainfo |
|
457 |
basepath, baseinfo = paction['includepath'], paction['info'] |
|
458 |
includepath = action['includepath'] |
|
459 |
# if the new action conflicts with the resolved action then |
|
460 |
# note the conflict, otherwise drop the action as it's |
|
461 |
# effectively overriden by the previous action |
|
462 |
if ( |
|
463 |
includepath[: len(basepath)] != basepath |
|
464 |
or includepath == basepath |
|
465 |
): |
|
466 |
L = conflicts.setdefault(discriminator, [baseinfo]) |
|
467 |
L.append(action['info']) |
|
468 |
|
|
469 |
else: |
|
470 |
output.append(ainfo) |
|
471 |
|
|
472 |
basepath, baseinfo = action['includepath'], action['info'] |
|
473 |
for _, action in rest: |
|
474 |
includepath = action['includepath'] |
|
475 |
# Test whether path is a prefix of opath |
|
476 |
if ( |
|
477 |
includepath[: len(basepath)] != basepath |
|
478 |
or includepath == basepath # not a prefix |
|
479 |
): |
|
480 |
L = conflicts.setdefault(discriminator, [baseinfo]) |
|
481 |
L.append(action['info']) |
|
482 |
|
|
483 |
if conflicts: |
|
484 |
raise ConfigurationConflictError(conflicts) |
|
485 |
|
|
486 |
# sort resolved actions by "i" and yield them one by one |
|
487 |
for i, action in sorted(output, key=operator.itemgetter(0)): |
|
488 |
# do not memoize the order until we resolve an action inside it |
|
489 |
state.min_order = action['order'] |
|
490 |
state.start = i + 1 |
|
491 |
state.remaining_actions.remove(action) |
|
492 |
state.resolved_ainfos[action['discriminator']] = (i, action) |
|
493 |
yield action |
|
494 |
|
|
495 |
|
|
496 |
def normalize_actions(actions): |
|
497 |
"""Convert old-style tuple actions to new-style dicts.""" |
|
498 |
result = [] |
|
499 |
for v in actions: |
|
500 |
if not isinstance(v, dict): |
|
501 |
v = expand_action_tuple(*v) |
|
502 |
result.append(v) |
|
503 |
return result |
|
504 |
|
|
505 |
|
|
506 |
def expand_action_tuple( |
|
507 |
discriminator, |
|
508 |
callable=None, |
|
509 |
args=(), |
|
510 |
kw=None, |
|
511 |
includepath=(), |
|
512 |
info=None, |
|
513 |
order=0, |
|
514 |
introspectables=(), |
|
515 |
): |
|
516 |
if kw is None: |
|
517 |
kw = {} |
|
518 |
return dict( |
|
519 |
discriminator=discriminator, |
|
520 |
callable=callable, |
|
521 |
args=args, |
|
522 |
kw=kw, |
|
523 |
includepath=includepath, |
|
524 |
info=info, |
|
525 |
order=order, |
|
526 |
introspectables=introspectables, |
|
527 |
) |
d579f2
|
528 |
|
MM |
529 |
|
|
530 |
@implementer(IActionInfo) |
|
531 |
class ActionInfo(object): |
|
532 |
def __init__(self, file, line, function, src): |
|
533 |
self.file = file |
|
534 |
self.line = line |
|
535 |
self.function = function |
|
536 |
self.src = src |
|
537 |
|
|
538 |
def __str__(self): |
|
539 |
srclines = self.src.split('\n') |
|
540 |
src = '\n'.join(' %s' % x for x in srclines) |
|
541 |
return 'Line %s of file %s:\n%s' % (self.line, self.file, src) |
|
542 |
|
|
543 |
|
|
544 |
def action_method(wrapped): |
|
545 |
""" Wrapper to provide the right conflict info report data when a method |
|
546 |
that calls Configurator.action calls another that does the same. Not a |
|
547 |
documented API but used by some external systems.""" |
|
548 |
|
|
549 |
def wrapper(self, *arg, **kw): |
|
550 |
if self._ainfo is None: |
|
551 |
self._ainfo = [] |
|
552 |
info = kw.pop('_info', None) |
|
553 |
# backframes for outer decorators to actionmethods |
|
554 |
backframes = kw.pop('_backframes', 0) + 2 |
|
555 |
if is_nonstr_iter(info) and len(info) == 4: |
|
556 |
# _info permitted as extract_stack tuple |
|
557 |
info = ActionInfo(*info) |
|
558 |
if info is None: |
|
559 |
try: |
|
560 |
f = traceback.extract_stack(limit=4) |
|
561 |
|
|
562 |
# Work around a Python 3.5 issue whereby it would insert an |
|
563 |
# extra stack frame. This should no longer be necessary in |
|
564 |
# Python 3.5.1 |
|
565 |
last_frame = ActionInfo(*f[-1]) |
|
566 |
if last_frame.function == 'extract_stack': # pragma: no cover |
|
567 |
f.pop() |
|
568 |
info = ActionInfo(*f[-backframes]) |
|
569 |
except Exception: # pragma: no cover |
|
570 |
info = ActionInfo(None, 0, '', '') |
|
571 |
self._ainfo.append(info) |
|
572 |
try: |
|
573 |
result = wrapped(self, *arg, **kw) |
|
574 |
finally: |
|
575 |
self._ainfo.pop() |
|
576 |
return result |
|
577 |
|
|
578 |
if hasattr(wrapped, '__name__'): |
|
579 |
functools.update_wrapper(wrapper, wrapped) |
|
580 |
wrapper.__docobj__ = wrapped |
|
581 |
return wrapper |