Description
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
+ thepytest11
entry point children + top levelconftest.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 tocheck_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
orsub_plugins
arg toPluginManager.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
anddep2parents
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