













Recently I came upon Pluggy, a Python library for developing plugin systems. It was originally developed as part of the pytest project - known for its rich plugin ecosystem - and later extracted into a standalone library. You're supposed to reach out for Pluggy if you want to add a plugin system to your tool or library and want to use something proven rather than rolling your own.
In this post I will share some notes on how Pluggy works, and will then review how it aligns with the fundamental concepts of plugin infrastructures.

Pluggy is built around the concept of hooks: functions that host applications or tools (from here on, just "hosts") expose and plugins implement. A host exposes hooks by using a decorator returned from pluggy.HookspecMarker and a plugin implements this hook using a decorator returned from pluggy.HookimplMarker.
Pluggy's documentation explains this fairly well; in this post, I'll show how to implement the htmlize tool with some plugins, introduced in the original article in my plugin series.
As a reminder, htmlize is a toy tool that takes markup notation similar to reStructuredText, and converts it to to HTML. It supports plugins to handle custom "roles" like:
some text :role:`customized text` and more text
As well as plugins that do arbitrary processing on the entire text.
Out host defines two hooks:
import pluggy hookspec = pluggy.HookspecMarker("htmlize") @hookspec(firstresult=True) def htmlize_role_handler(role_name): """Return a function accepting role contents. The function will be called with a single argument - the role contents, and should return what the role gets replaced with. """ pass @hookspec def htmlize_contents(post, db): """Return a function accepting full document contents. The function will be called with a single argument - the document contents (after paragraph splitting and role processing), and should return the transformed contents. """ pass
A hook is created by calling HookspecMarker with the project's name. This project name has to match between the host and its plugins. Pluggy is permissive about what hooks accept as parameters and what they return; for maximal flexibility and to stay true to the original htmlize example, our hooks return functions.
To accompany this HookspecMarker, the host also defines a HookimplMarker with the same name:
hookimpl = pluggy.HookimplMarker("htmlize")
This is used by plugins to attach to hooks when they're loaded.
The host's main function loads plugins at startup as follows:
pm = pluggy.PluginManager("htmlize") pm.add_hookspecs(hookspecs) pm.load_setuptools_entrypoints("htmlize")
hookspecs is our Python module containing the hooks shown above. load_setuptools_entrypoints is Pluggy's helper for loading plugins that were pip-installed into the same environment and registered as setuptools entry points. It's a way to signal - in one's setup.py or pyproject.toml file - some metadata that projects can review at runtime. In our project, the plugins register themselves with this section in the pyproject.toml file:
[project.entry-points.htmlize] tt = "tt"
This says "for entry point htmlize, define a new entry named tt". Pluggy's load_setuptools_entrypoints then uses importlib.metadata to access this information.
Note that Pluggy doesn't require using this mechanism. Hosts can implement any plugin discovery method they want, and add plugins directly to their PluginManager with the register method. But this is the mechanism used for pytest and many other projects; it makes it very easy to automatically discover and register plugins that are installed with pip and equivalent tools.
Once PluginManager loads the plugins, invoking them is straightforward; here's how htmlize invokes the contents hooks [1]:
# Build full contents back again, and ask plugins to act on # contents. contents = ''.join(parts) for handler in plugin_manager.hook.htmlize_contents(post=post, db=db): contents = handler(contents) return contents
Generally, hook invocations return a list of all the hooks attached to by different plugins (a single host application can have multiple plugins installed and attaching to the same hook). When the host invokes the hook as shown above, the default order is LIFO, but plugins can affect this with hook options like tryfirst and trylast.
Here's our entire narcissist plugin that's attaching to the contents hook:
import htmlize @htmlize.hookimpl def htmlize_contents(post, db): repl = f'<b>I ({post.author})</b>' def hook(contents): return re.sub(r'\bI\b', repl, contents) return hook
Some notes:
Let's see how this case study of Pluggy measures against the Fundamental plugin concepts that were covered several times on this blog.
It's important to remember that Pluggy is not a specific host application with a bespoke plugin system; rather, it's a reusable library for creating such plugin systems. Therefore, this is more of a meta case study.
Generally, Pluggy leaves discovery logic to the user's discretion. Its PluginManager has a register method for adding plugins, and these can be discovered in any way the application chooses.
That said, Pluggy comes with one discovery mechanism built in - through the entry points process of Python packaging, as shown above. This is hugely convenient for a large number of applications, as long as both the application and its plugins are installed via standard Python packaging tools (which is a very reasonable assumption in the Python ecosystem).
In the entry point process, plugins register themselves by adding a [project.entry-points.<HOST-ID>] section in their pyproject.toml file.
Otherwise - as in the previous section - users are free to devise their own registration schemes.
This one is easy, since it's called hooks in Pluggy parlance as well! Pluggy's implementation of hooks is rather elegant, with function decorators available for plugins to set. We've seen an example of this above with @htmlize.hookimpl decorating htmlize_contents.
Since Pluggy is designed for Python hosts and Python plugins, this one is fairly straightforward. The plugins typically assume the host project is already installed in the Python environment and its modules can be imported.
In our example, hookimpl is imported from htmlize by the plugin to accomplish this. It also shows how host data is passed to the plugin - the post and db parameters. These are APIs exposed by the host for the plugins' use.
In footnote 2 of my original fundamental concepts of plugin infrastructures post, I wrote [2]:
This is probably why there are very few well-established plugin frameworks in existence (even in low-level languages like C or C++). It's too easy (and tempting) to roll your own.
I still believe my statement is true - plugin frameworks are very easy to create, and the functionality they provide is relatively small compared to their large surface area. In other words, this is a shallow API.
That said, Pluggy does provide some nice functionality for the more advanced uses of plugins:
Are these worthwhile for your project? It really depends on the project, and it's always worth keeping the tradeoff between dependencies and project effort in mind.
The full code repository for this post is available here.
| [1] | Here plugin_manager is the value previously returned from pluggy.PluginManager; in the previous code snippet it's saved into pm - the different variable name is because a function call is made and plugin_manager is the parameter name. |
| [2] | To be fair, that post predates the creation of Pluggy! |
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。