Skip to content

Commit b4bb48d

Browse files
author
Tyler Goodlet
committed
WIP: Draft iterable hooks implementation
1 parent 817170d commit b4bb48d

File tree

2 files changed

+83
-7
lines changed

2 files changed

+83
-7
lines changed

pluggy/__init__.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import inspect
22
import warnings
3-
from .callers import _multicall, HookCallError, _Result, _legacymulticall
3+
from .callers import (
4+
_multicall, _itercall, HookCallError, _Result, _legacymulticall)
45

56
__version__ = '0.5.3.dev'
67

@@ -209,6 +210,8 @@ def __init__(self, project_name, implprefix=None):
209210
self._plugin_distinfo = []
210211
self.trace = _TagTracer().get("pluginmanage")
211212
self.hook = _HookRelay(self.trace.root.get("hook"))
213+
# alternative set of lazily executed hook calls
214+
self.ihook = _HookRelay(self.trace.root.get("hook"))
212215
self._implprefix = implprefix
213216
self._inner_hookexec = lambda hook, methods, kwargs: \
214217
hook.multicall(
@@ -246,14 +249,25 @@ def register(self, plugin, name=None):
246249
method = getattr(plugin, name)
247250
hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
248251
hook = getattr(self.hook, name, None)
252+
ihook = getattr(self.ihook, name, None)
253+
249254
if hook is None:
250255
hook = _HookCaller(name, self._hookexec)
256+
ihook = _HookCaller(name, self._hookexec, iterate=True)
251257
setattr(self.hook, name, hook)
258+
setattr(self.ihook, name, ihook)
259+
252260
elif hook.has_spec():
253261
self._verify_hook(hook, hookimpl)
254262
hook._maybe_apply_history(hookimpl)
263+
264+
self._verify_hook(ihook, hookimpl)
265+
ihook._maybe_apply_history(hookimpl)
266+
255267
hook._add_hookimpl(hookimpl)
268+
ihook._add_hookimpl(hookimpl)
256269
hookcallers.append(hook)
270+
hookcallers.append(ihook)
257271
return plugin_name
258272

259273
def parse_hookimpl_opts(self, plugin, name):
@@ -307,14 +321,19 @@ def add_hookspecs(self, module_or_class):
307321
spec_opts = self.parse_hookspec_opts(module_or_class, name)
308322
if spec_opts is not None:
309323
hc = getattr(self.hook, name, None)
324+
hi = getattr(self.ihook, name, None)
310325
if hc is None:
311326
hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
312327
setattr(self.hook, name, hc)
328+
hi = _HookCaller(name, self._hookexec, module_or_class,
329+
spec_opts, iterate=True)
330+
setattr(self.ihook, name, hi)
313331
else:
314332
# plugins registered this hook without knowing the spec
315-
hc.set_specification(module_or_class, spec_opts)
316-
for hookfunction in (hc._wrappers + hc._nonwrappers):
317-
self._verify_hook(hc, hookfunction)
333+
for h in [hc, hi]:
334+
h.set_specification(module_or_class, spec_opts)
335+
for hookfunction in (h._wrappers + h._nonwrappers):
336+
self._verify_hook(h, hookfunction)
318337
names.append(name)
319338

320339
if not names:
@@ -529,16 +548,16 @@ def __init__(self, trace):
529548

530549

531550
class _HookCaller(object):
532-
def __init__(self, name, hook_execute, specmodule_or_class=None,
533-
spec_opts=None):
551+
def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None,
552+
iterate=False):
534553
self.name = name
535554
self._wrappers = []
536555
self._nonwrappers = []
537556
self._hookexec = hook_execute
538557
self._specmodule_or_class = None
539558
self.argnames = None
540559
self.kwargnames = None
541-
self.multicall = _multicall
560+
self.multicall = _multicall if not iterate else _itercall
542561
self.spec_opts = spec_opts or {}
543562
if specmodule_or_class is not None:
544563
self.set_specification(specmodule_or_class, spec_opts)

pluggy/callers.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,60 @@ def _multicall(hook_impls, caller_kwargs, firstresult=False):
199199
pass
200200

201201
return outcome.get_result()
202+
203+
204+
def _itercall(hook_impls, caller_kwargs, specopts={}, hook=None):
205+
"""Execute a calls into multiple python functions/methods and yield
206+
the result(s) lazily.
207+
208+
``caller_kwargs`` comes from _HookCaller.__call__().
209+
"""
210+
__tracebackhide__ = True
211+
specopts = hook.spec_opts if hook else specopts
212+
results = []
213+
firstresult = specopts.get("firstresult")
214+
excinfo = None
215+
try: # run impl and wrapper setup functions in a loop
216+
teardowns = []
217+
try:
218+
for hook_impl in reversed(hook_impls):
219+
try:
220+
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
221+
except KeyError:
222+
for argname in hook_impl.argnames:
223+
if argname not in caller_kwargs:
224+
raise HookCallError(
225+
"hook call must provide argument %r" % (argname,))
226+
227+
if hook_impl.hookwrapper:
228+
try:
229+
gen = hook_impl.function(*args)
230+
next(gen) # first yield
231+
teardowns.append(gen)
232+
except StopIteration:
233+
_raise_wrapfail(gen, "did not yield")
234+
else:
235+
res = hook_impl.function(*args)
236+
if res is not None:
237+
results.append(res)
238+
yield res
239+
if firstresult: # halt further impl calls
240+
break
241+
except BaseException:
242+
excinfo = sys.exc_info()
243+
finally:
244+
if firstresult: # first result hooks return a single value
245+
outcome = _Result(results[0] if results else None, excinfo)
246+
else:
247+
outcome = _Result(results, excinfo)
248+
249+
# run all wrapper post-yield blocks
250+
for gen in reversed(teardowns):
251+
try:
252+
gen.send(outcome)
253+
_raise_wrapfail(gen, "has second yield")
254+
except StopIteration:
255+
pass
256+
257+
# raise any exceptions
258+
outcome.get_result()

0 commit comments

Comments
 (0)