Python’s setuptools has a rather interesting feature called “entry points” which facilitates plugin discovery. I’m using this in imhotep to allow linters to live as separate packages.

So how does it work?#

In your setup.py, you defined a property called entry_points which is a dictionary mapping the entry point key to a list of values. The values take the form of .py = imhotep_pylint.plugin:PyLint. This is another mapping in string form from a user-defined string to a dotted module name (and optionally, as here, an object to import).

from setuptools import setup
setup(
    # ...
    entry_points={
        'imhotep_linters': [
            '.py = imhotep_pylint.plugin:PyLint'
        ],
    },
    # ...
)

Taking an example from imhotep_pylint, you can see that this package defines an entry_point for “imhotep_linters”. This string is part of the imhotep API and plugins have to register their entry_points with that. From there, we define a list of one item, which says that the .py string should map to the PyLint object living in imhotep_pylint.plugin.

How do I discover these?#

You can use the pkg_resources package to discover these plugins at run-time. It looks something like this (pulled from imhotep):

def load_plugins():
    tools = []
    for ep in pkg_resources.iter_entry_points(group='imhotep_linters'):
        klass = ep.load()
        tools.append(klass(run))
    return tools

This uses the group name we’ve mutually agreed upon, imhotep_linters, to return an iterable of entry_points. From there, we call the load() method, which will materialize the entry_point into a class object. From there, it’s just like any other sort of class which you can instantiate and use.

This is actually all there is to implementing a plugin system for your package. Python’s setuptools makes this quite easy! You can find [[http://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins][more official docs here]] .