From a7c2b02784b360981a48c46794eb205505bfc372 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 12 Nov 2017 13:50:24 -0500 Subject: [PATCH] WIP: Draft iterable hooks implementation --- pluggy/__init__.py | 12 ++++++++--- pluggy/callers.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/pluggy/__init__.py b/pluggy/__init__.py index 46011b8e..fdf609bb 100644 --- a/pluggy/__init__.py +++ b/pluggy/__init__.py @@ -1,6 +1,7 @@ import inspect import warnings -from .callers import _multicall, HookCallError, _Result, _legacymulticall +from .callers import ( + _multicall, _itercall, HookCallError, _Result, _legacymulticall) __version__ = '0.5.3.dev' @@ -209,6 +210,8 @@ def __init__(self, project_name, implprefix=None): self._plugin_distinfo = [] self.trace = _TagTracer().get("pluginmanage") self.hook = _HookRelay(self.trace.root.get("hook")) + # alternative set of lazily executed hook calls + self.ihook = _HookRelay(self.trace.root.get("hook")) self._implprefix = implprefix self._inner_hookexec = lambda hook, methods, kwargs: \ hook.multicall( @@ -247,7 +250,9 @@ def register(self, plugin, name=None): hook = getattr(self.hook, name, None) if hook is None: hook = _HookCaller(name, self._hookexec) + ihook = _HookCaller(name, self._hookexec, iterate=True) setattr(self.hook, name, hook) + setattr(self.ihook, name, ihook) elif hook.has_spec(): self._verify_hook(hook, hookimpl) hook._maybe_apply_history(hookimpl) @@ -528,14 +533,15 @@ def __init__(self, trace): class _HookCaller(object): - def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None): + def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None, + iterate=False): self.name = name self._wrappers = [] self._nonwrappers = [] self._hookexec = hook_execute self.argnames = None self.kwargnames = None - self.multicall = _multicall + self.multicall = _multicall if not iterate else _itercall if specmodule_or_class is not None: assert spec_opts is not None self.set_specification(specmodule_or_class, spec_opts) diff --git a/pluggy/callers.py b/pluggy/callers.py index 3189f8aa..10d656b6 100644 --- a/pluggy/callers.py +++ b/pluggy/callers.py @@ -202,3 +202,57 @@ def _multicall(hook_impls, caller_kwargs, specopts={}, hook=None): pass return outcome.get_result() + + +def _itercall(hook_impls, caller_kwargs, specopts={}, hook=None): + """Execute a calls into multiple python functions/methods and yield + the result(s) lazily. + + ``caller_kwargs`` comes from _HookCaller.__call__(). + """ + __tracebackhide__ = True + specopts = hook.spec_opts if hook else specopts + results = [] + firstresult = specopts.get("firstresult") + excinfo = None + try: # run impl and wrapper setup functions in a loop + teardowns = [] + try: + for hook_impl in reversed(hook_impls): + try: + args = [caller_kwargs[argname] for argname in hook_impl.argnames] + except KeyError: + for argname in hook_impl.argnames: + if argname not in caller_kwargs: + raise HookCallError( + "hook call must provide argument %r" % (argname,)) + + if hook_impl.hookwrapper: + try: + gen = hook_impl.function(*args) + next(gen) # first yield + teardowns.append(gen) + except StopIteration: + _raise_wrapfail(gen, "did not yield") + else: + res = hook_impl.function(*args) + if res is not None: + results.append(res) + yield res + if firstresult: # halt further impl calls + break + except BaseException: + excinfo = sys.exc_info() + finally: + if firstresult: # first result hooks return a single value + outcome = _Result(results[0] if results else None, excinfo) + else: + outcome = _Result(results, excinfo) + + # run all wrapper post-yield blocks + for gen in reversed(teardowns): + try: + gen.send(outcome) + _raise_wrapfail(gen, "has second yield") + except StopIteration: + pass