Michael Merickel
2018-10-18 e4c0570d5c67ddf0ad9502169b59475ba0784d82
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     )