diff --git a/pluggy/callers.py b/pluggy/callers.py index 3ff67bec..9950dc6e 100644 --- a/pluggy/callers.py +++ b/pluggy/callers.py @@ -199,3 +199,62 @@ def _multicall(hook_impls, caller_kwargs, firstresult=False): 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 GeneratorExit: + pass # loop was terminated prematurely by caller + 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 + + # raise any exceptions + outcome.get_result() diff --git a/pluggy/hooks.py b/pluggy/hooks.py index ae7c321f..2bc25b48 100644 --- a/pluggy/hooks.py +++ b/pluggy/hooks.py @@ -168,8 +168,8 @@ 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 = [] @@ -177,7 +177,7 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, self._specmodule_or_class = None self.argnames = None self.kwargnames = None - self.multicall = _multicall + self.multicall = _multicall if not iterate else _itercall self.spec_opts = spec_opts or {} if specmodule_or_class is not None: self.set_specification(specmodule_or_class, spec_opts) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 6b35814b..06fff9a7 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -393,3 +393,60 @@ def example_hook(): assert getattr(pm.hook, 'example_hook', None) # conftest.example_hook should be collected assert pm.parse_hookimpl_opts(conftest, 'example_blah') is None assert pm.parse_hookimpl_opts(conftest, 'example_hook') == {} + + +def test_iterable_hooks(pm): + class Hooks(object): + @hookspec + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + + l = [] + + class Plugin1(object): + @hookimpl + def he_method1(self, arg): + l.append(1) + return 1 + + class Plugin2(object): + @hookimpl + def he_method1(self, arg): + l.append(2) + return 2 + + class Plugin3(object): + @hookimpl + def he_method1(self, arg): + l.append(3) + return 3 + + class Plugin4(object): + @hookimpl + def he_method1(self, arg): + l.append(4) + return 4 + + class PluginWrapper(object): + @hookimpl(hookwrapper=True) + def he_method1(self, arg): + assert not l + outcome = yield + res = outcome.get_result() + assert res + assert res == [4, 3, 2] == l + + pm.register(Plugin1()) + pm.register(Plugin2()) + pm.register(Plugin3()) + pm.register(Plugin4()) + pm.register(PluginWrapper()) + + for result, i in zip(pm.ihook.he_method1(arg=None), reversed(range(1, 5))): + assert result == i + if result == 2: # stop before the final iteration + break + + assert l == [4, 3, 2]