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, itsPythonCodeFactory
factory class and the adapter betweenPythonCode
and theIManagedModel
interface. python_code_codec.py
- This contains the adapter between
PythonCode
and theIFilterHints
,IUnicodeDecoder
andIUnicodeEncoder
interfaces. python_code_editor.py
- This contains the
PythonCodeEditorTool
class. project.py
- This contains the
Project
class, itsProjectFactory
factory class and the adapter betweenProject
and theIManagedModel
interface. project_codec.py
- This contains the adapter between
Project
and theIFilterHints
,IXmlDecoder
andIXmlEncoder
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 theIShell
interface and can be adapted to theIView
interface. Compare this with previous examples where we create an object that implemented theIView
interface and could be adapted to theIShell
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.