170 lines
5.9 KiB
Python
170 lines
5.9 KiB
Python
![]() |
"""
|
||
|
Call loop machinery
|
||
|
"""
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
from collections.abc import Generator
|
||
|
from collections.abc import Mapping
|
||
|
from collections.abc import Sequence
|
||
|
from typing import cast
|
||
|
from typing import NoReturn
|
||
|
import warnings
|
||
|
|
||
|
from ._hooks import HookImpl
|
||
|
from ._result import HookCallError
|
||
|
from ._result import Result
|
||
|
from ._warnings import PluggyTeardownRaisedWarning
|
||
|
|
||
|
|
||
|
# Need to distinguish between old- and new-style hook wrappers.
|
||
|
# Wrapping with a tuple is the fastest type-safe way I found to do it.
|
||
|
Teardown = Generator[None, object, object]
|
||
|
|
||
|
|
||
|
def run_old_style_hookwrapper(
|
||
|
hook_impl: HookImpl, hook_name: str, args: Sequence[object]
|
||
|
) -> Teardown:
|
||
|
"""
|
||
|
backward compatibility wrapper to run a old style hookwrapper as a wrapper
|
||
|
"""
|
||
|
|
||
|
teardown: Teardown = cast(Teardown, hook_impl.function(*args))
|
||
|
try:
|
||
|
next(teardown)
|
||
|
except StopIteration:
|
||
|
_raise_wrapfail(teardown, "did not yield")
|
||
|
try:
|
||
|
res = yield
|
||
|
result = Result(res, None)
|
||
|
except BaseException as exc:
|
||
|
result = Result(None, exc)
|
||
|
try:
|
||
|
teardown.send(result)
|
||
|
except StopIteration:
|
||
|
pass
|
||
|
except BaseException as e:
|
||
|
_warn_teardown_exception(hook_name, hook_impl, e)
|
||
|
raise
|
||
|
else:
|
||
|
_raise_wrapfail(teardown, "has second yield")
|
||
|
finally:
|
||
|
teardown.close()
|
||
|
return result.get_result()
|
||
|
|
||
|
|
||
|
def _raise_wrapfail(
|
||
|
wrap_controller: Generator[None, object, object],
|
||
|
msg: str,
|
||
|
) -> NoReturn:
|
||
|
co = wrap_controller.gi_code # type: ignore[attr-defined]
|
||
|
raise RuntimeError(
|
||
|
f"wrap_controller at {co.co_name!r} {co.co_filename}:{co.co_firstlineno} {msg}"
|
||
|
)
|
||
|
|
||
|
|
||
|
def _warn_teardown_exception(
|
||
|
hook_name: str, hook_impl: HookImpl, e: BaseException
|
||
|
) -> None:
|
||
|
msg = "A plugin raised an exception during an old-style hookwrapper teardown.\n"
|
||
|
msg += f"Plugin: {hook_impl.plugin_name}, Hook: {hook_name}\n"
|
||
|
msg += f"{type(e).__name__}: {e}\n"
|
||
|
msg += "For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning" # noqa: E501
|
||
|
warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=6)
|
||
|
|
||
|
|
||
|
def _multicall(
|
||
|
hook_name: str,
|
||
|
hook_impls: Sequence[HookImpl],
|
||
|
caller_kwargs: Mapping[str, object],
|
||
|
firstresult: bool,
|
||
|
) -> object | list[object]:
|
||
|
"""Execute a call into multiple python functions/methods and return the
|
||
|
result(s).
|
||
|
|
||
|
``caller_kwargs`` comes from HookCaller.__call__().
|
||
|
"""
|
||
|
__tracebackhide__ = True
|
||
|
results: list[object] = []
|
||
|
exception = None
|
||
|
try: # run impl and wrapper setup functions in a loop
|
||
|
teardowns: list[Teardown] = []
|
||
|
try:
|
||
|
for hook_impl in reversed(hook_impls):
|
||
|
try:
|
||
|
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
|
||
|
except KeyError as e:
|
||
|
# coverage bug - this is tested
|
||
|
for argname in hook_impl.argnames: # pragma: no cover
|
||
|
if argname not in caller_kwargs:
|
||
|
raise HookCallError(
|
||
|
f"hook call must provide argument {argname!r}"
|
||
|
) from e
|
||
|
|
||
|
if hook_impl.hookwrapper:
|
||
|
function_gen = run_old_style_hookwrapper(hook_impl, hook_name, args)
|
||
|
|
||
|
next(function_gen) # first yield
|
||
|
teardowns.append(function_gen)
|
||
|
|
||
|
elif hook_impl.wrapper:
|
||
|
try:
|
||
|
# If this cast is not valid, a type error is raised below,
|
||
|
# which is the desired response.
|
||
|
res = hook_impl.function(*args)
|
||
|
function_gen = cast(Generator[None, object, object], res)
|
||
|
next(function_gen) # first yield
|
||
|
teardowns.append(function_gen)
|
||
|
except StopIteration:
|
||
|
_raise_wrapfail(function_gen, "did not yield")
|
||
|
else:
|
||
|
res = hook_impl.function(*args)
|
||
|
if res is not None:
|
||
|
results.append(res)
|
||
|
if firstresult: # halt further impl calls
|
||
|
break
|
||
|
except BaseException as exc:
|
||
|
exception = exc
|
||
|
finally:
|
||
|
if firstresult: # first result hooks return a single value
|
||
|
result = results[0] if results else None
|
||
|
else:
|
||
|
result = results
|
||
|
|
||
|
# run all wrapper post-yield blocks
|
||
|
for teardown in reversed(teardowns):
|
||
|
try:
|
||
|
if exception is not None:
|
||
|
try:
|
||
|
teardown.throw(exception)
|
||
|
except RuntimeError as re:
|
||
|
# StopIteration from generator causes RuntimeError
|
||
|
# even for coroutine usage - see #544
|
||
|
if (
|
||
|
isinstance(exception, StopIteration)
|
||
|
and re.__cause__ is exception
|
||
|
):
|
||
|
teardown.close()
|
||
|
continue
|
||
|
else:
|
||
|
raise
|
||
|
else:
|
||
|
teardown.send(result)
|
||
|
# Following is unreachable for a well behaved hook wrapper.
|
||
|
# Try to force finalizers otherwise postponed till GC action.
|
||
|
# Note: close() may raise if generator handles GeneratorExit.
|
||
|
teardown.close()
|
||
|
except StopIteration as si:
|
||
|
result = si.value
|
||
|
exception = None
|
||
|
continue
|
||
|
except BaseException as e:
|
||
|
exception = e
|
||
|
continue
|
||
|
_raise_wrapfail(teardown, "has second yield")
|
||
|
|
||
|
if exception is not None:
|
||
|
raise exception
|
||
|
else:
|
||
|
return result
|