Skip to content

Commit 2bd6b5b

Browse files
Separate normal and wrapper hook implementations with distinct types
This change introduces type-safe separation between normal hook implementations and wrapper implementations by: - Creating WrapperImpl subclass for wrapper/hookwrapper implementations - Modifying _multicall to handle normal and wrapper implementations separately - Updating hook callers to maintain separate lists and enforce type constraints - Adding factory method in HookimplConfiguration for proper type instantiation - Ensuring historic hooks reject wrapper implementations at registration time The separation improves type safety, clarifies execution flow, and maintains backward compatibility while providing better error messages for misuse. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 7912b11 commit 2bd6b5b

File tree

2 files changed

+13
-7
lines changed

2 files changed

+13
-7
lines changed

src/pluggy/_callers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from ._hook_callers import HookImpl
1515
from ._hook_callers import WrapperImpl
16+
from ._hook_callers import WrapperImpl
1617
from ._result import Result
1718
from ._warnings import PluggyTeardownRaisedWarning
1819

src/pluggy/_hook_callers.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ def _insert_hookimpl_into_list(
6060
target_list.insert(i + 1, hookimpl)
6161

6262

63-
def _insert_hookimpl_into_list(hookimpl: HookImpl, target_list: list[HookImpl]) -> None:
63+
_T_HookImpl = TypeVar("_T_HookImpl", bound="HookImpl")
64+
65+
66+
def _insert_hookimpl_into_list(
67+
hookimpl: _T_HookImpl, target_list: MutableSequence[_T_HookImpl]
68+
) -> None:
6469
"""Insert a hookimpl into the target list maintaining proper ordering.
6570
6671
The ordering is: [trylast, normal, tryfirst]
@@ -549,9 +554,11 @@ def __call__(self, **kwargs: object) -> Any:
549554
if self.spec:
550555
self.spec.verify_all_args_are_provided(kwargs)
551556
firstresult = self.spec.config.firstresult if self.spec else False
552-
# Get the hookexec from the original
553557
hookexec = getattr(self._orig, "_hookexec")
554-
return hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
558+
559+
normal_impls = self._get_filtered(self._orig._normal_hookimpls)
560+
wrapper_impls = self._get_filtered(self._orig._wrapper_hookimpls)
561+
return hookexec(self.name, normal_impls, wrapper_impls, kwargs, firstresult)
555562

556563
normal_impls = self._get_filtered(self._orig._normal_hookimpls)
557564
wrapper_impls = self._get_filtered(self._orig._wrapper_hookimpls)
@@ -576,9 +583,7 @@ def call_historic(
576583
if self.spec:
577584
self.spec.verify_all_args_are_provided(kwargs)
578585

579-
# If the original is a HistoricHookCaller, add to its history
580-
if hasattr(self._orig, "_call_history"):
581-
self._orig._call_history.append((kwargs, result_callback))
586+
self._orig._call_history.append((kwargs, result_callback))
582587

583588
# Execute with filtered hookimpls (historic hooks don't support wrappers)
584589
hookexec = getattr(self._orig, "_hookexec")
@@ -631,7 +636,6 @@ def __repr__(self) -> str:
631636
_SubsetHookCaller = SubsetHookCaller
632637

633638

634-
@final
635639
class HookImpl:
636640
"""Base class for hook implementations in a :class:`HookCaller`."""
637641

@@ -648,6 +652,7 @@ class HookImpl:
648652
"trylast",
649653
"hookimpl_config",
650654
)
655+
651656
function: Final[_HookImplFunction[object]]
652657
argnames: Final[tuple[str, ...]]
653658
kwargnames: Final[tuple[str, ...]]

0 commit comments

Comments
 (0)