PluginLoader
The PluginLoader dynamically loads and manages plugins from the mloda_plugins package, and discovers plugins shipped by separately installed packages via entry points.
Quick Start
from mloda.user import PluginLoader
# Load all plugins
loader = PluginLoader.all()
# Or load specific groups
loader = PluginLoader()
loader.load_group("feature_group")
Main Methods
PluginLoader.all()
Creates a loader, loads all bundled plugins, then folds in installed entry points.
load_group(group_name: str)
Loads all plugins from a specific group folder, including nested subdirectories. Supports slash-separated paths for subdirectories (e.g. "feature_group/input_data/read_files").
load_matching(group_name: str, pattern: str)
Loads only files within a group whose filename matches a glob pattern (e.g. "*transformer*"). Useful when only a subset of files in a group is needed.
load_all_plugins()
Loads all plugin groups.
load_entry_points(group: str | None = None)
Discovers plugins from installed packages that declare entry points. Pass a group name to restrict discovery to one group; None loads all three. Returns the sorted, deduplicated list of registry keys it registered.
list_loaded_modules(plugin_category: str | None)
Lists loaded plugin modules, optionally filtered by category.
display_plugin_graph(plugin_category: str | None)
Shows plugin dependencies as a graph.
Example
# Create loader and load plugins
loader = PluginLoader()
loader.load_all_plugins()
# List loaded modules
modules = loader.list_loaded_modules()
# Show dependencies
graph = loader.display_plugin_graph()
Plugins are automatically discovered from .py files in these directories.
Entry Points
Separately installed packages publish plugins through three entry-point groups, one per plugin base type:
mloda.feature_groupsfor FeatureGroup classesmloda.compute_frameworksfor ComputeFramework classesmloda.extendersfor Extender classes
Manifest Convention
A package declares one entry per group it ships plugins for. The entry points at a module attribute, the manifest: a plain sequence of plugin classes. The entry-point name (my-pkg below) is a package label only, never a registry key; registered classes keep the usual module:qualname keys.
# pyproject.toml
[project.entry-points."mloda.feature_groups"]
my-pkg = "my_pkg.manifest:FEATURE_GROUPS"
[project.entry-points."mloda.compute_frameworks"]
my-pkg = "my_pkg.manifest:COMPUTE_FRAMEWORKS"
The manifest module lists the concrete classes explicitly:
# my_pkg/manifest.py
from mloda.provider import FeatureGroup
class CustomerChurnFeatureGroup(FeatureGroup):
"""Shipped by my-pkg; registers under its module:qualname key."""
FEATURE_GROUPS = [CustomerChurnFeatureGroup]
Declaring Optional Dependencies
A package can name its own optional import roots per entry point via a companion mloda.optional_dependencies entry, whose name matches the entry it protects:
[project.entry-points."mloda.feature_groups"]
my-pkg = "my_pkg.manifest:FEATURE_GROUPS"
[project.entry-points."mloda.optional_dependencies"]
my-pkg = "my_pkg.optional_deps:OPTIONAL_DEPENDENCIES"
# my_pkg/optional_deps.py
# Must always be safely importable on its own.
OPTIONAL_DEPENDENCIES = ("some_optional_lib",)
The declaration is scoped to the distribution that published it (matched by the installed package, not the entry-point name), so two unrelated packages reusing the same entry-point label never collide.
Loading
Discovery is lazy: installing a package does nothing by itself. Manifests are imported and registered only when load_entry_points() runs, either directly or as the final step of PluginLoader.all(). Discovered classes register into the default registry with provenance source="entry_point" (see Plugin Registry).
from mloda.user import PluginLoader
loader = PluginLoader()
# All three groups; returns the sorted, deduplicated registry keys that were registered.
keys = loader.load_entry_points()
assert keys == sorted(set(keys))
# One group only; unknown group names raise ValueError.
fg_keys = loader.load_entry_points(group="mloda.feature_groups")
Behavior Notes
- Validation is loud, by design. A malformed entry point fails discovery with an error instead of being skipped: a manifest that is not a list or tuple of plugin classes, or that contains classes of the wrong base type for its group, raises
TypeErrornaming the entry point. - Abstract classes are skipped silently; manifests may list a shared abstract base alongside its concrete subclasses.
- Collisions raise. If a different class already holds a manifest class's
module:qualnamekey, loading raisesPluginRegistryCollisionError. Loading the same manifests twice is idempotent. - Missing optional dependencies skip.
ImportError(coversModuleNotFoundErrortoo) around an entry point's.load()is caught. It is attributed to a declared (or globalOPTIONAL_PLUGIN_DEPENDENCIES) optional root either by the error's own module name, or by the innermost (deepest) traceback frame, i.e. where the failure actually happened, belonging to that root's code. A match skips only that entry point and logs at WARNING naming the entry point and the missing module; otherwise the error re-raises. - Skips are visible. A skipped in-tree
mloda_pluginsmodule or entry point logs one WARNING naming it and the missing dependency, and warns again only when that dependency changes, after a successful load in between, or afterreset_cache().PluginLoader.skipped_plugins()returns every skipped module and entry point with its missing dependency;reset_cache()clears it. - Reusable directly. The traceback-frame attribution above is importable directly:
traceback_blames_root(exc, root)frommloda.providerormloda.steward, for downstream packages building their own optional-dependency guards. Callers must exclude their own module's root from the roots they pass in, the same wayload_entry_pointsexcludes the entry point's own module, so a namespace collision can't misattribute the caller's own bug to that root. - Own-package failures always raise. If the missing root is the entry point's own module root, it re-raises even if declared optional. The entry point's own namespace (and anything under it) is also never eligible for the traceback-based attribution, so a genuine failure in the plugin's own code still always raises even if it happens to share a name with a declared optional root. When the missing module is the entry point's own module or a parent package (files gone, metadata left), the error names the entry point, distribution and module, and suggests
pip install --force-reinstall <distribution>. - Policies apply, after import. A registration denied by an installed plugin policy raises inside
register(); the loader catches the denial, skips the class, leaves its key out of the returned list, and logs one warning per denied key per registry instance. The policy gates registration only, not import: the manifest module is imported before the policy applies, so installing a package implies trusting its import side effects.