Using Pluggy and Stevedore for Python Plugin Development
This article explains Python plugin architecture, introducing pluggy and stevedore frameworks, showing how to define hook specifications, implement plugins, register them via entry points, and use driver or extension managers to load and invoke plugins in backend applications.
In computing, a plug‑in is a software component that adds a specific feature to an existing program. Python’s dynamic nature makes plugin implementation especially flexible, often using namespace packages and dynamic imports.
Pluggy is a plugin tool derived from pytest . It provides hook specifications and implementations via markers:
<code>hookspec = pluggy.HookspecMarker("eggsample")
hookimpl = pluggy.HookimplMarker("eggsample")</code>Example hook specifications:
<code>import pluggy
hookspec = pluggy.HookspecMarker("eggsample")
@hookspec
def eggsample_add_ingredients(ingredients: tuple):
"""Return a list of ingredients."""
@hookspec
def eggsample_prep_condiments(condiments: dict):
"""Reorganize the condiments tray."""
</code>Corresponding implementation:
<code>import pluggy
hookimpl = pluggy.HookimplMarker("eggsample")
class ExamplePluggy:
@hookimpl
def eggsample_add_ingredients(self):
spices = ["salt", "pepper"]
eggs = ["egg", "egg"]
return spices + eggs
@hookimpl
def eggsample_prep_condiments(self, condiments):
condiments["mint sauce"] = 1
</code>To discover and load external plugins, pluggy uses load_setuptools_entrypoints which scans the entry_points registered under the same namespace:
<code>import itertools, random, pluggy
def get_plugin_manager():
pm = pluggy.PluginManager("eggsample")
pm.add_hookspecs(hookspecs)
pm.load_setuptools_entrypoints("eggsample")
pm.register(ExamplePluggy)
return pm
</code>Plugins are invoked via the manager’s hook, e.g., pm.hook.eggsample_add_ingredients(ingredients) .
External plugins follow the same specification:
<code>import eggsample
@eggsample.hookimpl
def eggsample_add_ingredients(ingredients):
if "egg" in ingredients:
return ["lovely spam", "wonderous spam"]
else:
return ["splendiferous spam", "magnificent spam"]
</code>Packaging registers the plugin in setup.py using the same entry‑point namespace:
<code>from setuptools import setup
setup(
name="eggsample-spam",
install_requires="eggsample",
entry_points={"eggsample": ["spam = eggsample_spam"]},
py_modules=["eggsample_spam"],
)
</code>Stevedore , maintained by OpenStack, offers a similar plugin system with a class‑based interface. A base formatter class is defined:
<code>import abc
class FormatterBase(metaclass=abc.ABCMeta):
"""Base class for example plugins."""
def __init__(self, max_width=60):
self.max_width = max_width
@abc.abstractmethod
def format(self, data):
"""Format the data and return text."""
</code>A simple formatter implementation:
<code>from stevedore.example import base
class Simple(base.FormatterBase):
"""A very basic formatter."""
def format(self, data):
for name, value in sorted(data.items()):
line = f"{name} = {value}\n"
yield line
</code>The package is bundled with an entry_points section that registers the formatter under the stevedore.example.formatter namespace.
<code>setup(
name='stevedore-examples',
entry_points={
'stevedore.example.formatter': [
'simple = stevedore.example.simple:Simple',
'plain = stevedore.example.simple:Simple',
],
},
...
)
</code>Plugins can be loaded as drivers:
<code>import argparse
from stevedore import driver
parser = argparse.ArgumentParser()
parser.add_argument('format', nargs='?', default='simple')
parser.add_argument('--width', default=60, type=int)
args = parser.parse_args()
data = {'a': 'A', 'b': 'B', 'long': 'word ' * 80}
mgr = driver.DriverManager(
namespace='stevedore.example.formatter',
name=args.format,
invoke_on_load=True,
invoke_args=(args.width,)
)
for chunk in mgr.driver.format(data):
print(chunk, end='')
</code>Or as extensions:
<code>import argparse
from stevedore import extension
parser = argparse.ArgumentParser()
parser.add_argument('--width', default=60, type=int)
args = parser.parse_args()
data = {'a': 'A', 'b': 'B', 'long': 'word ' * 80}
mgr = extension.ExtensionManager(
namespace='stevedore.example.formatter',
invoke_on_load=True,
invoke_args=(args.width,)
)
def format_data(ext, data):
return (ext.name, ext.obj.format(data))
results = mgr.map(format_data, data)
for name, result in results:
print(f'Formatter: {name}')
for chunk in result:
print(chunk, end='')
print()
</code>Practical recommendation : based on development experience, stevedore is preferred for its clear base‑class interface, flexible invocation, and lower complexity.
Define interfaces via a base class.
Plugins are more flexible to call.
Stevedore offers superior ease of use.
Python Programming Learning Circle
A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.