Skip to content

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:

[project.entry-points."binoc.plugins"]
<plugin-key> = <target>

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:

[project.entry-points."binoc.plugins"]
biobinoc = "biobinoc:register"
# biobinoc/__init__.py
def register(registry):
    from biobinoc.fasta import FastaComparator
    from biobinoc.normalizer import SequenceNormalizer
    registry.register_comparator("biobinoc.fasta", FastaComparator())
    registry.register_transformer("biobinoc.sequence_normalizer", SequenceNormalizer())

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:

[project.entry-points."binoc.plugins"]
biobinoc = "biobinoc"

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 a NativeComparator / NativeTransformer / NativeRenderer wrapper for each exported plugin. Per-file dispatch then goes through the C ABI with no Python involvement.

The native side is generated by the export_plugin! macro in binoc-sdk:

binoc_sdk::export_plugin! {
    module: biobinoc,
    comparators: [FastaComparator],
    transformers: [SequenceNormalizer],
}

See Plugin SDK and ABI ADR for the C ABI contract.

Registry API

Both shapes ultimately call methods on the PluginRegistry:

Method Purpose
register_comparator(name, comparator) Register a comparator by name.
register_transformer(name, transformer) Register a transformer by name.
register_renderer(name, renderer) Register a renderer by name.

Names are the opaque strings referenced from dataset config (comparators: [biobinoc.fasta]). They should be namespaced by package (see Naming conventions below).

For ad-hoc / scripting / notebook use you can skip entry-point registration entirely and attach plugin instances to a Config directly. Ad-hoc comparators are inserted before the built-in binary fallback:

config = binoc.Config.default()
config.add_comparator(FastaComparator())
config.add_transformer(SequenceNormalizer())
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 binoc as 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, not binoc. Depend on the binoc-sdk minor line you built against in Cargo.toml; in pyproject.toml, depend on binoc with a floor for the loader features you need. Native plugin compatibility is checked at runtime via the plugin's sdk_version.

See Release surface and automated publishing ADR for which packages are published and why.

Where to go next