How to create a flexible editor

1. Introduction

This tutorial explains how to get working most of all PlugBoard components toghether.

This document would like to be a first shot example made step by step for all parts of, so its target are mainly new comers to PlugBoard.

However, before using this framework then understand this tutorial, you must know the zope.interface and setuptools libraries.

Note

This tutorial uses some components which need to be able to access Python library path (for instance site-packages).

We will use an XML config and PyGTK 2.8

2. Creating the project

Now let's create the skeleton directory structure:

plugboardctl.py create PlugEditor

Notice how this script created for you the setup.py file specifying inside the current version of PlugBoard as dependency and a starting entrypoint string for our application plugins.

3. The main file

PlugBoard, as described in the architecture document, offers an IApplication interface which is the principal component of an application.

Its implementation is found at plugboard.application.Application, so write this simply code:

 plugeditor/main.py

from plugboard import application

def main():
  app = application.Application()

Now we created our IApplication implementation instance. After that, we said in setup.py there's a string which defines a Python path for plugin entrypoints, for instance plugeditor.plugins. So, since we are using a setuptools-based plugin management to find installed plugins, install the IPluginResource as follows:

 plugeditor/main.py

from plugboard import application, plugin

def main():
  app = application.Application()
  pr = plugin.SetuptoolsPluginResource(app, 'plugeditor.plugins')
  pr.refresh()

SetuptoolsPluginResource is a builtin implementation of IPluginResource, which accepts as the first argument the plugin entrypoint path. The method call pr.refresh() is used the retrieve the list of plugins from the given entrypoint path.

Note

The refresh(entrypoint_path=None) method accepts a keyword argument for specifying the entrypoint path for plugins. If no path has been given, it uses the one specified at the initialization.

It also clears the plugins list before retrieving them again from the given entrypoint path.

Once you have been done this, we can start writing plugins. In this kind of application, i chose to make the core (for instance the main window and other related things) as a plugin itself, so it could be changed as well using other implementations.

4. The main window plugin

4.1 The interface

Ok, now let's create the main window interface of the editor:

 plugeditor/plugins/interfaces.py

from plugboard.plugin import IPlugin
import gtk

class IMainWindowPlugin(IPlugin):
  def get_window():
    """
    Returns the main window widget
    """
  get_window.return_type = gtk.Window

Creating an interface for the main window plugin let the application to be very flexible for switching from an implementation to another.

In the code above, we declare a class deriving from IPlugin which contains common methods and informations regarding plugins. The get_window.return_type = gtk.Window directive is just an information for developers who want to create an IMainWindowPlugin implementation.

Note

If you don't know which declaring component of zope.interface the assignment to get_window.return_type is, please read the zope.interface documentation. This is called tag, used to add extra informations over an attribute or method.

4.2 The implementation

Now, we need to implement the previous created IMainWindowPlugin interface:

 plugeditor/plugins/core.py

from plugboard import plugin
from zope.interface import implements
import interfaces, gtk

class MainWindowPlugin(plugin.Plugin):
  implements(interfaces.IMainWindowPlugin)

  def __init__(self, application):
    self.widget = gtk.Window()
    self.widget.set_title("PlugEditor")

  def load(self, context):
    self.widget.show_all()

  def get_widget(self):
    return self.widget

  def set_child(

The initialization just set the instance variable widget value, while on loading the window will be shown.

Note

If you don't know how a plugin is loaded into the application, please read the PlugBoard architecture documentation

Once we done this, we should add this new plugin to the entrypoint, so that SetuptoolsPluginResource can find it:

 setup.py

[...]
entry_points="""
[plugeditor.plugins]
MainWindowPlugin = plugeditor.plugins.core:MainWindowPlugin    
""",
[...]

The first name MainWindowPlugin is not important for the purposes of SetuptoolsPluginResource, however it could be a good idea to keep the name always clear because of other IPluginResource implementations for setuptools which could need it.

5. The context resource

The IContextResource is used to load specific plugins into a specific context. Each context contain a list of plugins which must be listed in IPluginResource. So, according to this concept, let's use the XMLContextPlugin implementation as follows:

 plugeditor/main.py

from plugboard import application, plugin, context
import user, os
from xml.dom import minidom
import gtk

config_file = os.path.join(user.home, '.plugeditor')

def get_config():
  if not os.path.exists(config_file):
    file(config_file, 'w').write('<plugeditor />')
  try:
    return minidom.parse(config_file).documentElement
  except:
    pass

def main():
  app = application.Application()
  pr = plugin.SetuptoolsPluginResource(app, 'plugeditor.plugins')
  pr.refresh()
  config = get_config()
  if not config:
    print >> sys.stderr, "No valid configuration file as been found"
  cr = context.XMLContextResource(app, config)
  cr.refresh()
  cr['default'].load()
  gtk.main()

In the first lines of the code, we simply get the document element (plugeditor) of the XML config file, while in the main() function we pass to the XMLContextResource the got xml element.

The XML document should have the following structure:

 ~/.plugboard

<plugeditor>
  <context name='default'>
    <plugin path='plugeditor.plugins.core.MainWindowPlugin' />
  </context>
</plugeditor>

Notice how we loaded the default context in the code. Logically, everything should be as much dynamic as possible with an appropriate UI to let the user define both contexts and plugins.

Note

XMLContextResource initialization let you specify what names must be used for both elements and attributes; here's the implementation:

def __init__(self, application, dom_element=None,
                   context_element='context', context_name='name',
		   plugin_element='plugin', plugin_path='path')

In the XML config, we defined the previous created plugin to be inserted in the context default then calling the load method will initialize, preload and load the requested plugin.

This kind of structure, allows plugins to have different settings for each context, then each plugin can be present in many contexts.

6. Run the example

If you followed the above instructions and everything has gone well, you can run the example then a window should appear on the screen:

 (Privileged console)

python setup.py develop

 (Python interpreter)

>>> from plugeditor import main
>>> main.main()

Notice how we need to install PlugEditor in the Python path (trough the develop command) because of the setuptools-based plugin. This behavior can change if you'll implement a different IPluginResource for your application without using setuptools.

After we need to import modules and start manually the application; naturally this is done because we didn't provide any script for this task.

If you want to see loaded plugins/contexts just add print list(pr.get_plugins()), list(cr.get_contexts()) in main().

Top^ | All contents by deelan, some rights reserved | Page last modified Feb 26, 2006