Getting Started with dip.plugins

A well designed dip application will be implemented as a set of components. The more those components are decoupled from each other, the greater chance there is of being able to reuse them in a different context. Defining components in terms of interfaces is a major step in being able to decouple them. The use of default handlers makes it easy to provide a default implementation of an interface while still allowing it to be overridden with an alternative implementation. However, that still requires that one component is explictly aware of another component to use as a default implementation.

The dip.plugins module provides mechanisms for making connections between components while ensuring that the components themselves are completely decoupled from each other.

Assuming that an application has been structured sensibly (i.e. putting logically seperate code in separate modules - not how we have been structuring our examples so far), then converting an application to be plugin based is normally a case of adding plugin definitions.

Later on in this section we will implement some plugin definitions for our python_ide.py example.

Concepts

A plugin is an implementation of the IPlugin interface that makes connections between components by publishing objects, either as contributions to extension points or as services. Plugins are managed by a plugin manager. A plugin will play no part in an application until it is enabled.

An extension point is a list of published objects, usually of a particular type or implementing a particular interface. Each extension point has a unique string identifier. All extension points defined by dip have identifiers beginning with dip.. Plugins make contributions to extension points when they are enabled. A plugin can bind an extension point to an attribute of an object. A bound attribute will normally be a List but can be anything that has an append() method. A contribution is a list of objects. When a contribution is made each object is appended to each attribute that is bound to the extension point. When an attribute is bound then any previous contributions are appended to the attribute.

A service is an object that implements a particular interface. Several plugins may provide services that implement the same interface. When a plugin requests a service the plugin manager will choose which service is actually used. The plugin does not care about the particular service, its only concern is that it has an object that implements the interface. A plugin can then bind a service to an attribute of an object.

When a plugin is enabled it will create any services it provides, possibly requesting other services to configure them with. It will also make any contributions to extension points and possibly bind extension points to objects it creates.

A plugin may specify that it requires that another plugin, identified by its string identifier, is already present and enabled.

Plugins are lightweight objects, i.e. they are quick to import and have small memory footprints. It is only when a plugin is enabled that more significant imports are done and potentially resource hungry objects created.

Writing Plugins

Going back to our python_ide.py example lets assume that we have restructured the code along the following lines.

python_code.py
This contains the PythonCode class, its PythonCodeFactory factory class and the adapter between PythonCode and the IManagedModel interface.
python_code_codec.py
This contains the adapter between PythonCode and the IFilterHints, IUnicodeDecoder and IUnicodeEncoder interfaces.
python_code_editor.py
This contains the PythonCodeEditorTool class.
project.py
This contains the Project class, its ProjectFactory factory class and the adapter between Project and the IManagedModel interface.
project_codec.py
This contains the adapter between Project and the IFilterHints, IXmlDecoder and IXmlEncoder interfaces.
project_editor.py
This contains the ProjectEditorTool class.

This structure represents the different functional pieces. There are strong arguments for breaking it down further, particularly to keep adapters separate from the types that they are adapting.

We can now write a plugin for each of the above. The following is the plugin that publishes an instance of PythonCodeFactory as a managed model factory:

from dip.model import implements, Model
from dip.plugins import IPlugin
from dip.ui import IDisplay


@implements(IPlugin, IDisplay)
class PythonCodePlugin(Model):
    """ The PythonCodePlugin is the plugin definition for the
    PythonCodeFactory managed model factory.
    """

    # The identifier of the plugin.
    id = 'myorganization.plugins.python_code'

    # The name of the plugin.
    name = "Python code plugin"

    def configure(self, plugin_manager):
        """ Configure the plugin. """

        # Create the model factory instance.
        from .python_code import PythonCodeFactory
        model_factory = PythonCodeFactory()

        # Contribute the model factory.
        plugin_manager.contribute(
                'dip.shell.model_factories', model_factory)

A plugin must implement the IPlugin interface. Our plugin also implements the IDisplay interface.

The id attribute specifies the plugin’s identifier. Plugins with the same identifier are assumed to perform the same function.

The name attribute specifies the plugin’s user friendly name. In the current version of dip this is unused but a future version will allow the user to explicitly enable and disable plugins, particularly those that have been discovered dynamically.

The configure() method is called by the plugin manager when the plugin is enabled. Here we create our model factory and contribute it to the dip.shell.model_factories extension point. We assume that the extension point will be bound to an attribute of an object elsewhere in the application - but we don’t really care.

All of the plugins for our example will look very similar to this. We will show just one more - the plugin that contributes the codec for the decoding and encoding a Project instance:

from dip.io.codecs.xml import XmlCodec
from dip.model import implements, Model
from dip.plugins import IPlugin
from dip.ui import IDisplay


@implements(IPlugin, IDisplay)
class ProjectCodecPlugin(Model):
    """ The ProjectCodecPlugin is the plugin definition for the codec that
    decodes and encodes a Project instance.
    """

    # The identifier of the plugin.
    id = 'myorganization.plugins.project_codec'

    # The name of the plugin.
    name = "Project codec plugin"

    def configure(self, plugin_manager):
        """ Configure the plugin. """

        # Make sure the adapter gets registered.
        from . import python_codec

        # Create the codec instance.
        codec = XmlCodec(format='myorganization.formats.project')

        # Contribute the codec.
        plugin_manager.contribute('dip.io.codecs', codec)

Of course our plugin based version of our original python_ide.py file looks different as it now mostly consists of adding a series of plugins:

import sys

from dip.io.plugins import FilesystemStoragePlugin
from dip.plugins import PluginManager
from dip.shell import IShell
from dip.shell.plugins import (DirtyToolPlugin, MainWindowShellPlugin,
        ModelManagerToolPlugin, QuitToolPlugin)
from dip.ui import Application, IView

from .python_code_plugin import PythonCodePlugin
from .python_code_codec_plugin import PythonCodeCodecPlugin
from .python_code_editor_plugin import PythonCodeEditorPlugin

from .project_plugin import ProjectPlugin
from .project_codec_plugin import ProjectCodecPlugin
from .project_editor_plugin import ProjectEditorPlugin


# Every application needs an Application.
app = Application()

# Add dip provided plugins for a shell, tools and storage.
PluginManager.add_plugin(MainWindowShellPlugin())

PluginManager.add_plugin(DirtyToolPlugin())
PluginManager.add_plugin(ModelManagerToolPlugin())
PluginManager.add_plugin(QuitToolPlugin())

PluginManager.add_plugin(FilesystemStoragePlugin())

# Add the application specific plugins.
PluginManager.add_plugin(PythonCodePlugin())
PluginManager.add_plugin(PythonCodeCodecPlugin())
PluginManager.add_plugin(PythonCodeEditorPlugin())

PluginManager.add_plugin(ProjectPlugin())
PluginManager.add_plugin(ProjectCodecPlugin())
PluginManager.add_plugin(ProjectEditorPlugin())

# Ask for a shell for the user interface.
shell = PluginManager.service(IShell)

# If a command line argument was given try and open it as a project.
if len(sys.argv) > 1:
    shell.open('myorganization.shell.tools.project_editor',
            sys.argv[1], 'myorganization.formats.project')

# Set the shell view's window title and make it visible.
view = IView(shell)
view.window_title = "Python IDE[*]"
view.visible = True

# Enter the event loop.
app.execute()

There are two things to note in this code:

  • The first is the use of the add_plugin() class method as a convenient way of adding a plugin to the default plugin manager. Note that plugins are enabled by default.
  • The second is the use of the service() class method as a convenient way of obtaining a service from the default plugin manager. Note that this returns an object that implements the IShell interface and can be adapted to the IView interface. Compare this with previous examples where we create an object that implemented the IView interface and could be adapted to the IShell interface.

By adopting a plugin based approach we now have an application comprising a set of independent components. Adding a new component requires two lines to be added to the above code, one line to import the new plugin and another to add it to the plugin manager. Although this is a simple change it is a change nevertheless. A future version of the dip.plugins module will support the automatic discovery of plugins in order to deal with this.