-
Notifications
You must be signed in to change notification settings - Fork 125
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
base: main
Are you sure you want to change the base?
Conversation
res = hook_impl.function(*args) | ||
if res is not None: | ||
results.append(res) | ||
yield res |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
pluggy/callers.py
Outdated
except StopIteration: | ||
pass | ||
|
||
return outcome.get_result() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line currently does nothing and should be removed.
a7c2b02
to
e6d7bd5
Compare
Just added a more correct/complete integration with the manager and a test to verify. Still rough but a starting point nonetheless. Also what about supporting hooks as generators? Bad idea? We'd probably only support it for py3 anyway since we can use |
@tgoodlet nice, thanks! This is a good starting point.
Currently hook calls either return a list (normal hooks) or a single value (hooks marked as |
@nicoddemus so techincally we could jig this such that But then regular calls will be slower. However, if all of this is cythonized it may not matter.
Yeah only problem is that's a low slower. |
Hmm sorry what you mean, could you clarify?
Oh is this the reason why we are having both |
We could theoretically make
Precisely. That being said, regarding cython, I'd always thought a cythonized call loop would be an optional dependency. |
OK thanks for confirming! 👍
This is really strange. I remember your benchmark and it seemed correct. I did a quick benchmark unrelated to pluggy, just to show the speed difference between constructing a list vs using a generator: def f():
r = []
for i in range(100):
r.append(i)
return r
def g():
for i in range(100):
yield i
And this results match my expectations. Perhaps is there something else in pluggy's code that might be causing this difference? |
What if we time the two approaches to measure them? We can use the code from your test as basis: from pluggy import PluginManager, HookspecMarker, HookimplMarker
hookspec = HookspecMarker("example")
hookimpl = HookimplMarker("example")
pm = PluginManager("example")
class Hooks(object):
@hookspec
def he_method1(self, arg):
pass
pm.add_hookspecs(Hooks)
class Plugin1(object):
@hookimpl
def he_method1(self, arg):
pass
class Plugin2(object):
@hookimpl
def he_method1(self, arg):
pass
class Plugin3(object):
@hookimpl
def he_method1(self, arg):
pass
class Plugin4(object):
@hookimpl
def he_method1(self, arg):
pass
class PluginWrapper(object):
@hookimpl(hookwrapper=True)
def he_method1(self, arg):
yield
pm.register(Plugin1())
pm.register(Plugin2())
pm.register(Plugin3())
pm.register(Plugin4())
pm.register(PluginWrapper())
|
Duh, why don't I do it myself? 😁 Forked your branch and get nearly the same timings:
Unless I'm missing something, their performance is nearly identical, so IMHO we should go to just change |
@nicoddemus ahh yeah I was testing it the opposite way... The only thing I wonder is how does it perform under different numbers of hooks/wrappers as in the benchmark test suite. I think we should try that out first while there's separate implementations. |
i would like to bring attention to the fact that hookwrappers are unable to semantically correctly operate if we stream partial results out without giving them a chance to operate/alter the stream |
Hmm indeed that is a problem, because the post-hookwrappers can't alter the outcome, because the outcome has already been processed by the caller. Not sure what we should do here, it seems to be a big blocker. |
i beleive for this feature we have the choice between massive cost in terms of conceptual complexity for implementation or glaring conceptual holes in the api i leaning towards avoiding it due to inconsistency |
@RonnyPfannschmidt @nicoddemus yes agreed. This is why I was implementing it separate from the main hook call set. I think @RonnyPfannschmidt is right you can't combine to the 2 ideas which makes me wonder if the only way we could support it is by having seperately marked iterable hooks which are also called from a different |
For the use cases I have in mind, making the hook explicitly iterable would be fine, if not even preferred. If such hooks then don't support hookwrappers would be fine by me. |
From a user standpoint, we would mark it as: @hookspec(iterable=True)
def myproject_get_password(self, arg1, arg2):
"""
""" ? |
@nicoddemus yeah I think that could work. |
@nicoddemus, @RonnyPfannschmidt @fschulze just had a convo with @vodik about this and I think we could make an alternative version of what a wrapper is for iterable hooks that's even more powerful. Instead of having setup and teardown before and after all That is a hookwrapper is a generator which looks as follows: @hookimpl(wrapper=True):
def myproject_get_password(arg1, arg2):
iter_results = yield
for result in iter_results:
# intercept and do something with each result
yield result # delivers the hookimpl call result to the hook caller (or next hookwrapper in line)
# do something after the result has been delivered but before the next
# hook call has been invoked inside _itercall Here We could also do some even more interesting stuff in py3.3+. We can actually support the original wrapper semantics in a cleaner way with @hookimpl(wrapper=True):
def myproject_get_password(arg1, arg2):
iter_results = yield
# do the normal pre-call stuff
try:
results = yield from iter_results # yields every hookcall upwards to caller; returns full list of yielded results
except BaseException as err:
if raise_on_err:
raise err
else:
# do the normal post-call stuff
results.clear()
results.append('my_override_result')
return results # this could still be used for the normal (non-iterable) hook calls Which has the extra nice nicety that we really wouldn't need the Let me know what you guys think! |
it has a simple question - what does this do to python2.7 |
@tgoodlet about your first example, when you say
At first it does seem the new version is more flexible but I think it is a little more complex to write/understand, so I'm not sure it is better. It might be just a matter of getting used to it though. Don't know, have to think about it a little better.
IIRC it should work the same, except results = yield from iter_results In py27 one has to write: results = []
for result in iter_results:
yield result
results.append(result) @tgoodlet please correct me if I'm wrong. |
@nicoddemus yes exactly. So a wrapper is able to intercept each value before delivered to the caller. This provides a way to execute before/after each iteration as well as modify the value or, not deliver it at all. For example the wrapper could iterarate all hook calls but not deliver any results up to the caller until it's prepossessed them: @hookimpl(wrapper=True):
def myproject_get_password(arg1, arg2):
iter_results = yield
results = []
for result in iter_results:
results.append(result)
# do some checks over all results
if 'myresult' in results:
pass
else:
results.append('myresult')
# do the preprocessing of each result
for result in filter(preprocess, results):
yield result # finally delivered to caller
Yes of course. I was just pointing out how nice it would look. Also, there's some optimizations python does with |
@tgoodlet as far as i can tell thats a glefully fractioned controll flow using the same structural mechanism for absolutely distinct meanings, im reasonably sure this will break people necks from partial/complete misuse im pretty opposed to is as of now - if you cant demonstrate a reasonable api for the hookwrappers, just disallow them for now from my pov this cant possibly have a reasonable api without async await/async for/ |
I agree with @RonnyPfannschmidt, my gut feeling is that this is creating a complex control flow which might be problematic in the future (it is a fun thought exercise though). As @RonnyPfannschmidt said, I think we should go ahead with the original idea of iterable hook calls but without |
@nicoddemus @RonnyPfannschmidt cool I'll see if I can finalize it this week. |
6e04ed6
to
8890bfd
Compare
Hey guys I'll crank this out over the weekend but I need #101 to land first to avoid conflicts. |
Example/untested draft implementation for discussion in #50.
Ping @fschulze too.