Hide keyboard shortcuts

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 

7 

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 

17 

18 

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 

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 

156 

157 

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() 

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 

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 = {} 

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) 

353def 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 

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 

504 

505 

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 ) 

528 

529 

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 

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 

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.""" 

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