Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Draft iterable hooks implementation #98

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions pluggy/callers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is literally the only thing added.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now that bit feeels really fishy because this is a massive mixup of concepts (sometimes minimal changes are the absolute antithesis of responsible software development)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uppon closer inspection tihs mixes iteration with result returning as object, instinctuvely this cant possibly be conceptually sound

Copy link
Contributor Author

@goodboy goodboy Nov 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed I'm just pushing out a quick version.
Obviously the final cut should get rid of anything from the original function which isn't necessary in the generator.

If you're pointing out that the return on the last line does nothing you'd be right.

Copy link
Member

@nicoddemus nicoddemus Nov 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uppon closer inspection tihs mixes iteration with result returning as object, instinctuvely this cant possibly be conceptually sound

I'm not sure what you mean, the implementation is very similar to the existing one except for the yield part and removing the return at the end as @goodboy commented.

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()
6 changes: 3 additions & 3 deletions pluggy/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,16 @@ 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._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)
Expand Down
57 changes: 57 additions & 0 deletions testing/test_pluginmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]