Skip to content

Commit 8c3f2f7

Browse files
RonnyPfannschmidtCursor AICursor Grok 4.5
committed
chore(structure): split hook modules into role-specific files
Move hook types, markers, callers, implementations, and multicall out of the monolithic _hooks/_callers modules so later typed-config and CompletionHook work can land without thrashing one huge file. Keep _hooks and _callers as re-export shims for import compatibility. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Cursor Grok 4.5 <grok@cursor.com>
1 parent 735edb0 commit 8c3f2f7

7 files changed

Lines changed: 1027 additions & 910 deletions

File tree

src/pluggy/_caller.py

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
"""
2+
Hook callers and relay.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from collections.abc import Callable
8+
from collections.abc import Mapping
9+
from collections.abc import Sequence
10+
from collections.abc import Set
11+
from typing import Any
12+
from typing import Final
13+
from typing import final
14+
from typing import TYPE_CHECKING
15+
from typing import TypeAlias
16+
import warnings
17+
18+
from ._config import HookimplOpts
19+
from ._config import HookspecOpts
20+
from ._decorators import _Namespace
21+
from ._decorators import HookSpec
22+
from ._implementation import _Plugin
23+
from ._implementation import HookImpl
24+
25+
26+
_HookExec: TypeAlias = Callable[
27+
[str, Sequence[HookImpl], Mapping[str, object], bool],
28+
object | list[object],
29+
]
30+
31+
32+
@final
33+
class HookRelay:
34+
"""Hook holder object for performing 1:N hook calls where N is the number
35+
of registered plugins."""
36+
37+
__slots__ = ("__dict__",)
38+
39+
def __init__(self) -> None:
40+
""":meta private:"""
41+
42+
if TYPE_CHECKING:
43+
44+
def __getattr__(self, name: str) -> HookCaller: ...
45+
46+
47+
# Historical name (pluggy<=1.2), kept for backward compatibility.
48+
_HookRelay = HookRelay
49+
50+
51+
_CallHistory: TypeAlias = list[
52+
tuple[Mapping[str, object], Callable[[Any], None] | None]
53+
]
54+
55+
56+
class HookCaller:
57+
"""A caller of all registered implementations of a hook specification."""
58+
59+
__slots__ = (
60+
"name",
61+
"spec",
62+
"_hookexec",
63+
"_hookimpls",
64+
"_call_history",
65+
)
66+
67+
def __init__(
68+
self,
69+
name: str,
70+
hook_execute: _HookExec,
71+
specmodule_or_class: _Namespace | None = None,
72+
spec_opts: HookspecOpts | None = None,
73+
) -> None:
74+
""":meta private:"""
75+
#: Name of the hook getting called.
76+
self.name: Final = name
77+
self._hookexec: Final = hook_execute
78+
# The hookimpls list. The caller iterates it *in reverse*. Format:
79+
# 1. trylast nonwrappers
80+
# 2. nonwrappers
81+
# 3. tryfirst nonwrappers
82+
# 4. trylast wrappers
83+
# 5. wrappers
84+
# 6. tryfirst wrappers
85+
self._hookimpls: Final[list[HookImpl]] = []
86+
self._call_history: _CallHistory | None = None
87+
# TODO: Document, or make private.
88+
self.spec: HookSpec | None = None
89+
if specmodule_or_class is not None:
90+
assert spec_opts is not None
91+
self.set_specification(specmodule_or_class, spec_opts)
92+
93+
# TODO: Document, or make private.
94+
def has_spec(self) -> bool:
95+
return self.spec is not None
96+
97+
# TODO: Document, or make private.
98+
def set_specification(
99+
self,
100+
specmodule_or_class: _Namespace,
101+
spec_opts: HookspecOpts,
102+
) -> None:
103+
if self.spec is not None:
104+
raise ValueError(
105+
f"Hook {self.spec.name!r} is already registered "
106+
f"within namespace {self.spec.namespace}"
107+
)
108+
self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
109+
if spec_opts.get("historic"):
110+
self._call_history = []
111+
112+
def is_historic(self) -> bool:
113+
"""Whether this caller is :ref:`historic <historic>`."""
114+
return self._call_history is not None
115+
116+
def _remove_plugin(self, plugin: _Plugin) -> None:
117+
"""Remove all hook implementations registered by the given plugin."""
118+
remaining = [impl for impl in self._hookimpls if impl.plugin != plugin]
119+
if len(remaining) == len(self._hookimpls):
120+
raise ValueError(f"plugin {plugin!r} not found")
121+
self._hookimpls[:] = remaining
122+
123+
def get_hookimpls(self) -> list[HookImpl]:
124+
"""Get all registered hook implementations for this hook."""
125+
return self._hookimpls.copy()
126+
127+
def _add_hookimpl(self, hookimpl: HookImpl) -> None:
128+
"""Add an implementation to the callback chain."""
129+
for i, method in enumerate(self._hookimpls):
130+
if method.hookwrapper or method.wrapper:
131+
splitpoint = i
132+
break
133+
else:
134+
splitpoint = len(self._hookimpls)
135+
if hookimpl.hookwrapper or hookimpl.wrapper:
136+
start, end = splitpoint, len(self._hookimpls)
137+
else:
138+
start, end = 0, splitpoint
139+
140+
if hookimpl.trylast:
141+
self._hookimpls.insert(start, hookimpl)
142+
elif hookimpl.tryfirst:
143+
self._hookimpls.insert(end, hookimpl)
144+
else:
145+
# find last non-tryfirst method
146+
i = end - 1
147+
while i >= start and self._hookimpls[i].tryfirst:
148+
i -= 1
149+
self._hookimpls.insert(i + 1, hookimpl)
150+
151+
def __repr__(self) -> str:
152+
return f"<HookCaller {self.name!r}>"
153+
154+
def _verify_all_args_are_provided(self, kwargs: Mapping[str, object]) -> None:
155+
# This is written to avoid expensive operations when not needed.
156+
if self.spec:
157+
for argname in self.spec.argnames:
158+
if argname not in kwargs:
159+
notincall = ", ".join(
160+
repr(argname)
161+
for argname in self.spec.argnames
162+
# Avoid self.spec.argnames - kwargs.keys()
163+
# it doesn't preserve order.
164+
if argname not in kwargs.keys()
165+
)
166+
warnings.warn(
167+
f"Argument(s) {notincall} which are declared in the hookspec "
168+
"cannot be found in this hook call",
169+
stacklevel=2,
170+
)
171+
break
172+
173+
def __call__(self, **kwargs: object) -> Any:
174+
"""Call the hook.
175+
176+
Only accepts keyword arguments, which should match the hook
177+
specification.
178+
179+
Returns the result(s) of calling all registered plugins, see
180+
:ref:`calling`.
181+
"""
182+
assert not self.is_historic(), (
183+
"Cannot directly call a historic hook - use call_historic instead."
184+
)
185+
self._verify_all_args_are_provided(kwargs)
186+
firstresult = self.spec.opts.get("firstresult", False) if self.spec else False
187+
# Copy because plugins may register other plugins during iteration (#438).
188+
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
189+
190+
def call_historic(
191+
self,
192+
result_callback: Callable[[Any], None] | None = None,
193+
kwargs: Mapping[str, object] | None = None,
194+
) -> None:
195+
"""Call the hook with given ``kwargs`` for all registered plugins and
196+
for all plugins which will be registered afterwards, see
197+
:ref:`historic`.
198+
199+
:param result_callback:
200+
If provided, will be called for each non-``None`` result obtained
201+
from a hook implementation.
202+
"""
203+
assert self._call_history is not None
204+
kwargs = kwargs or {}
205+
self._verify_all_args_are_provided(kwargs)
206+
self._call_history.append((kwargs, result_callback))
207+
# Historizing hooks don't return results.
208+
# Remember firstresult isn't compatible with historic.
209+
# Copy because plugins may register other plugins during iteration (#438).
210+
res = self._hookexec(self.name, self._hookimpls.copy(), kwargs, False)
211+
if result_callback is None:
212+
return
213+
if isinstance(res, list):
214+
for x in res:
215+
result_callback(x)
216+
217+
def call_extra(
218+
self, methods: Sequence[Callable[..., object]], kwargs: Mapping[str, object]
219+
) -> Any:
220+
"""Call the hook with some additional temporarily participating
221+
methods using the specified ``kwargs`` as call parameters, see
222+
:ref:`call_extra`."""
223+
assert not self.is_historic(), (
224+
"Cannot directly call a historic hook - use call_historic instead."
225+
)
226+
self._verify_all_args_are_provided(kwargs)
227+
opts: HookimplOpts = {
228+
"wrapper": False,
229+
"hookwrapper": False,
230+
"optionalhook": False,
231+
"trylast": False,
232+
"tryfirst": False,
233+
"specname": None,
234+
}
235+
hookimpls = self._hookimpls.copy()
236+
for method in methods:
237+
hookimpl = HookImpl(None, "<temp>", method, opts)
238+
# Find last non-tryfirst nonwrapper method.
239+
i = len(hookimpls) - 1
240+
while i >= 0 and (
241+
# Skip wrappers.
242+
(hookimpls[i].hookwrapper or hookimpls[i].wrapper)
243+
# Skip tryfirst nonwrappers.
244+
or hookimpls[i].tryfirst
245+
):
246+
i -= 1
247+
hookimpls.insert(i + 1, hookimpl)
248+
firstresult = self.spec.opts.get("firstresult", False) if self.spec else False
249+
return self._hookexec(self.name, hookimpls, kwargs, firstresult)
250+
251+
def _maybe_apply_history(self, method: HookImpl) -> None:
252+
"""Apply call history to a new hookimpl if it is marked as historic."""
253+
if self.is_historic():
254+
assert self._call_history is not None
255+
for kwargs, result_callback in self._call_history:
256+
res = self._hookexec(self.name, [method], kwargs, False)
257+
if res and result_callback is not None:
258+
# XXX: remember firstresult isn't compat with historic
259+
assert isinstance(res, list)
260+
result_callback(res[0])
261+
262+
263+
# Historical name (pluggy<=1.2), kept for backward compatibility.
264+
_HookCaller = HookCaller
265+
266+
267+
class _SubsetHookCaller(HookCaller):
268+
"""A proxy to another HookCaller which manages calls to all registered
269+
plugins except the ones from remove_plugins."""
270+
271+
# This class is unusual: in inhertits from `HookCaller` so all of
272+
# the *code* runs in the class, but it delegates all underlying *data*
273+
# to the original HookCaller.
274+
# `subset_hook_caller` used to be implemented by creating a full-fledged
275+
# HookCaller, copying all hookimpls from the original. This had problems
276+
# with memory leaks (#346) and historic calls (#347), which make a proxy
277+
# approach better.
278+
# An alternative implementation is to use a `_getattr__`/`__getattribute__`
279+
# proxy, however that adds more overhead and is more tricky to implement.
280+
281+
__slots__ = (
282+
"_orig",
283+
"_remove_plugins",
284+
)
285+
286+
def __init__(self, orig: HookCaller, remove_plugins: Set[_Plugin]) -> None:
287+
self._orig = orig
288+
self._remove_plugins = remove_plugins
289+
self.name = orig.name # type: ignore[misc]
290+
self._hookexec = orig._hookexec # type: ignore[misc]
291+
292+
@property # type: ignore[misc]
293+
def _hookimpls(self) -> list[HookImpl]:
294+
return [
295+
impl
296+
for impl in self._orig._hookimpls
297+
if impl.plugin not in self._remove_plugins
298+
]
299+
300+
@property
301+
def spec(self) -> HookSpec | None: # type: ignore[override]
302+
return self._orig.spec
303+
304+
@property
305+
def _call_history(self) -> _CallHistory | None: # type: ignore[override]
306+
return self._orig._call_history
307+
308+
def __repr__(self) -> str:
309+
return f"<_SubsetHookCaller {self.name!r}>"

0 commit comments

Comments
 (0)