Fetchez le Python

Technical blog on the Python programming language, in a pure Frenglish style

Basic plugin system using ABCs and the “extensions” package

I need a very simple plugin system for one of my projects. The project is a small WSGI application called mysysadmin that allows you to launch some commands on your system to manage some applications. It also allows you to view log files in your web browser.

It’s similar in some ways to WebMin,

So in my application, every tab is a plugin that manages one application. I have a plugin for Apache, another one for MySQL, and so one.

Back to my plugin system. So every plugin that is registered becomes a tab in the WSGI application, as long as it provides all the methods my web application needs to interact with it. So I want to check that each plugin strictly provides the API needed by the main program.

The first tools that came in mind were :

But I find both projects a bit complex to implement such a simple plugin system.I could use the standalone Plugins package Phillip provides instead of setuptools, but it still does too much things imho. That’s someting I am currently learning by working on packaging matters : one library should not provide too many features.

Extensions : a simple plugin system

So I have started to implement a light-weight plugin system called extensions, which reuses setuptools entry points principles but is more simple to use. The goal of this project is to provide very simple APIs to handle plugins, and to make it work without introducing a new argument into the setup.py setup method, like setuptools does.

For instance, if you want to define an apache function in your modules module, in your myapp package, you just call the register function :

from extensions import register

register('mysysadmin.modules', 'apache', 'myapp.modules:apache')

That’s it !

And to use it, the mysysadmin application can use a simple API called get, that iterates over all plugins defined for “mysysadmin.modules” :

from extensions import get

for plugin in get(group='mysysadmin.modules'):
    # do something with the plugin

The magic is done by writing in the .egg-info directory installed for the package that contains each plugin, a PLUGIN file that contains the list of registered elements. It’s an idea borrowed from setuptoools entry points. So get iterates over all .egg-info directories in your path and load the PLUGIN files it finds.  Nothing new here. That’s how setuptools does, and that’s perfect.

If you have any feedback on extensions, let me know !

Strict plugins

The other need is to strictly check that every plugin provides the API needed, e.g. fulfill the requirements. This is what we could call Design by contract .

You can provide a base class for this, and ask the plugins to inherit from it. Or you can ask the Plugin to provide a marker to specify it implements a given behavior. zope.interface can do a nice job for the latter,and let you check that a given object implements an interface.

But I wanted to give a shot to the brand new Python ABCs and make sure anyone can write a plugin in plain Python, without having to rely on any kind of marker system. ABCs will let you check that a class implements some methods without requiring it to inherit from a specific class, to implement a specific interface or provide a custom marker. Pure duck typing !

So let’s define for our application a Plugin class, that gives the signature every plugin will need to provide. It uses ABCMeta as its meta class, and the abstractmethod for every method that should be implemented by every plugin.

Here’s an extract :

from abc import ABCMeta, abstractmethod

class Plugin(object):
    __metaclass__ = ABCMeta
    @abstractmethod
    def get_command_list(self):
        return []

    @abstractmethod
    def run_command(self, name):
        pass

    @classmethod
    def __subclasshook__(cls, klass):
        if cls is Plugin:
            for method in cls.__abstractmethods__:
                if any(method in base.__dict__ for base in klass.__mro__):
                    continue
                return NotImplemented
            return True
        return NotImplemented

The __subclasshook__ method is a class method that will be called everytime a class is tested using issubclass(klass, Plugin). In that case, it will check that every method marked with the abstractmethod decorator is provided by the class.

So basically, the application can discover and use the plugins, with:

from extensions import get

for plugin in get(group='mysysadmin.modules'):
    klass = plugin.load()
    if not issubclass(klass, Plugin):
        logging.info('sorry, not a suitable plugin')
        continue
    # do something with the plugin
    xxx

Abstract Base Classes are one honking great idea — let’s do more of those!

Filed under: python

19 Responses

  1. Hey Tarek,

    Probably entirely unrelated but I’ve been working on a configuration system that is a bit like zope.configuration: http://docs.repoze.org/plugin/config.html . The package itself has a bit that finds all the allowed “directives” (think zope.configuration “handlers”). I use plain old setuptools entry points for this. Is “extensions” mostly just a way to prevent folks from needing to change setup.py in this way?

  2. jpc says:

    yup aother article here (the same day ;) about the same subject but(without python2.6 ABC capabilities)

    http://lucumr.pocoo.org/2006/7/3/python-plugin-system

    But Armin’s article don’t accept comments or retro-links

    @++

  3. Complex? All you need is to define an interface:

    >>> from zope import interface, component
    >>> class IPlugin(interface.Interface):
    >>> pass

    To register a plugin after that all the plugin maker needs to do is

    >>> from zope import component
    >>> from the_app.interface import IPlugin
    >>> component.provideUtility(APlugin, IPlugin)

    OK, that’s one more line of import, as you need to import the interface too, compared to your extensions.

    Using the plugins is not too complex either:

    >>> for plugin in component.getAllUtilitiesRegisteredFor(IPlugin):
    >>> print plugin

    But fine, competition is good. But the component architecture isn’t complex to use. The innards are, but that’s because making a plugin system is the simplest thing you can do with it. It does so much more that you can’t do even with ABCs.

  4. Tarek Ziadé says:

    @Lennart: I don’t want to force plugin makers to use a given interface system. With ABCs duck typing they don’t have to use any third-party package like zope.interface.

    Plus, how do you register your plugin in Python ? Here, the plugin is made available as long as you install your package into Python.

  5. That is true, in my example above the plugin module needs to be imported, so you would also need to tell the main application about it somehow. On the other hand, that means you can turn the existance of the plugins on and off.

    And you don’t have to actually *use* the interface system. The interface is just used as a marker for what kind of plugins it is. As you see from my code, the plugin doesn’t have to implement the interface.

  6. Tarek Ziadé says:

    @Lennart

    > And you don’t have to actually *use* the interface system.
    > The interface is just used as a marker for what kind of plugins it is.
    > As you see from my code, the plugin doesn’t have to implement the interface.

    Therefore a simple string can be used to mark a plugin. Armin called it a “capability”
    in his post, I would call it a “contract”. Duck typing is another way to check if a plugin
    implements a contract. So, basically, there’s no real need of zope.interface for a simple plugin system.

    I like Duck typing checking the most because there’s no need to use any marker on the plugin side.

  7. “Therefore a simple string can be used to mark a plugin.”

    This is probably not the best of ideas. Strings are not type checked, so you easily get typos and debugging of the type “why doesn’t my plugin show up?”. Some formalism is good. And your ABC is kind of like an interface, right?

    I’m not saying your system is bad, but I am a bit suspicious that you invented something new more because it was fun to do so, than because it was strictly necessary. Not a bad reason, of course. I do it all the time. :)

    Martin

  8. Tarek Ziadé says:

    @Martin :

    “I’m not saying your system is bad, but I am a bit suspicious that you invented something new more because it was fun to do so, than because it was strictly necessary. Not a bad reason, of course. I do it all the time. :)

    I am not sure what are you trying to say here. Is that “use zope.interface, don’t reinvent the wheel ?”

    So basically you are saying (as I understand it) is that I *have* use zope.interface to check my plugins and depend on this lib rather than use the ABC system already present in Python stdlib for this use case ?

    Now you are free to invent new concepts for fun. But don’t misunderstand what I am doing here : I am not
    inventing anything new. I am just using the power of Python stdlib.

  9. Hi Tarek,

    I don’t mean you have to use zope.interface (and zope.component), but you have in effect just created your own plugin system (‘extension’) that competes with it, and with the various other plugin systems out there. There’s nothing wrong with that. I’m just not sure I see what it adds. And if I were to pick a plugin system for an application I was using, I don’t think I’d be using one that uses plain strings as discriminators. In my experience, that’s too error prone and brittle.

    Martin

  10. Tarek Ziadé says:

    @Martin:

    Hi Martin ;)

    “extensions” has nothing to do with discriminators, and doesn’t compete with zope.interface or zope.component. It just provides a global registry mechanism, global to Python itself. It also provides a global plugin discovery system.

    Nothing invented here, that’s from setuptools entry points. Now you could argue that I have created something that competes with setuptools, and that is entirely true :)

    But as far as I know, there’s no way with zope.interface or zope.component to provide such global mechanisms unless you explicitely provide a registry mechanism and you make sure all plugins are loaded when your application runs. (like zcml in zope or plone)

    Now for the discriminator part, it’s up to your application to decide wheter it checks the plugins or not. And you are free to use any system here: plain strings (what armin called “capabilities”), duck typing with ABCs, or zope.interface for this part.

    And I think ABC is sufficient for that, and it happens to be available in Python itself, so..

    • “But as far as I know, there’s no way with zope.interface or zope.component to provide such global mechanisms unless you explicitely provide a registry mechanism and you make sure all plugins are loaded when your application runs. (like zcml in zope or plone)”

      Mmm… how is what you did different from zope.component.provideUtility()?

      Martin

  11. Tarek Ziadé says:

    @Martin : AFAIK, “zope.component.provideUtility()” works with a global site manager instance, which have to be loaded in memory by your application. You also have to load the code of each plugin that make a call to “zope.component.provideUtility”. So at some point, you have to list the plugins to load somewhere. (in ZCML files for instance)

    With “extensions”, the registery is written statically in .egg-info when you install each package. So you can do plugin discovery without having to load any code.

    You could compare this in some ways to zcml slugs Zope packages are writing to register themselves in your zope app. But this time, there’s no need to write any slug file, and any extra mechanism. There’s nothing to load in memory either, unless you want to use it.

    This is a brilliant idea, and this is what Phillip has created with entry points. You use this mechanism every time you run a buildout : recipes are entry points, and zc.buildout doesn’t ask you to use interfaces. zc.buildout iterates through entry points, and does not use zope.component neither zope.interface for that. But recipes *are* plugins.

  12. “So, basically, there’s no real need of zope.interface for a simple plugin system.”

    Of course not. My point is that I don’t think using the CA is complex, and that your system isn’t significantly less complex to use. There may be other reasons not to use it, but complexity of usage is not one.

  13. The whole above conversation is bizarre. Tarek’s “extensions” package has nothing to do with zope.component/zope.interface/zope.configuration. It’s more or less a way to define setuptools entry points without changing the setup.py file of your project. So if you’re complaining about Tarek’s thing, you should also complain about setuptools entry points, because it’s just candy on top of them.

  14. Hi Tarek,

    I hadn’t understood that this went into egg-info (register(‘mysysadmin.modules’, ‘apache’, ‘myapp.modules:apache’) looks like Python code to me…). That is indeed a nice feature, at least if you want the “all-installed-packages-are-loaded” behaviour that you also get with entry points.

    Martin

  15. Tarek Ziadé says:

    @Chris sorry, I have unspamed your comments (I don’t know why WP does this without asking me)

    1. about your first comment : absolutely. And this also allows someone that doesn’t want to use setuptools to run his setup.py to be able to use the entry points feature.

    2. the second comment : I think it’s a misunderstanding on how entry points works actually

    @Martin : you have the same behavior with entry points. you can load them or not. (see EntryPoint.load() )

  16. I also don’t think it competes with zope.component. The usecases are different. BTW, with zope.component you don’t have to create and maintain the global registry yourself, since it is automatically created at import time.

  17. Tarek Ziadé says:

    @Christophe: Yes, at *import* time. Whereas entry points (therefor extensions) can discover plugins without loading any code. And decide what to load. It’s a static registery that tells where is the code, but does not explicitely lods it. Plugins are therefore proxies in that case.

  18. @Christophe: Of course it’s competition. And competition is *good*. Nobody (well certainly not me) is saying Tarek shouldn’t have made another plugin system. The more plugin systems the merrier. If it’s good people will use it, if it’s not they won’t.

    The Component Architecture of course covers many more usecases than just making plugins, but making plugins is definitely one of the usecases covered by the CA. Therefore another plugin system is competition. If you want to use CA or this, or any other way of doing plugins is a matter of taste and details, and that’s why it’s good to have many options.

Leave a Reply