Plugins¶
Plugins are a comfortable way of extending Slash’s behavior. They are objects inheriting from a common base class
that can be activated to modify or what happens in select point of the infrastructure.
The Plugin Interface¶
Plugins have several special methods that can be overriden, like get_name
or configure_argument_parser
. Except for these methods and the ones documented, each public method (i.e. a method not beginning with an underscore) must correspond to a slash hook by name.
The name of the plugin should be returned by get_name
. This name should be unique, and not shared by any other plugin.
Plugin Discovery¶
Plugins can be loaded from multiple locations.
Search Paths¶
First, the paths in plugins.search_paths
are searched for python files. For each file, a function called install_plugins
is called (assuming it exists), and this gives the file a chance to install its plugins.
Plugin Installation¶
To install a plugin, use the slash.plugins.manager.install
function, and pass it the plugin class that is being installed. Note that installed plugins are not active by default, and need to be explicitly activated (see below).
Only plugins that are PluginInterface
derivative instances are accepted.
To uninstall plugins, you can use the slash.plugins.manager.uninstall
.
Note
uninstalling plugins also deactivates them.
Internal Plugins¶
By default, plugins are considered “external”, meaning they were
loaded by the user (either directly or indirectly). External plugins
can be activated and deactivated through the command-line using
--with-<plugin name>
and --without-<plugin name>
.
In some cases, though, you may want to install a plugin in a way that would not let the user disable it externally. Such plugins are considered “internal”, and cannot be deactivated through the command line.
You can install a plugin as an internal plugin by passing internal=True
to the install function.
Plugin Activation¶
Plugins are activated via slash.plugins.manager.activate
and deactivated via slash.plugins.manager.deactivate
.
During the activation all hook methods get registered to their respective hooks, so any plugin containing an unknown hook will trigger an exception.
Note
by default, all method names in a plugin are assumed to belong to the slash gossip group. This means that the method session_start
will register on slash.session_start
. You can override this behavior by using slash.plugins.registers_on()
:
from slash.plugins import registers_on
class MyPlugin(PluginInterface):
@registers_on('some_hook')
def func(self):
...
registers_on(None)
has a special meaning - letting Slash know that this is not a hook entry point, but a private method belonging to the plugin class itself.
See also
Activating plugins from command-line is usually done with the --with-
prefix. For example, to activate a plugin called test-plugin
, you can pass --with-test-plugin
when running slash run
.
Also, since some plugins can be activated from other locations, you can also override and deactivate plugins using --without-X
(e.g. --without-test-plugin
).
Conditionally Registering Hooks¶
You can make the hook registration of a plugin conditional, meaning it should only happen if a boolean condition is True
.
This can be used to create plugins that are compatible with multiple versions of Slash:
class MyPlugin(PluginInterface):
...
@slash.plugins.register_if(int(slash.__version__.split('.')[0]) >= 1)
def shiny_new_hook(self):
...
See also
Plugin Command-Line Interaction¶
In many cases you would like to receive options from the command line. Plugins can implement the configure_argument_parser
and the configure_parsed_args
functions:
class ResultsReportingPlugin(PluginInterface):
def configure_argument_parser(self, parser):
parser.add_argument("--output-filename", help="File to write results to")
def configure_from_parsed_args(self, args):
self.output_filename = args.output_filename
Plugin Configuration¶
Plugins can override the config
method to provide configuration to be placed under plugin_config.<plugin name>
:
class LogCollectionPlugin(PluginInterface):
def get_default_config(self):
return {
'log_destination': '/some/default/path'
}
The configuration is then accessible with get_current_config
property.
Plugin Examples¶
An example of a functioning plugin can be found in the Customizing and Extending Slash section.
Errors in Plugins¶
As more logic is added into plugins it becomes more likely for exceptions to occur when running their logic. As seen above, most of what plugins do is done by registering callbacks onto hooks. Any exception that escapes these registered functions will be handled the same way any exception in a hook function is handled, and this depends on the current exception swallowing configuration.
See also
Plugin Dependencies¶
Slash supports defining dependencies between plugins, in a mechanism closely related to to gossip’s hook dependencies. The purpose of these dependencies is to make sure a certain hook registration in a specific plugin (or all such hooks for that matter) is called before or after equivalent hooks on other plugins.
Notable examples of why you might want this include, among many other cases:
Plugins reporting test status needing a state computed by other plugins
Error handling plugins wanting to be called first in certain events
Log collection plugins wanting to be called only after all interesting code paths are logged
Defining Plugin Dependencies¶
Defining dependencies is done primarily with two decorators Slash
provides: @slash.plugins.needs
and
@slash.plugins.provides
. Both of these decorators use string
identifiers to denote the dependencies used. These identifiers are
arbitrary, and can be basically any string, as long as it matches
between the dependent plugin and the providing plugin.
Several use cases exist:
Hook-Level Dependencies¶
Adding the slash.plugins.needs
or slash.plugins.provides
decorator to a specific hook method on a plugin indicates that we
would like to depend on or be the dependency accordingly. For example:
class TestIdentificationPlugin(PluginInterface):
@slash.plugins.provides('awesome_test_id')
def test_start(self):
slash.context.test.awesome_test_id = awesome_id_allocation_service()
class TestIdentificationLoggingPlugin(PluginInterface):
@slash.plugins.needs('awesome_test_id')
def test_start(self):
slash.logger.debug('Test has started with the awesome id of {!r}', slash.context.test.awesome_id)
In the above example, the test_start
hook on
TestIdentificationLoggingPlugin
needs the test_start
of
TestIdentificationPlugin
to be called first, and thus requires
the 'awesome_test_id'
identifier which is provided by the latter.
Plugin-Level Dependencies¶
Much like hook-level dependencies, you can decorate the entire plugin
with the needs
and provides
decorators, creating a dependency
on all hooks provided by the plugin:
@slash.plugins.provides('awesome_test_id')
class TestIdentificationPlugin(PluginInterface):
def test_start(self):
slash.context.test.awesome_test_id = awesome_id_allocation_service()
@slash.plugins.needs('awesome_test_id')
class TestIdentificationLoggingPlugin(PluginInterface):
def test_start(self):
slash.logger.debug('Test has started with the awesome id of {!r}', slash.context.test.awesome_id)
The above example is equivalent to the previous one, only now future hooks added to either of the plugins will automatically assume the same dependency specifications.
Note
You can use provides
and needs
in more complex
cases, for example specifying needs
on a specific hook
in one plugin, where the entire other plugin is decorated
with provides
(at plugin-level).
Note
Plugin-level provides and needs also get transferred upon inheritence, automatically adding the dependency configuration to derived classes.
Plugin Manager¶
As mentioned above, the Plugin Manager provides API to activate (or deacativate) and install (or uninstall) plugins.
Additionally, it provides access to instances of registered plugins by their name via slash.plugins.manager.get_plugin
.
This could be used to access plugin attributes whose modification (e.g. by fixtures) can alter the plugin’s behavior.
Plugins and Parallel Runs¶
Not all plugins can support parallel execution, and for others implementing support for it can be much harder than supporting non-parallel runs alone.
To deal with this, in addition to possible mistakes or corruption caused by plugins incorrectly used in parallel mode, Slash requires each plugin to indicate whether or not it supports parallel execution. The assumption is that by default plugins do not support parallel runs at all.
To indicate that your plugin supports parallel execution, use the plugins.parallel_mode
marker:
from slash.plugins import PluginInterface, parallel_mode
@parallel_mode('enabled')
class MyPlugin(PluginInterface):
...
parallel_mode
supports the following modes:
disabled
- meaning the plugin does not support parallel execution at all. This is the default.parent-only
- meaning the plugin supports parallel execution, but should be active only on the parent process.child-only
- meaning the plugin should only be activated on worker/child processes executing the actual tests.enabled
- meaning the plugin supports parallel execution, both on parent and child.