From b18141489d98ff34211e3a36c369b1adec1a1255 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 20 Dec 2025 12:39:08 +0200 Subject: [PATCH 1/3] callers: simplify "hook call must provide argument" error handling This also makes it easier to see that `args` can't end up unbound. --- src/pluggy/_callers.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pluggy/_callers.py b/src/pluggy/_callers.py index 6c54ce61..6f8462dd 100644 --- a/src/pluggy/_callers.py +++ b/src/pluggy/_callers.py @@ -97,12 +97,9 @@ def _multicall( 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 + raise HookCallError( + f"hook call must provide argument {e.args[0]!r}" + ) from e if hook_impl.hookwrapper: function_gen = run_old_style_hookwrapper(hook_impl, hook_name, args) From 387c7422d4724133085ec8f7aa6f3e7a44a40410 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 20 Dec 2025 12:49:47 +0200 Subject: [PATCH 2/3] callers: remove useless try Replaces #277. The extra try has no effect. --- src/pluggy/_callers.py | 59 +++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/pluggy/_callers.py b/src/pluggy/_callers.py index 6f8462dd..64eb2ac8 100644 --- a/src/pluggy/_callers.py +++ b/src/pluggy/_callers.py @@ -90,41 +90,40 @@ def _multicall( __tracebackhide__ = True results: list[object] = [] exception = None + teardowns: list[Teardown] = [] 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: - raise HookCallError( - f"hook call must provide argument {e.args[0]!r}" - ) from e + for hook_impl in reversed(hook_impls): + try: + args = [caller_kwargs[argname] for argname in hook_impl.argnames] + except KeyError as e: + raise HookCallError( + f"hook call must provide argument {e.args[0]!r}" + ) from e - if hook_impl.hookwrapper: - function_gen = run_old_style_hookwrapper(hook_impl, hook_name, args) + if hook_impl.hookwrapper: + function_gen = run_old_style_hookwrapper(hook_impl, hook_name, args) - next(function_gen) # first yield - teardowns.append(function_gen) + 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: + 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) - if res is not None: - results.append(res) - if firstresult: # halt further impl calls - break - except BaseException as exc: - exception = exc + 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 From 12880913bf3114f84a5af63b3f6a5ac5c5a6eb7b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 20 Dec 2025 16:19:16 +0200 Subject: [PATCH 3/3] callers: avoid `cast` function call in runtime It actually adds measurable overhead here -- ~15% according to testing/benchmark.py (on Python 3.14). --- src/pluggy/_callers.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/pluggy/_callers.py b/src/pluggy/_callers.py index 64eb2ac8..450db1a7 100644 --- a/src/pluggy/_callers.py +++ b/src/pluggy/_callers.py @@ -9,6 +9,7 @@ from collections.abc import Sequence from typing import cast from typing import NoReturn +from typing import TYPE_CHECKING from typing import TypeAlias import warnings @@ -29,8 +30,10 @@ def run_old_style_hookwrapper( """ backward compatibility wrapper to run a old style hookwrapper as a wrapper """ - - teardown: Teardown = cast(Teardown, hook_impl.function(*args)) + if TYPE_CHECKING: + teardown = cast(Teardown, hook_impl.function(*args)) + else: + teardown = hook_impl.function(*args) try: next(teardown) except StopIteration: @@ -107,15 +110,18 @@ def _multicall( 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) + res = hook_impl.function(*args) + # If this cast is not valid, a type error is raised below, + # which is the desired response. + if TYPE_CHECKING: function_gen = cast(Generator[None, object, object], res) + else: + function_gen = res + try: next(function_gen) # first yield - teardowns.append(function_gen) except StopIteration: _raise_wrapfail(function_gen, "did not yield") + teardowns.append(function_gen) else: res = hook_impl.function(*args) if res is not None: