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.
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.
Going back to our python_ide.py example lets assume that we have restructured the code along the following lines.
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)
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, '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:
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.