Usage Overview

Create a plugin manager

A PluginManager is the main object that registers and organizes plugins. It is instantiated with a project_name:

plugin_manager = PluginManager('my_project')

All hook specifications and hook implementations must use the same project_name if they are to be recognized by this plugin_manager instance.

Add some hook specifications

You add hook specifications which outline the function signatures plugins may implement:

plugin_manager.add_hookspecs(some_class_or_module)

… where “some_class_or_module” is any namespace object (such as a class or module) that has some functions that have been decorated as hook specifications for 'my_project' using a HookSpecificationMarker decorator.

# some_class_or_module.py

from napari_plugin_engine import HookSpecificationMarker

my_project_hook_specification = HookSpecificationMarker('my_project')

@my_project_hook_specification
def do_something(arg1: int, arg2: int): -> int:
    """Take two integers and return one integer."""

After calling add_hookspecs(), your plugin_manager instance will have a new HookCaller instance created under the plugin_manager.hooks namespace, for each hook specification discovered. In this case, there will be a new one at plugin_manager.hooks.do_something.

(Plugins) write hook implementations

Plugins may then provide implementations for your hook specifications, by creating classes or modules that contain functions that are decorated with an instance of a HookImplementationMarker that has been created using the same project name (in this example: 'my_project')

# some_plugin.py

from napari_plugin_engine import HookImplementationMarker

my_project_hook_implementation = HookImplementationMarker('my_project')

@my_project_hook_implementation
def do_something(arg1, arg2):
    return arg1 + arg2

Register plugins

You may directly register these modules with the plugin_manager

import some_plugin

plugin_manager.register(some_plugin)

Autodiscover plugins in the environment

However, it is more often the case that you will want to discover plugins in your environment. napari-plugin-engine provides two ways to discover plugins via two different conventions:

1. Using package metadata: looking for distributions that declare a specific entry_point in their setup.py file.

2. Using naming convention: looking for modules that begin with a specific prefix.

You can look for either or both, in single call to discover(), which will import any modules or entry_points that follow one of the aforementioned conventions, and search them for functions decorated with the appropriate HookImplementationMarker (as shown above in some_plugin.py)

plugin_manager.discover(
   entry_point='my_project.plugin', prefix='my_project_'
)

Use (call) the plugin implementations

Your HookCaller should now be populated with any of the implementations found in plugins, as HookImplementation objects on the HookCaller.

# show all implementations for do_something
plugin_manager.hooks.do_something.get_hookimpls()

Finally, you can call some or all of the plugin implementation functions by directly calling the HookCaller object:

result = plugin_manager.hooks.do_something(arg1=2, arg2=7)

# assuming only some_plugin.py from above is registered:
print(result)  # [9]

By default, all plugin implementations are called, and all non-None results are returned in a list. However, this is configurable and depends on how the @my_project_hook_specification was used, and how the HookCaller was called

How the plugin_name is chosen

1. If plugin discovery via entry_points is used

(e.g. plugin_manager.discover(entry_point='app.plugin')), then plugins will be named using the name of the entry_point provided by each plugin. Note, a single package may provide multiple plugins via entry points. For example, if a package had the following entry_points declared in their setup.py file:

# setup.py

setup(
...
entry_points={'app.plugin': ['plugin1 = module_a', 'plugin2 = module_b']},
...
)

… then manager.discover(entry_point='app.plugin') would register two plugins, named "plugin1" (which would inspect module_a for implementations) and "plugin2" (which would inspect module_b for implementations).

2. If plugin discovery via naming convention is used

(e.g. plugin_manager.discover(prefix='app_')), then…

2a. If a dist-info folder is found for the module

Then the plugin will be named using the Name key in the distribution METADATA file if one is available. Usually, this will come from having a setup(name="distname", ...) entry in a setup.py file. See Core metadata specifications and PEP 566 for details.

2a. If no distribution metadata can be located

The the plugin will be named using the name of the module itself.

3. If a plugin is directly registered

(e.g. plugin_manager.register(object, name)), then if a name argument is provided to the PluginManager.register() method, it will be used as the plugin_name, otherwise, the string form of the object is used: str(id(object))