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

Question about hook call order #250

Open
tlambert03 opened this issue Jan 25, 2020 · 8 comments
Open

Question about hook call order #250

tlambert03 opened this issue Jan 25, 2020 · 8 comments

Comments

@tlambert03
Copy link
Contributor

❓ Question

I'm new to pluggy, and trying to decide whether to use it as the plugin manager in a project I work on (looks great!). One of the main things I find myself wanting more control over is the exact order in which implementations of a specific hook are called (beyond simply registering an @hookimpl with trylast or tryfirst). Specifically, I would ultimately like to let the end user reorder plugin implementations of specific hooks after plugin registration.

To accomplish that I find myself tempted to write methods to reorder _HookCaller._nonwrappers ... but because that is a private attribute, I am hesitant to do so and wonder if I am maybe misunderstanding a fundamental design principle here. Is that a valid approach, or discouraged?

@goodboy
Copy link
Contributor

goodboy commented Jan 26, 2020

Hey @tlambert03. The call time order is LIFO relative to registration as mentioned in the docs. So if you want a specific order currently you'd have to register hooks last to first in order to get that order when invoking. We don't have a public API for ordering currently but it might be something useful? I've thought about offering a generator API which allows for controlling which hooks are invoked by yielding them in the order desired - this might play with #50 potentially.

If you're concerned about order it may be more sensible to build a small *ordered hook protocol" much like pytest does. Instead of trying to control the order of invocation of a set of implementations for a single hook, make multiple hooks that are invoked in a particular order: If multiple implementations are provided for a hook you get all results in a list so if it's a matter of just reordering results then it should be mostly straight forward (though you might not know which element is from which implementation). I have feeling what you're actually after is something more along the lines of what we've discussed in #50.

Hope this helps!

@tlambert03
Copy link
Contributor Author

tlambert03 commented Jan 26, 2020

Thanks for the response! Definitely helpful to get your thoughts on this. I think my case is along the lines of #50 (though actually... after reading that, I'm now slightly confused about my understanding of the firstresult option). The ordered hook protocol is interesting, but I'm still not sure I see a full/ideal solution for my usage. In case it's helpful, I'll provide a little more background on our use case.

I'm contemplating using pluggy as the plugin manager for napari, a multidimensional image/data viewer. The first hook we're discussing would be an file reader plugin, where plugins can add support for different file formats. Plugins would implement a hook that accepts a filepath (str) and returns None if the path represents a file/directory/URL format that the plugin does not support, or a callback function capable of actually reading the data if it does recognize the format (I'm glossing over some side details, but that's the basic idea). Here are the reasons I wanted the consumer to be able to reorder the hooks:

  • there may be many plugins registered, yet we want to ensure that this particular hook runs very fast.
  • we want to use firstresult to ensure that as soon as one of the plugins recognizes the format, its callback function is used and the other plugins do not even get a chance to run their hooks.
  • multiple plugins will likely recognize/support a given file, so we'd very much like the end-user to be able to tune the order of hook calls to suite their use case (we would provide a GUI for them to do so), potentially even reordering multiple times after plugins have been registered.

it's not immediately clear how to use an ordered hook protocol to allow the end user to reoderd the hooks, while also using the firstresult idea to immediately terminate the hook chain once a plugin claims that file. If you agree that this is a use case that can't be cleanly accomplished at the moment and would consider a PR, I can think about it some more and potentially submit one. Thanks again!

(sorry accidentally hit the close button).

@elchupanebrej
Copy link

Hey @tlambert03 , @goodboy

Maybe the next kind of hookimpl API could be helpful:

from pluggy import PluginManager, HookimplMarker

hookimpl = HookimplMarker("myproject")


class Plugin1:
    @hookimpl(before=['Plugin3'])
    def myhook(self, args):
        return 1


class Plugin2:
    @hookimpl(before=['Plugin1'])
    def myhook(self, args):
        return 2


class Plugin3:
    @hookimpl(after=['Plugin2'])
    def myhook(self, args):
        return 3


pm = PluginManager("myproject")
pm.register(Plugin1())
pm.register(Plugin2())
pm.register(Plugin3())

assert pm.hook.myhook(args=()) == [2, 1, 3]

Implementation could be pretty easy based on a projection of partially ordered set to strict order. Tryfirst/trylast also could be in place but have to be less strict than the defined order. Please share your thoughts.

@RonnyPfannschmidt
Copy link
Member

Having topologies is a long desired feature, what's lacking is a actually sensible implementation

@AvlWx2014
Copy link

AvlWx2014 commented Sep 15, 2022

What about introducing public API to allow subclasses of PluginManager (or through constructor parameters to PluginManager instead) to provide a custom hook execution function? With some hand-holding from pluggy I think this would help address running plugins in any order clients want without clients being able to go wrong easily, and without necessarily having to overhaul PluginManager.

For example, I'm working on being able to execute plugins that have dependencies on other plugins. Right now I have a subclass of PluginManager that maintains the set of registered plugins in a DAG and overrides self._inner_hookexec with its own function that executes plugins in a different order.

Example:

class DagPluginManager(PluginManager):
    def __init__(self, project_name):
        super().__init__(project_name)
        self._dag = SimpleDirectedGraph()
        self._inner_hookexec = self._dag_exec
        
    def _dag_exec(self, _, hook_impls, caller_kwargs, __):
        # execute plugins in custom order based on graph

Obviously this approach is pretty fragile since it relies on internal API and some implementation details, which is why I want to help implement something that isn't fragile. Wdyt?

@RonnyPfannschmidt
Copy link
Member

Having a dag of plugins both for conditional and for unconditional activation and ordering is a long wanted feature

Unfortunately there is a number of caveats and not enough time

Im happy to provide input, but I can't work on it myself anytime soon

@AvlWx2014
Copy link

Rather than overhauling PluginManager to use a dag (or adding another PluginManager implementation which uses a dag), I was suggesting it could be helpful to simply allow client code to inject their own exec function to substitute for multicall. I'm basically doing this right now, but it's not public API. If you think that's useful I can submit a WIP PR for it, but if not I could try the DAG approach.

@RonnyPfannschmidt
Copy link
Member

instead of replacing multicall, i would suggest to have something that orders the items added baed on a better topology

right now we have tryfirst/trylast and the hookwrapper vs normal hook order

whats needed as well would be to have something like before/after/requires to add the extra items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants