_callers.py 5.9 KB

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