Michael Merickel
2018-10-19 d579f2104de139e0b0fc5d6c81aabb2f826e5e54
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