Working on the upcoming version of Keryx, we’ve gained some interest in making Keryx support many different Linux distributions. This means we will have to support other package management systems, more than simply APT. A sort of plugin system is needed for this and it would need to be easy to use. Our frontends to this library will need to determine what features are available so the interface can be modified accordingly. So how do you approach this without making it terribly complicated?

Well first off we want to make sure the plugins load from a specific folder. I decided to make plugins be either single files or full directories. If the option to use a full directory is used, the init.py file must contain the plugin description info and primary class. In order to keep this easily maintainable, the plugin manager is simply a class that contains a dictionary of the plugins. Numerous instances of the plugins can be created as well as separate plugin systems (for example, maybe you were wanting to do a backend plugin system and a separate frontend plugin system). Creating multiple instances of a plugin is also important. We do this by keeping the module stored in the manager instead of an instance. This way the manager is simply a way to find and create instances of plugins.

Here’s a prototype of what I have described here, reasonably well commented. I called them “definitions” instead of “plugins” in case you were wondering:

import logging
import os
import sys


class DefinitionManager:
    """Definition Manager

    Loads the definitions from a folder and manages them
    """


    definitions = {}


    def __init__(self, folder):
        """Load all available definitions stored in folder"""

        #TODO: User path and variable expansions
        folder = os.path.abspath(folder)

        if not os.path.isdir(folder):
            logging.error("Unable to load plugins because '%s' is not a folder" % folder)
            return

        # Append the folder because we need straight access
        sys.path.append(folder)

        # Build list of folders in directory
        to_import = [f for f in os.listdir(folder) if not f.endswith(".pyc")]

        # Do the actual importing
        for module in to_import:
            self.__initialize_def(module)


    def __initialize_def(self, module):
        """Attempt to load the definition"""

        # Import works the same for py files and package modules so strip!
        if module.endswith(".py"):
            name = module [:-3]
        else:
            name = module

        # Do the actual import
        __import__(name)
        definition = sys.modules[name]

        # Add the definition only if the class is available
        if hasattr(definition, definition.info["class"]):
            self.definitions[definition.info["name"]] = definition
            logging.info("Loaded %s" % name)


    def new_instance(self, name, *args, **kwargs):
        """Creates a new instance of a definition
        name - name of the definition to create

        any other parameters passed will be sent to the __init__ function
        of the definition, including those passed by keyword
        """
        definition = self.definitions[name]
        return getattr(definition, definition.info["class"])(*args, **kwargs)

I’ve used this approach for backend plugins on Unwrapt, a platform independent package management system emulator. To see my up-to-date implementation head  over at http://github.com/excid3/unwrapt in DefinitionManager.py.

Leave a comment if you have any questions or suggestions.

comments powered by Disqus