Python Plugins with Yapsy

I have a little Python/GTK+ project that I work on here and there. It's
a little text editor that I've re-written a dozen times (first few versions
were written in C actually). Really, it's more of a platform for me to
experiment with GTK+ and various GUI design techniques. I decided to scrap
the plugin system which I had hacked together from scratch and replace it with
something based on Yapsy (Yet Another Plugin
SYstem).

Yapsy isn't a complete plugin framework but a rather simple system from which to
build your own complete plugin system. Using only the Python standard library,
Yapsy wasn't too far off from what I was tring to implement on my own. But, I
liked some of it's approaches much better than what I had come up with. Since
there are not many examples out there, I wrote an
Example Python/GTK+ Application using Yapsy
to demonstrate some of the techniques I'm playing with and put a TON of comments
throughout the code.

The Plugin Manager

The Yapsy PluginManager
class handles locating, loading, and activating plugins. However, Yapsy also
provides a singleton version of the class. I prefer using the singleton pattern
for the plugin manager rather than passing around the instance to all the
components of my application.

The PluginManager is given a list of directories in which to search for
plugins. These directories contain special INI-style information files
for each plugin as well as the Python code for the actual plugins. By passing
the PluginManager directories based on the
XDG Base Directory Specification,
the end user could install plugins by simply copying them to their home folder
data directory (eg. ~/.local/share/my-application/plugins) yet your application can
also install plugins to the system data directory (eg.
/usr/share/my-application/plugins).

Now, you may be wondering what happens if the same plugin exists in different
directories. Perhaps even different versions of the same plugin. Fear not, Yapsy
has a PluginManagerDecorator for just that purpose. Prior to getting the
manager PluginManager instance, you can call the setBehavior() method to
install the VersionedPluginManager decorator.

Now the PluginManager will only load the most recent version of a plugin.

Another handy PluginManagerDecorator is ConfigurablePluginManager which
will allow the plugin manager to load/save which plugins have been activated to
a configuration file (as well as plugin settings). The ConfigurablePluginManager
is installed in the same way as the VersionedPluginManager.

In order for the PluginManager to save the activated plugins and re-load them
upon startup, a ConfigParser must be passed to the manager. The
XDG Base Directory Specification
comes in handy once again when saving your configuration file.

Once the PluginManager is setup, the plugins can be located and loaded using
the collectPlugins() method.

manager.collectPlugins()

If the manager was setup with ConfigurablePluginManager then the plugins that
were last activated by the user will also be automatically activated.

The Plugins

Each of the plugins manager.collectPlugins() consists of at least 2 files.
First, the plugin information file provides some meta information about the
plugin, which I use in a Gtk.About dialog, and a Python module that defines
a class which extends the Yapsy IPlugin interface. It's this class that
extends IPlugin that is the plugin code. What that plugin can do is up to you.

In a real world application you would actually create your base plugin
class(es) which would extend IPlugin and then your application's plugins would
instead extend your base plugin class. But, just directly extending IPlugin is
a good way to get started.

Now, in a basic python application you would iterate over
PluginManager.getAllPlugins() or PluginManager.getPluginsOfCategory() to
call pre-determined methods on any activated plugins at specific places in your
application code. However, in a GTK+ application you can simply give the plugins
access to specific objects and let the plugins connect to GObject signals.

For example, if your application extends Gtk.Application, a plugin may then
connect to the "window-added" signal to add it's GUI components to any
application window. The plugin would override the IPlugin.activate() method
to add GUI components/connect to signals and the IPlugin.deactivate() method
to remove those GUI components and disconnect the signal handlers. This concept
is pretty well demonstrated in my
Example Python/GTK+ Application using Yapsy

The only tricky part is giving your plugins access to your objects. Since the
PluginManager instantiates the plugin and calls it's activate() and
deactivate() methods, you cannot specify additional arguments. You could
extend PluginManager with your own manager class and that would indeed be an
ideal solution for some situations. However, my little text editor only needs
the plugins to have access to my application object which has an API for everything
they would need. My solution is to simply give the PluginManager a reference
to the application object, and let the plugins get it from the manager singleton.

Plugin List Widget

My example project includes a couple of widgets in a file aptly named
widgets.py
for displaying the list of installed plugins. The plugins can be activated and
deactivated via checkboxes in the list and an about dialog shows the meta
information for the plugin.

The list (which is a Gtk.TreeView) is populated by iterating
PluginManager.getAllPlugins(). This method gives you a list of PluginInfo
objects which contain the meta information for the plugin (name, version,
author, description, etc.) as well as the instance of the plugin itself in a
property named plugin_object.

When the checkbox in the PluginTreeView is toggled,
PluginManager.activatePluginByName() or PluginManager.deactivatePluginByName()
is called. Since PluginManager was setup with ConfigurablePluginManager, the
ConfigParser is changed to reflect which plugins are active.

Viola--it's that simple. If I end up actually using these widgets in my project,
I will clean them up and make them a little more versatile and release them
separately.

Final Thoughts

All in all I'm pretty impressed with Yapsy. Right out of box (so to speak) it is
handling locating, loading, activating/deactivating, versions, and configurations
for my plugin system. And it does all this with only the Python standard library.
This allows me to focus on how the plugins will play with my application, what
API they will have access to, how to handle exceptions within plugins, etc.

Did you enjoy Python Plugins with Yapsy? If you would like to help
support my work, A donation of a buck or two would be very much appreciated.