Plugin discovery¶
Binoc uses Python entry points for plugin discovery. This is the same mechanism for Python-only plugins and for native Rust plugins packaged as Python extension modules — the entry point's value distinguishes which loader to use.
For the rationale (why Python owns discovery and Rust owns execution, and why not pluggy / dynamic libraries / WASM), see Plugin discovery ADR. For the conceptual overview, see Plugin model.
The entry point group¶
Every binoc plugin is registered under the
binoc.plugins entry point group:
At startup, the binoc CLI (or any binoc host, such as
binoc.diff() from Python) calls
importlib.metadata.entry_points(group="binoc.plugins") and iterates
the result.
The <plugin-key> is a free string used only for display and
deduplication; by convention it matches the package name (for
example biobinoc, binoc-sqlite). The <target> distinguishes the
plugin type.
Two entry point shapes¶
Python plugins — module:function¶
Pure-Python plugins register a callable that is invoked with the
PluginRegistry:
# biobinoc/__init__.py
def register(registry):
from biobinoc.renderer import FastaSummaryRenderer
registry.register_renderer("biobinoc.fasta_summary", FastaSummaryRenderer())
The register(registry) function is called once at startup. Registered
plugin instances are held alive for the duration of the process.
Rust (native) plugins — bare module name¶
Rust plugins packaged as a maturin-built Python extension module
register the module name only, no :function:
Discovery detects the missing :function suffix, loads the native
shared library (the .so, .dylib, or .pyd installed alongside
the module) via libloading, reads the plugin descriptor, and registers
each exported native renderer. Rule-pack native loading is deferred until
the stable ABI tier lands.
The native side is generated by the export_plugin! macro in
binoc-sdk:
See Plugin SDK and ABI ADR for the C ABI contract.
Registry API¶
Both shapes ultimately call methods on the PluginRegistry:
| Method | Purpose |
|---|---|
register_renderer(name, renderer) |
Register a renderer by name. |
Names should be namespaced by package (see Naming conventions below).
config = binoc.Config.default()
registry = binoc.PluginRegistry.default()
registry.register_renderer("biobinoc.fasta_summary", FastaSummaryRenderer())
changeset = binoc.diff("snapshot-a", "snapshot-b", config=config)
Naming conventions¶
To prevent collisions across plugin packs:
| Thing | Convention | Examples |
|---|---|---|
| PyPI package name | binoc-* is the shared ecosystem namespace |
binoc-sqlite, binoc-html |
| Plugin names | package.name |
biobinoc.fasta, binoc-sqlite.sqlite |
| Tags | package.tag-name |
biobinoc.sequence-changed, binoc.column-reorder |
| Item types | package.type-name |
biobinoc.fasta-alignment, binoc.tabular |
| Actions | Standard actions unnamespaced; custom namespaced | add, remove, modify (standard); biobinoc.gap-shift (custom) |
The binoc.* namespace is reserved for the standard library.
Versioning¶
- Python plugins. Depend on
binocas a host package with a lower bound on the Python APIs you use. Do not add an upper bound unless you know of a specific incompatibility. - Rust (native) plugins. The Rust compatibility boundary is
binoc-sdk, notbinoc. Depend on thebinoc-sdkminor line you built against inCargo.toml; inpyproject.toml, depend onbinocwith a floor for the loader features you need. Native plugin compatibility is checked at runtime via the plugin'ssdk_version.
See Release surface and automated publishing ADR for which packages are published and why.
Where to go next¶
- Install and use plugins — from the user's side.
- Publish a plugin — packaging, entry points, and PyPI.
- Plugin model and Dispatch model — current rule-family extension model.