Skip to content
Peter Levi edited this page Jan 12, 2025 · 1 revision

This is a short tutorial on writing plugins for Variety.

The plugin system in Variety is for now very very young and very much a work in progress. Plugins are written as simple Python classes that are then put somewhere inside the ~/.config/variety/plugins folder - either directly, or they can have their own subfolder.

Variety comes bundled with some plugins that reside in /opt/extras.ubuntu.com/variety/variety/plugins/. They are simple enough and if you take a look at them you might not need to read this whole tutorial below.

The folder ~/.config/variety/pluginconfig is used to store any data that plugins might need to persist. The folder structure below mirrors the one in the plugins folder, so there will be subfolders here too. The plugin API provides a method to get the correct path for writing data, read below.

The base classes for plugins are IPlugin and IVarietyPlugin:

class IPlugin(object):
    """
    The most simple interface to be inherited when creating a plugin.
    """

    @classmethod   # IMPORTANT that this is a classmethod, not an instance method
    def get_info(cls):
        """
        Returns the basic info about the plugin. Please make sure the name is unique among all Variety plugins
        Format:
        return {
           "name": "Sample name",
           "description": "Sample description",
           "version": "1.0",
           "author": "Author name", # optional
           "url": "Plugin URL"  # optional
        }
        """
        return {}

    def __init__(self):
        """
        All plugins must have a default constructor with no parameters.
        Remember to call super.
        """
        self.active = False

        # These will be filled in by Jumble.load() and available before the first activate() call
        self.jumble = None
        self.path = None # Path to the plugin python file
        self.folder = None # Folder where plugin is located (can be used for loading UI resources, etc.).
        # This folder may be read-only. A separate config folder convention should be used to store config files.

    def activate(self):
        """
        Called at plugin activation. Please do not allocate large portions of memory or resources before this is called.
        Remember to call super first.
        This method can be called multiple times within a session.
        It may be called when the plugin is already active - in this case it should simply return.
        """
        if self.active:
            return
        self.active = True

    def deactivate(self):
        """
        Called when the plugin is disabled. Please free used memory and resources here.
        Remember to call super first.
        This method can be called multiple times within a session.
        It may be called when the plugin is already inactive - in this case it should simply return.
        """
        self.active = False

    def is_active(self):
        return self.active
import os
from jumble.IPlugin import IPlugin
from variety.Util import Util

class IVarietyPlugin(IPlugin):
    """
    Variety-specific plugin interface
    """
    def activate(self):
        super(IVarietyPlugin, self).activate()
        self.config_folder = os.path.join(self.jumble.parent.config_folder, "pluginconfig/" + os.path.basename(self.folder))
        Util.makedirs(self.config_folder)

    def get_config_folder(self):
        """
        :return: The config directory which the plugin can use to store config or cache files
        """
        return self.config_folder

Quote plugins

For now Variety only supports quotes plugins, nothing else. The user can enable or disable these from the quote-related preferences.

Below is the IQuoteSource interface and the source code of the Local files quotes source - it illustrates all the methods that a quote plugin for Variety must implement.

from IVarietyPlugin import IVarietyPlugin

class IQuoteSource(IVarietyPlugin):
    def supports_search(self):
        """
        False means that this plugins does not support searching by keyword or author (only get_random will
        ever be called) and this plugin will be used only if the user has not specified search criteria.
        True means get_for_keyword and get_for_author should also be implemented.
        :return: True or False
        """
        return False

    def get_random(self):
        """
        Returns some quotes.
        Individual quotes are hashes like the one below. Only quote should be non-null, the others can be None.
        """
        return [{
            "quote": "Quote",
            "author": "Author",
            "sourceName": "My Quote Site",
            "link": "http://example.com"
        }]

    def get_for_keyword(self, keyword):
        """
        Returns some quotes matching the given keyword.
        Returns [] if it cannot find matches.
        """
        return []

    def get_for_author(self, author):
        """
        Returns some quotes matching the given author.
        Returns [] if it cannot find matches.
        """
        return []

Full example

And here is the full example of a working quotes plugin:

import os
import re
from variety.plugins.IQuoteSource import IQuoteSource
from gettext import gettext as _
import logging

logger = logging.getLogger("variety")


class LocalFilesSource(IQuoteSource):
    def __init__(self):
        super(IQuoteSource, self).__init__()
        self.quotes = []

    @classmethod
    def get_info(cls):
        return {
            "name": "Local text files",
            "description": _("Displays quotes, defined in local text files.\n"
                             "Put your own txt files in: ~/.config/variety/pluginconfig/quotes/.\n"
                             "The file format is:\n\nquote -- author\n.\nsecond quote -- another author\n.\netc...\n\n"
                             "Example: http://rvelthuis.de/zips/quotes.txt"),
            "author": "Peter Levi",
            "version": "0.1"
        }

    def supports_search(self):
        return True

    def activate(self):
        if self.active:
            return

        super(LocalFilesSource, self).activate()

        self.quotes = []

        # prefer files in the pluginconfig
        for f in os.listdir(self.get_config_folder()):
            if f.endswith(".txt"):
                self.load(os.path.join(self.get_config_folder(), f))

        # use the defaults if nothing useful in pluginconfig
        if not self.quotes:
            for f in os.listdir(self.folder):
                if f.endswith(".txt"):
                    self.load(os.path.join(self.folder, f))

    def deactivate(self):
        self.quotes = []

    def load(self, path):
        try:
            logger.info("Loading quotes file %s" % path)
            with open(path) as f:
                s = f.read().decode('utf-8', errors='ignore')
                for q in re.split(r'(^\.$|^%$)', s, flags=re.MULTILINE):
                    try:
                        if q.strip():
                            parts = q.split('-- ')
                            quote = ' '.join(parts[0].split())
                            if quote[0] == quote[-1] == '"':
                                quote = u"\u201C%s\u201D" % quote[1:-1]
                            author = parts[1].strip() if len(parts) > 1 else None
                            self.quotes.append({"quote": quote, "author": author, "sourceName": os.path.basename(path)})
                    except Exception:
                        logger.debug('Could not process local quote %s' % q)
        except Exception:
            logger.exception("Could not load quotes file %s" % path)

    def get_random(self):
        return self.quotes

    def get_for_author(self, author):
        return [q for q in self.quotes if q["author"] and q["author"].lower().find(author.lower()) >= 0]

    def get_for_keyword(self, keyword):
        return self.get_for_author(keyword) + \
               [q for q in self.quotes if q["quote"].lower().find(keyword.lower()) >= 0]
Clone this wiki locally