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

Introduce a plugin "context" system #150

Open
goodboy opened this issue May 20, 2018 · 2 comments
Open

Introduce a plugin "context" system #150

goodboy opened this issue May 20, 2018 · 2 comments

Comments

@goodboy
Copy link
Contributor

goodboy commented May 20, 2018

As per discussion in #51 I started thinking about how to address the problem of nested plugin dependencies and clobbering of the global registry state. This is just a very quickly concocted initial design draft so feel free to critique the heck out of it.

The main issue as I understand it is summarized:

  • the PluginManager contains a global state of plugin hooks.
  • registering new plugins (and their dependencies) permanently mutates this global hook registry.
    • sure plugins can be unregistered but there is no way to know what dependencies (other plugins) should also be unregistered.
  • when dynamically registering and then subsequently unregistering plugins, dependencies which were also registered will not be removed without careful tracking by the user - this results in stale hooks left in the registry which may cause problems for the other plugins not expecting them to be there when operating in a different plugins context.
  • there is no way to track when a plugin sub-dependency is required by more then one parent plugin

The problem as discovered with pytest was avoided through the limitation in pytest-dev/pytest#3084. Currently sub-conftest.py files can't introduce new plugins dynamically without causing this registry clobbering via plugin dependencies.

An idea for a solution is to add a sub-system whereby each registered plugin is expected to explicitly declare any 1st depth level (plugin) dependencies in order for the PluginManager to keep track of the overall plugin dependency graph thereby allowing for modelling distinct plugins contexts.

Let's set some premises and terminology:

  • a plugins context is a snap-shot of the dependency tree at some point in time where each plugin in the tree has its hooks already registered (thereby affecting the runtime execution of the host project); execution under different contexts results in different behaviour of the host project
  • it's not feasible to expect each plugin to know all its (transitive) dependencies only its immediate sub-dependencies
  • a context can be modelled as the entire n-ary dependency tree of all currently registered plugin nodes where the root sub-tree is top most single depth sub-tree (eg. pytest + the pytest11 entry point children + top level conftest.py children)
  • the dependency tree may naturally contain cycles (some plugin may have more then one dependent parent plugin) and will not be a DAG
  • a plugin can be registered specifying 1 optional piece of info: a sub_plugins: [str] list of expected dependency plugin names allowing for tracking each single depth sub-tree layer of the dependency graph
  • a context can be verified at each step using the existing PluginManager.check_pending() API to ensure dependencies which are not listed are not registered and vice-versa, dependencies which are declared are at some point are in fact registered prior to a call to check_pending()
  • by keeping track of each single depth sub-tree (a plugin and its immediate 1st level dependencies) we can store a dependency graph of all plugins and use it to conduct traversals for:
    • listing all downstream dependencies of a particular plugin
    • unregistering subsets of plugins easily
    • tracking differences between hook calls under different contexts

API and implementation:

  • introduce a dependencies or sub_plugins arg to PluginManager.register()
  • introduce a PluginManager._host_deps a sequence listing the top-most first level deps of the host project (describes the first deps layer)
  • store an additional 2 internal dictionaries of plugin2deps and dep2parents which respectively allow tracking deps per node (each layer) and parents per node
  • add an internal tree traversal function (topological sort) which can be used to iterate all downstream deps of a plugin for either the purposes of removal or context comparison

Edge cases:

  • top level nodes (deps in the first layer) must be distinguished differently since a host project should not be required to know all plugins that may be used; a special API call or flag might be needed when registering root dependencies as they will need to be appended dynamically to the _host_deps set
  • removing (de-regristration) of any node requires that zero dependents (parent node plugins) point to it
@goodboy
Copy link
Contributor Author

goodboy commented Jan 21, 2019

@RonnyPfannschmidt @nicoddemus not to rush you but I would appreciate any thoughts on this when you get a chance.

It's definitely a rough design / description so bear that in mind.

@RonnyPfannschmidt
Copy link
Member

this looks like a nice initial architecture idea - but at first glance also something we couldn't even hope to use with the current conftest/plugin/loaded plugins mechanisms in pytest

i suspect we need to iterate a few times to get something we can experiment with, however for me its a low hanging fruit i can do with easily another 5 years down the line

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

2 participants