Plugin Framework¶
Freeseer uses plugins so developers can easily extend the capabilities of Freeseer in a modular fashion.
Freeseer’s plugin framework is built on Yapsy, a minimal plugin system that only depends on Python’s standard library.
Plugin System Setup¶
Yapsy’s PluginManager
class provides the core logic needed to find, load,
and activate plugins. Freeseer has a PluginManager
class that builds
on top of that, and can be found in src/freeseer/framework/plugin.py
.
Yapsy provides a PluginFileLocator
class which locates plugins when they are
accessible via the filesystem. [1] Plugins are described by a text file
called the plugin info file which have a “.yapsy-plugin” extension by default.
But Freeseer plugins use a customized extension, “.freeseer-plugin”.
Todo
introduce the code snippet so it’s not shown as a surprise
from yapsy.PluginManager import PluginManagerSingleton
from yapsy.ConfigurablePluginManager import ConfigurablePluginManager
...
class PluginManager(QtCore.QObject):
"""Freeseer's Plugin Manager provides plugin support."""
def __init__(...):
...
PluginManagerSingleton.setBehaviour([ConfigurablePluginManager])
self.plugmanc = PluginManagerSingleton.get()
...
locator = self.plugmanc.getPluginLocator()
locator.setPluginInfoExtension("freeseer-plugin")
...
Freeseer searches three different directory paths for plugins:
- User’s HOME directory (
~/.freeseer/plugins/
) - Relative to the src directory (
src/freeseer/plugins/
) - If you installed Freeseer, the Python installation packages (
site-packages/freeseer/plugins/
)
Yapsy’s IPlugin
class defines the minimal interface needed for Yapsy
plugins. We want Freeseer’s plugin classes to have a richer interface than what
IPlugin
provides, so we created the IBackendPlugin
class in
freeseer/framework/plugin.py
which inherits from IPlugin
and defines the
minimal interface for all Freeseer plugin classes. All Freeseer plugins should
descend from the IBackendPlugin
class.
from yapsy.IPlugin import IPlugin
...
class IBackendPlugin(IPlugin):
"""Defines the interface for all Freeseer plugins."""
CATEGORY = "Undefined"
...
class IAudioInput(IBackendPlugin):
"""A Freeseer plugin for Audio Input."""
CATEGORY = "AudioInput"
...
Each class that is a descendant of the IPlugin
class needs a CATEGORY
attribute defined. When you are writing your own Freeseer plugin, you often
don’t need to define a new category. You can extend one of the existing plugin
classes and will not need to override the CATEGORY
attribute.
If you are creating a new category, you will need to override the CATEGORY
attribute and add the new category name and class name to the PluginManager’s
category filter in the form of a key-value pair, where the key is the
plugin’s category and the value is the plugin’s classname.
class PluginManager(QtCore.QObject):
...
self.plugmanc.setCategoriesFilter({
IAudioInput.CATEGORY: IAudioInput,
IAudioMixer.CATEGORY: IAudioMixer,
IVideoInput.CATEGORY: IVideoInput,
IVideoMixer.CATEGORY: IVideoMixer,
IImporter.CATEGORY: IImporter,
IOutput.CATEGORY: IOutput})
self.plugmanc.collectPlugins()
...
Yapsy provides a number of useful decorators for its PluginManager which modify
behaviour. Freeseer’s plugin system uses the ConfigurablePluginManager
which
allows Freeseer to save and load the active plugins and their settings to
a configuration file.
from yapsy.ConfigurablePluginManager import ConfigurablePluginManager
...
class PluginManager(QtCore.QObject):
...
PluginManagerSingleton.setBehaviour([ConfigurablePluginManager])
...
Many of the Freeseer plugins, such as the video and audio plugins, use the
ConfigurablePluginManager
to save the active plugins.
Creating a Plugin¶
The basic steps for creating a new plugin are:
Write a plugin info file,
plugin_name.freeseer-plugin
, inside the appropriate directory withinsrc/freeseer/plugins/
. This file will hold metadata for the plugin and has the following format:[Core] Name = Plugin Name Module = plugin_module_or_directory [Documentation] Author = Your Name Version = Latest version of Freeseer that your plugin is compatible with Website = http://fosslc.org Description = Simple one-sentence plugin description
Create the plugin Python file(s)
- If you are creating a single-file plugin, create a Python module with the same name as your plugin info file:
plugin_name.freeseer-plugin plugin_name.py
- If you are creating a multi-file plugin, your Python modules should be separated from your plugin info file:
- Create a plugin directory with the same name as your plugin info file (minus the extension).
- In the new plugin directory, create the file
__init__.py
and write your plugin class inside it. Your class should extend one of theIBackendPlugin
subclasses (e.g.IAudioInput
). Don’t forget to override the class atributename
with your plugin’s name.- Add other useful plugin code in other modules if necessary. For example, if your plugin requires a GUI, create a module called
widget.py
inside your plugin’s directory and import it inside your plugin’s__init__.py
module.
Accessing a Plugin¶
Any modules that need to access the plugins will need to import Freeseer’s
PluginManager
:
from freeseer.framework.plugin import PluginManager
There are a number of ways to access the plugins via the PluginManager
. You
can iterate over all of the plugins (or all of the plugins in a given category)
or you can access a specific plugin by its name.
While Yapsy provides methods for accessing plugins (e.g. getAllPlugin()
,
getPluginsOfCategory()
, and getPluginByName()
), the recommended way to
access the plugins is to use the accessor methods provided by Freeseer’s
PluginManager
:
get_plugin_by_name(name, category)
get_all_plugins()
get_plugins_of_category(category)
get_audioinput_plugins()
get_audiomixer_plugins()
get_videoinput_plugins()
get_videomixer_plugins()
get_importer_plugins()
get_output_plugins()
When you call any of the above accessor methods, you receive a PluginInfo
object or a list of PluginInfo
objects. Such an object contains meta
information about the plugin. Each PluginInfo
object has an attribute
plugin_object
which returns an instance of the plugin which you can then
use.
For example:
plugman = PluginManager(config_dir)
plugin_info = plugman.get_plugin_by_name(name, category)
plugin = plugin_info.plugin_object
plugin.do_something()
As another example, here’s a snippet of the Freeseer codebase where a class uses
a plugin. It does so by creating an instance of the PluginManager and then calls the
plugin by name, using the plugin_object
attribute to access the plugin object:
from freeseer.framework.plugin import PluginManager
...
class QtDBConnector(object):
def __init__(self, config_dir, ...):
...
self.plugman = PluginManager(config_dir)
...
...
def add_talks_from_rss(self, feed_url):
"""Adds talks from an RSS feed."""
plugin = self.plugman.get_plugin_by_name("Rss FeedParser", "Importer")
feedparser = plugin.plugin_object
presentations = feedparser.get_presentations(feed_url)
if presentations:
for presentation in presentations:
talk = Presentation(presentation["Title"],
presentation["Speaker"],
presentation["Abstract"], # Description
presentation["Level"],
presentation["Event"],
presentation["Room"],
presentation["Time"])
self.insert_presentation(talk)
else:
log.info("RSS: No data found")
Footnotes
[1] | The plugins are detected through Python, so all directories leading
to plugins should have an __init__.py file in them. |