Source code for pycsamt.pipeline._plugins
"""Entry-point discovery for third-party pipeline steps.
A plugin package registers pipeline steps by declaring a zero-argument
callable under the ``pycsamt.pipeline.steps`` entry-point group, e.g. in its
``pyproject.toml``::
[project.entry-points."pycsamt.pipeline.steps"]
my_plugin = "my_package.pipeline_steps:register"
where ``my_package.pipeline_steps.register()`` calls
:func:`pycsamt.pipeline.register_step` for whatever it contributes.
Discovery is **never** run automatically on ``import pycsamt.pipeline`` — no
third-party code should execute just because a package happens to be
installed. Call :func:`discover_plugins` explicitly, or use the
``pycsamt pipe`` CLI, which calls it for you before every command.
"""
from __future__ import annotations
import warnings
from dataclasses import dataclass
from importlib import metadata as _importlib_metadata
ENTRY_POINT_GROUP = "pycsamt.pipeline.steps"
[docs]
@dataclass
class PluginLoadResult:
"""Outcome of loading one ``pycsamt.pipeline.steps`` entry point.
Attributes
----------
name:
The entry point's registered name (left-hand side in
``[project.entry-points."pycsamt.pipeline.steps"]``).
ok:
Whether the entry point loaded and ran without raising.
error:
``str(exception)`` when ``ok`` is ``False``, else ``None``.
"""
name: str
ok: bool
error: str | None = None
def _iter_entry_points(group: str):
try:
return _importlib_metadata.entry_points(group=group)
except TypeError:
# Python 3.9: entry_points() takes no arguments, returns a
# dict[str, list[EntryPoint]] keyed by group name.
return _importlib_metadata.entry_points().get(group, [])
[docs]
def discover_plugins(
*, group: str = ENTRY_POINT_GROUP, on_error: str = "warn"
) -> list[PluginLoadResult]:
"""Load every ``pycsamt.pipeline.steps`` entry point and run it.
Each entry point must resolve to a zero-argument callable; it is
expected to call :func:`pycsamt.pipeline.register_step` itself for
whatever steps it contributes. Never called automatically — see the
module docstring.
Parameters
----------
group:
Entry-point group to scan. Defaults to ``ENTRY_POINT_GROUP``.
on_error:
``"warn"`` (default): a plugin that fails to load or raises is
reported as a failed :class:`PluginLoadResult` and a
:class:`UserWarning`; discovery continues with the remaining
plugins. ``"raise"``: the first failure propagates immediately.
Returns
-------
list[PluginLoadResult]
One entry per discovered entry point, in discovery order.
"""
if on_error not in ("warn", "raise"):
raise ValueError(f"on_error must be 'warn' or 'raise', got {on_error!r}")
results: list[PluginLoadResult] = []
for ep in _iter_entry_points(group):
try:
register_fn = ep.load()
register_fn()
except Exception as exc: # noqa: BLE001 - one bad plugin must not sink the rest
if on_error == "raise":
raise
warnings.warn(
f"pyCSAMT pipeline plugin {ep.name!r} failed to load: {exc}",
stacklevel=2,
)
results.append(PluginLoadResult(name=ep.name, ok=False, error=str(exc)))
else:
results.append(PluginLoadResult(name=ep.name, ok=True))
return results
__all__ = ["ENTRY_POINT_GROUP", "PluginLoadResult", "discover_plugins"]