Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/pyramid/config/actions.py : 80%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import functools
2import itertools
3import operator
4import sys
5import traceback
6from zope.interface import implementer
8from pyramid.compat import reraise
9from pyramid.exceptions import (
10 ConfigurationConflictError,
11 ConfigurationError,
12 ConfigurationExecutionError,
13)
14from pyramid.interfaces import IActionInfo
15from pyramid.registry import undefer
16from pyramid.util import is_nonstr_iter
19class 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
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``).
45 .. warning:: This method is typically only used by :app:`Pyramid`
46 framework extension authors, not by :app:`Pyramid` application
47 developers.
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.
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.
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.
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``).
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.
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)
77 if kw is None:
78 kw = {}
80 autocommit = self.autocommit
81 action_info = self.action_info
83 if not self.introspection:
84 # if we're not introspecting, ignore any introspectables passed
85 # to us
86 introspectables = ()
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()
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)
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
126 def _set_action_state(self, state):
127 self.registry.action_state = state
129 action_state = property(_get_action_state, _set_action_state)
131 _ctx = action_state # bw compat
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.
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.
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
158# this class is licensed under the ZPL (stolen from Zope)
159class 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()
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.
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
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)
210 def execute_actions(self, clear=True, introspector=None):
211 """Execute the configuration actions
213 This calls the action callables after resolving conflicts
215 For example:
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,), {})]
230 If the action raises an error, we convert it to a
231 ConfigurationExecutionError.
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
251 Note that actions executed before the error still have an effect:
253 >>> output
254 [('f', (1,), {}), ('f', (2,), {})]
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.
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,), {})]
273 """
274 try:
275 all_actions = []
276 executed_actions = []
277 action_iter = iter([])
278 conflict_state = ConflictResolverState()
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 = []
293 action = next(action_iter, None)
294 if action is None:
295 # we are done!
296 break
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', ())
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
320 if introspector is not None:
321 for introspectable in introspectables:
322 introspectable.register(introspector, info)
324 executed_actions.append(action)
326 self.actions = all_actions
327 return executed_actions
329 finally:
330 if clear:
331 self.actions = []
334class 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 = {}
340 # actions left over from a previous iteration
341 self.remaining_actions = []
343 # after executing an action we memoize its order to avoid any new
344 # actions sending us backward
345 self.min_order = None
347 # unique tracks the index of the action so we need it to increase
348 # monotonically across invocations to resolveConflicts
349 self.start = 0
352# this function is licensed under the ZPL (stolen from Zope)
353def resolveConflicts(actions, state=None):
354 """Resolve conflicting actions
356 Given an actions list, identify and try to resolve conflicting actions.
357 Actions conflict if they have the same non-None discriminator.
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.
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.
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.
374 """
375 if state is None:
376 state = ConflictResolverState()
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
382 def orderandpos(v):
383 n, v = v
384 return (v['order'] or 0, n)
386 def orderonly(v):
387 n, v = v
388 return v['order'] or 0
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 = {}
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))
412 for i, action in actiongroup:
413 # Within an order, actions are executed sequentially based on
414 # original action ordering ("i").
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)
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
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
434 L = unique.setdefault(discriminator, [])
435 L.append(ainfo)
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
448 ainfos.sort(key=bypath)
449 ainfo, rest = ainfos[0], ainfos[1:]
450 _, action = ainfo
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'])
469 else:
470 output.append(ainfo)
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'])
483 if conflicts:
484 raise ConfigurationConflictError(conflicts)
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
496def 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
506def 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 )
530@implementer(IActionInfo)
531class 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
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)
544def 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."""
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)
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
578 if hasattr(wrapped, '__name__'):
579 functools.update_wrapper(wrapper, wrapped)
580 wrapper.__docobj__ = wrapped
581 return wrapper