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