Plugins

ockham edited this page Dec 17, 2012 · 1 revision

This page describes the plugin architecture of Gourmet.

Gourmet's plugin architecture was developed to allow development to focus on simplifying and streamlining the main interface without losing any power and without closing down the development of future functionality.

At this point, a great number of features are plugins, and therefore can be turned off -- most notably, all functionality exposing the concept of the "ingredient key" to users is a plugin, as is all import and export code, as is nutritional information. Another advantage of the plugin system is that if a component fails for whatever reason -- for example, if there is a missing dependency for one of the export plugins -- Gourmet as a whole can still run successfully.

It is my assumption that the plugin architecture will develop and mature as more plugins are written. As that happens, and as new contributors join the project, my hope is that this wiki can serve as the home of documentation for future developers.

Table of Contents

The Plugin Architecture - the base classes

The basic plugin architecture of Gourmet is based in two main sets of classes:

1. The base Plugin classes, defined in plugin.py. All plugins must subclass the base plugins.
2. The base Pluggable class, defined in plugin_loader.py.

Each Pluggable class accepts a certain subset of Plugin base classes as plugins. Any plugin registered with Gourmet of that class will be registered with the base class. Every feature of Gourmet that a plugin writer could want access will need to subclass Pluggable. For example, the recipe card view is a pluggable, the database masterclass is a plugin, and so on. The Pluggable classes all access plugins through the singleton MasterLoader class (plugin_gui.PluginChooser provides a dialog that users use to enable and disable plugins).

Note that each Plugin class provides a plugin to *one* aspect of Gourmet. However, what a *user* thinks of as a plugin will often comprise many different Plugin classes -- the nutrition plugin, for example, must provide different classes to plug in to the database, (DatabasePlugin), the recipe card (RecipeCardPlugin), the base exporters (BaseExporterPlugin), etc.

What Goes Into a Plugin

Plugins that ship with Gourmet are all in the src/lib/plugins/ directory. If you want to experiment with a plugin, you can also try adding a plugin to your local gourmet directory underneath a subdirectory "plugins" (~/.gourmet/plugins).

Inevitably the best way to learn about plugins will be to find a plugin that does something similar to what you want to do, and read the code. However, it's also a good idea to read the below to get an overview of how it all works.

The .gourmet-plugin file

Each plugin is described in a file that ends in a .gourmet-plugin suffix. Gourmet will search the default plugin directories for files with the .gourmet-plugin suffix to learn about plugins. The .gourmet-plugin file provides the info that will be provided to the user about the plugin and provides the name of the module that will be imported to initialize the plugin. Below are the contents of key_editor.gourmet-plugin (note -- actually we create a .in file with fields marked for translation which are then fed to intltool to generate the .gourmet-plugin file with proper translations).

[Gourmet Plugin]
Module = key_editor
Version = 1.0
API_Version = 1.0
Name = Key Editor
Comment = Assign and edit ingredient keys (unique identifiers for ingredients, used for consolidating items on shopping lists).
Authors = Thomas M. Hinkle <thomas_hinkle@sf.net>
API_Version - The API_Version field is there as a precaution -- if the Gourmet plugin API develops in ways that aren't backward-compatible, we'll update that number and it will let us warn users about plugins that are no longer compatible. At present, we don't do anything with it.
Version - this is the version number for the plugin itself.
Comment - this is the sentence-or-two description the user sees of the plugin.
Name - this is the name of the plugin that the user sees
Module - this points us to our python code.

The module

Gourmet will import the module named in the gourmet-plugin file (it should be in the same directory). The module needs to define one variable -- the plugins variable, which is a list of plugin classes that make up this plugin. As mentioned above, what the user experiences as a plugin may actually be more than one plugin. Let me give some examples.
key_editor

plugins = [KeyEditorPlugin]
The key_editor module defines a plugins list with just one item -- KeyEditorPlugin. The KeyEditorPlugin class is a subclass of the ToolPlugin -- a plugin that is added to the Tools menu of the recipe card and recipe index views. In this case, the plugin simply provides a dialog that allows the user to edit ingredient keys en masse.

nutritional information

plugins = [
    data_plugin.NutritionDataPlugin,
    main_plugin.NutritionMainPlugin,
    reccard_plugin.NutritionDisplayPlugin,
    export_plugin.NutritionBaseExporterPlugin
    ]
Adding nutritional information to Gourmet is a much more complicated matter. The data_plugin adds tables to the Gourmet database to handle nutritional information. The recipe card plugin adds nutritional information to the recipe card display. The export plugin writes nutritional information into all exports (so that nutritional information shows up in printouts, pdfs, etc), and so on.

The plugin classes

The classes listed in the modules actually do all the work of being a plugin. In each case, you're subclassing one of the classes in src/lib/plugin.py, so you start by importing that base class and then creating the subclass.

The basics are as follows:
The plugin class is instantiated when the plugin is first activated.
For each pluggable the plugin plugs into, the activate() method is called with the pluggable as its argument -- this allows the plugin to access the pluggable and do things to it. This is called once per pluggable instance either when the plugin is activated by the user or when the pluggable is instantiated (if the pluggable is already turned on).

Different plugin base classes provide convenience methods to make simple plugins easier to do. For example, for a number of classes you can use UIManager's convenience to add menu items and actions simply by creating the write XML string with the menu description and creating an ActionManager with the actions described.

Example plugins

Plugin base classes allow plugin writers to access virtually every aspect of the code, including:

1. The database (adding tables, accessing and storing data, etc) 2. The menus 3. The base interfaces (it's easy to add a tab with new functionality to the main interface or the recipe card) 4. import/export & print functionality

If you are looking for examples, here are some pointers:

1. A simple plugin that provides a "tool" -- see "Unit Converter" plugin. 2. A plugin that modifies every textbox in a recipe card -- see "Spellcheck" plugin 3. A plugin that provides print support - see pdf_export plugin 4. A plugin that adds a tab to the main interface - see recipe browser plugin 5. A complex plugin that modifies nearly everything -- see nutritional information plugin.