_callers.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. """
  2. Call loop machinery
  3. """
  4. from __future__ import annotations
  5. from typing import cast
  6. from typing import Generator
  7. from typing import Mapping
  8. from typing import NoReturn
  9. from typing import Sequence
  10. from typing import Tuple
  11. from typing import Union
  12. import warnings
  13. from ._hooks import HookImpl
  14. from ._result import HookCallError
  15. from ._result import Result
  16. from ._warnings import PluggyTeardownRaisedWarning
  17. # Need to distinguish between old- and new-style hook wrappers.
  18. # Wrapping with a tuple is the fastest type-safe way I found to do it.
  19. Teardown = Union[
  20. Tuple[Generator[None, Result[object], None], HookImpl],
  21. Generator[None, object, object],
  22. ]
  23. def _raise_wrapfail(
  24. wrap_controller: (
  25. Generator[None, Result[object], None] | Generator[None, object, object]
  26. ),
  27. msg: str,
  28. ) -> NoReturn:
  29. co = wrap_controller.gi_code
  30. raise RuntimeError(
  31. "wrap_controller at %r %s:%d %s"
  32. % (co.co_name, co.co_filename, co.co_firstlineno, msg)
  33. )
  34. def _warn_teardown_exception(
  35. hook_name: str, hook_impl: HookImpl, e: BaseException
  36. ) -> None:
  37. msg = "A plugin raised an exception during an old-style hookwrapper teardown.\n"
  38. msg += f"Plugin: {hook_impl.plugin_name}, Hook: {hook_name}\n"
  39. msg += f"{type(e).__name__}: {e}\n"
  40. msg += "For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning" # noqa: E501
  41. warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=5)
  42. def _multicall(
  43. hook_name: str,
  44. hook_impls: Sequence[HookImpl],
  45. caller_kwargs: Mapping[str, object],
  46. firstresult: bool,
  47. ) -> object | list[object]:
  48. """Execute a call into multiple python functions/methods and return the
  49. result(s).
  50. ``caller_kwargs`` comes from HookCaller.__call__().
  51. """
  52. __tracebackhide__ = True
  53. results: list[object] = []
  54. exception = None
  55. only_new_style_wrappers = True
  56. try: # run impl and wrapper setup functions in a loop
  57. teardowns: list[Teardown] = []
  58. try:
  59. for hook_impl in reversed(hook_impls):
  60. try:
  61. args = [caller_kwargs[argname] for argname in hook_impl.argnames]
  62. except KeyError:
  63. for argname in hook_impl.argnames:
  64. if argname not in caller_kwargs:
  65. raise HookCallError(
  66. f"hook call must provide argument {argname!r}"
  67. )
  68. if hook_impl.hookwrapper:
  69. only_new_style_wrappers = False
  70. try:
  71. # If this cast is not valid, a type error is raised below,
  72. # which is the desired response.
  73. res = hook_impl.function(*args)
  74. wrapper_gen = cast(Generator[None, Result[object], None], res)
  75. next(wrapper_gen) # first yield
  76. teardowns.append((wrapper_gen, hook_impl))
  77. except StopIteration:
  78. _raise_wrapfail(wrapper_gen, "did not yield")
  79. elif hook_impl.wrapper:
  80. try:
  81. # If this cast is not valid, a type error is raised below,
  82. # which is the desired response.
  83. res = hook_impl.function(*args)
  84. function_gen = cast(Generator[None, object, object], res)
  85. next(function_gen) # first yield
  86. teardowns.append(function_gen)
  87. except StopIteration:
  88. _raise_wrapfail(function_gen, "did not yield")
  89. else:
  90. res = hook_impl.function(*args)
  91. if res is not None:
  92. results.append(res)
  93. if firstresult: # halt further impl calls
  94. break
  95. except BaseException as exc:
  96. exception = exc
  97. finally:
  98. # Fast path - only new-style wrappers, no Result.
  99. if only_new_style_wrappers:
  100. if firstresult: # first result hooks return a single value
  101. result = results[0] if results else None
  102. else:
  103. result = results
  104. # run all wrapper post-yield blocks
  105. for teardown in reversed(teardowns):
  106. try:
  107. if exception is not None:
  108. teardown.throw(exception) # type: ignore[union-attr]
  109. else:
  110. teardown.send(result) # type: ignore[union-attr]
  111. # Following is unreachable for a well behaved hook wrapper.
  112. # Try to force finalizers otherwise postponed till GC action.
  113. # Note: close() may raise if generator handles GeneratorExit.
  114. teardown.close() # type: ignore[union-attr]
  115. except StopIteration as si:
  116. result = si.value
  117. exception = None
  118. continue
  119. except BaseException as e:
  120. exception = e
  121. continue
  122. _raise_wrapfail(teardown, "has second yield") # type: ignore[arg-type]
  123. if exception is not None:
  124. raise exception.with_traceback(exception.__traceback__)
  125. else:
  126. return result
  127. # Slow path - need to support old-style wrappers.
  128. else:
  129. if firstresult: # first result hooks return a single value
  130. outcome: Result[object | list[object]] = Result(
  131. results[0] if results else None, exception
  132. )
  133. else:
  134. outcome = Result(results, exception)
  135. # run all wrapper post-yield blocks
  136. for teardown in reversed(teardowns):
  137. if isinstance(teardown, tuple):
  138. try:
  139. teardown[0].send(outcome)
  140. except StopIteration:
  141. pass
  142. except BaseException as e:
  143. _warn_teardown_exception(hook_name, teardown[1], e)
  144. raise
  145. else:
  146. _raise_wrapfail(teardown[0], "has second yield")
  147. else:
  148. try:
  149. if outcome._exception is not None:
  150. teardown.throw(outcome._exception)
  151. else:
  152. teardown.send(outcome._result)
  153. # Following is unreachable for a well behaved hook wrapper.
  154. # Try to force finalizers otherwise postponed till GC action.
  155. # Note: close() may raise if generator handles GeneratorExit.
  156. teardown.close()
  157. except StopIteration as si:
  158. outcome.force_result(si.value)
  159. continue
  160. except BaseException as e:
  161. outcome.force_exception(e)
  162. continue
  163. _raise_wrapfail(teardown, "has second yield")
  164. return outcome.get_result()