66
77from collections .abc import Callable
88from collections .abc import Generator
9+ from collections .abc import Mapping
10+ from typing import cast
911from typing import Final
1012from typing import final
13+ from typing import Protocol
14+ from typing import runtime_checkable
1115from typing import TypeAlias
1216from typing import TypeVar
1317
1418from ._config import HookimplConfiguration
1519from ._decorators import varnames
20+ from ._result import HookCallError
1621from ._result import Result
1722
1823
2227_HookImplFunction : TypeAlias = Callable [..., _T | Generator [None , Result [_T ], None ]]
2328
2429
25- @final
30+ @runtime_checkable
31+ class CompletionHook (Protocol ):
32+ """Teardown callback returned by :meth:`WrapperImpl.setup_and_get_completion_hook`.
33+
34+ Receives the current ``(result, exception)`` outcome of the hook call and
35+ returns the possibly replaced ``(result, exception)`` pair.
36+ """
37+
38+ def __call__ (
39+ self ,
40+ result : object | list [object ] | None ,
41+ exception : BaseException | None ,
42+ ) -> tuple [object | list [object ] | None , BaseException | None ]: ...
43+
44+
2645class HookImpl :
27- """A hook implementation in a :class:`HookCaller`."""
46+ """Base class for hook implementations in a :class:`HookCaller`."""
2847
2948 __slots__ = (
3049 "function" ,
3150 "argnames" ,
3251 "kwargnames" ,
3352 "plugin" ,
34- "opts " ,
53+ "hookimpl_config " ,
3554 "plugin_name" ,
3655 "wrapper" ,
3756 "hookwrapper" ,
@@ -45,7 +64,7 @@ def __init__(
4564 plugin : _Plugin ,
4665 plugin_name : str ,
4766 function : _HookImplFunction [object ],
48- hook_impl_opts : HookimplConfiguration ,
67+ hook_impl_config : HookimplConfiguration ,
4968 ) -> None :
5069 """:meta private:"""
5170 #: The hook implementation function.
@@ -59,23 +78,147 @@ def __init__(
5978 self .plugin : Final = plugin
6079 #: The :class:`HookimplConfiguration` used to configure this hook
6180 #: implementation.
62- self .opts : Final = hook_impl_opts
81+ self .hookimpl_config : Final = hook_impl_config
6382 #: The name of the plugin which defined this hook implementation.
6483 self .plugin_name : Final = plugin_name
6584 #: Whether the hook implementation is a :ref:`wrapper <hookwrapper>`.
66- self .wrapper : Final = hook_impl_opts .wrapper
85+ self .wrapper : Final = hook_impl_config .wrapper
6786 #: Whether the hook implementation is an :ref:`old-style wrapper
6887 #: <old_style_hookwrappers>`.
69- self .hookwrapper : Final = hook_impl_opts .hookwrapper
88+ self .hookwrapper : Final = hook_impl_config .hookwrapper
7089 #: Whether validation against a hook specification is :ref:`optional
7190 #: <optionalhook>`.
72- self .optionalhook : Final = hook_impl_opts .optionalhook
91+ self .optionalhook : Final = hook_impl_config .optionalhook
7392 #: Whether to try to order this hook implementation :ref:`first
7493 #: <callorder>`.
75- self .tryfirst : Final = hook_impl_opts .tryfirst
94+ self .tryfirst : Final = hook_impl_config .tryfirst
7695 #: Whether to try to order this hook implementation :ref:`last
7796 #: <callorder>`.
78- self .trylast : Final = hook_impl_opts .trylast
97+ self .trylast : Final = hook_impl_config .trylast
98+
99+ @property
100+ def opts (self ) -> HookimplConfiguration :
101+ """Alias for :attr:`hookimpl_config`.
102+
103+ .. deprecated::
104+ Use :attr:`hookimpl_config` instead.
105+ """
106+ return self .hookimpl_config
107+
108+ def _get_call_args (self , caller_kwargs : Mapping [str , object ]) -> list [object ]:
109+ """Extract the positional arguments for calling this hook implementation.
110+
111+ :raises HookCallError: If a required argument is missing.
112+ """
113+ try :
114+ return [caller_kwargs [argname ] for argname in self .argnames ]
115+ except KeyError as e :
116+ raise HookCallError (f"hook call must provide argument { e .args [0 ]!r} " ) from e
79117
80118 def __repr__ (self ) -> str :
81- return f"<HookImpl plugin_name={ self .plugin_name !r} , plugin={ self .plugin !r} >"
119+ return (
120+ f"<{ type (self ).__name__ } "
121+ f"plugin_name={ self .plugin_name !r} , plugin={ self .plugin !r} >"
122+ )
123+
124+
125+ @final
126+ class NormalImpl (HookImpl ):
127+ """A normal (non-wrapper) hook implementation in a :class:`HookCaller`."""
128+
129+ def __init__ (
130+ self ,
131+ plugin : _Plugin ,
132+ plugin_name : str ,
133+ function : _HookImplFunction [object ],
134+ hook_impl_config : HookimplConfiguration ,
135+ ) -> None :
136+ """:meta private:"""
137+ if hook_impl_config .wrapper or hook_impl_config .hookwrapper :
138+ raise ValueError (
139+ "NormalImpl cannot be used for wrapper implementations. "
140+ "Use WrapperImpl instead."
141+ )
142+ super ().__init__ (plugin , plugin_name , function , hook_impl_config )
143+
144+
145+ @final
146+ class WrapperImpl (HookImpl ):
147+ """A wrapper hook implementation in a :class:`HookCaller`."""
148+
149+ def __init__ (
150+ self ,
151+ plugin : _Plugin ,
152+ plugin_name : str ,
153+ function : _HookImplFunction [object ],
154+ hook_impl_config : HookimplConfiguration ,
155+ ) -> None :
156+ """:meta private:"""
157+ if not (hook_impl_config .wrapper or hook_impl_config .hookwrapper ):
158+ raise ValueError (
159+ "WrapperImpl can only be used for wrapper implementations. "
160+ "Use NormalImpl for normal implementations."
161+ )
162+ super ().__init__ (plugin , plugin_name , function , hook_impl_config )
163+
164+ def setup_and_get_completion_hook (
165+ self , hook_name : str , caller_kwargs : Mapping [str , object ]
166+ ) -> CompletionHook :
167+ """Run the wrapper setup phase and return its :class:`CompletionHook`.
168+
169+ Old-style hookwrappers and new-style wrappers are handled uniformly by
170+ adapting old-style wrappers via ``run_old_style_hookwrapper``.
171+
172+ The returned completion hook performs the teardown: it sends the
173+ current outcome into the wrapper generator (or throws the current
174+ exception) and returns the possibly replaced ``(result, exception)``
175+ pair.
176+ """
177+ # Local import to avoid a circular import with the execution module.
178+ from ._execution import _raise_wrapfail
179+ from ._execution import run_old_style_hookwrapper
180+
181+ args = self ._get_call_args (caller_kwargs )
182+
183+ wrapper_gen : Generator [None , object , object ]
184+ if self .hookwrapper :
185+ wrapper_gen = run_old_style_hookwrapper (self , hook_name , args )
186+ else :
187+ wrapper_gen = cast (Generator [None , object , object ], self .function (* args ))
188+
189+ try :
190+ next (wrapper_gen ) # first yield / setup phase
191+ except StopIteration :
192+ _raise_wrapfail (wrapper_gen , "did not yield" )
193+
194+ def completion_hook (
195+ result : object | list [object ] | None , exception : BaseException | None
196+ ) -> tuple [object | list [object ] | None , BaseException | None ]:
197+ try :
198+ if exception is not None :
199+ try :
200+ wrapper_gen .throw (exception )
201+ except RuntimeError as re :
202+ # StopIteration from generator causes RuntimeError
203+ # even for coroutine usage - see #544
204+ if (
205+ isinstance (exception , StopIteration )
206+ and re .__cause__ is exception
207+ ):
208+ wrapper_gen .close ()
209+ return result , exception
210+ else :
211+ raise
212+ else :
213+ wrapper_gen .send (result )
214+ # Following is unreachable for a well behaved hook wrapper.
215+ # Try to force finalizers otherwise postponed till GC action.
216+ # Note: close() may raise if generator handles GeneratorExit.
217+ wrapper_gen .close ()
218+ _raise_wrapfail (wrapper_gen , "has second yield" )
219+ except StopIteration as si :
220+ return si .value , None
221+ except BaseException as e :
222+ return result , e
223+
224+ return completion_hook
0 commit comments