pyfly.plugins is a lightweight plugin / extension system inspired by
org.fireflyframework.plugins. Plugins declare extension points and
extensions; the manager loads them in dependency order.
from pyfly.plugins import plugin, extension, extension_point, PluginManager
@extension_point(id="formatters")
class _FormatterPoint: ...
@plugin(id="json-formatter", version="1.0.0")
class JsonFormatterPlugin:
@extension(point="formatters", priority=10)
class JsonFormatter:
name = "json"
async def start(self) -> None: ...
async def stop(self) -> None: ...manager = PluginManager()
await manager.add(JsonFormatterPlugin)
await manager.start_all()
formatters = await manager.registry.get("formatters")
await manager.stop_all()PluginManager rejects circular dependencies (PluginResolutionError)
and sorts extensions by priority within each extension point (highest
priority first).
manager.add() scans the plugin class for nested @extension_point classes
before scanning its @extension contributions, so the registry knows each
point's declared interface type.
ExtensionRegistry tracks extension points alongside their contributions:
| Method | Description |
|---|---|
await register_extension_point(point_id, point_type) |
Record a point id and the class that defines its interface. |
await has_extension_point(point_id) |
Whether a point id has been registered. |
await extension_point_ids() |
All registered extension-point ids. |
await register(point_id, instance, priority=0) |
Register an extension instance. |
await get(point_id) |
The registered extensions for a point, highest priority first. |
When an extension is contributed to a point whose interface type is known,
register() type-checks the instance against that declared
@extension_point class and raises ValueError if it is not an instance of
it (mirroring Java's DefaultExtensionRegistry). Extensions contributed to an
id with no registered point type are still accepted (lenient,
backward-compatible).
Unloading a plugin unregisters the extensions it contributed, so they no longer leak in the registry:
| Method | Description |
|---|---|
await manager.remove(plugin_id) |
Run the plugin's unload hook, unregister its extensions, and forget it. Returns False if the plugin is unknown. |
await manager.unload_all() |
Remove every plugin in reverse dependency order. |
start_all() runs each plugin's init then start hooks in dependency order;
stop_all() runs stop then unload in reverse order.
Every plugin tracked by PluginManager has a runtime PluginDescriptor that
records its current lifecycle state:
from pyfly.plugins import PluginDescriptor, PluginState
desc: PluginDescriptor = await manager.get_plugin("my-plugin")
print(desc.state) # PluginState.STARTED
print(desc.loaded_at) # datetime of add()
print(desc.last_state_change) # datetime of last transition
print(desc.failed_reason) # str if FAILED, else NonePluginState values:
| Value | Meaning |
|---|---|
LOADED |
Plugin registered via add() but not yet started |
STARTED |
start() hook completed successfully |
STOPPED |
stop() hook completed successfully |
FAILED |
A lifecycle hook raised an exception |
Two new optional fields are available on @plugin:
@plugin(id="auth", version="1.0.0", name="Auth Plugin", author="security-team")
class AuthPlugin:
...namedefaults toidwhen omitted (backward-compatible).authordefaults to"".
Both fields are accessible on Plugin and PluginDescriptor.plugin.
Use these instead of start_all / stop_all when you want fine-grained control:
Starts the named plugin and all its transitive dependencies, in dependency
order. Already-STARTED plugins are skipped. On hook failure the plugin's state
is set to FAILED and a PluginStartError is raised.
# Assuming C depends on B which depends on A:
await manager.start_plugin("c")
# → starts A, then B, then CStops the named plugin and all plugins that (transitively) depend on it, in
reverse dependency order (dependents first). Already-STOPPED/LOADED plugins are
skipped. On hook failure the state is set to FAILED and a PluginStopError
is raised.
await manager.stop_plugin("a")
# → stops C, then B, then AReturns the descriptor for the plugin, or None if no plugin with that id is
registered.
All plugin exceptions extend PyFlyException:
PyFlyException
└── PluginException # base for all plugin errors
├── PluginLoadError # plugin could not be loaded/registered
├── PluginStartError # start/init hook raised
├── PluginStopError # stop/unload hook raised
├── PluginStateError # invalid state transition or unknown plugin id
└── PluginResolutionError # missing dependency or cycle during topo-sort
Import from pyfly.kernel.exceptions (or pyfly.kernel):
from pyfly.kernel.exceptions import PluginStartError, PluginStateErrorget() returns a list of all extensions for a point. Use get_extension() when
you only expect (and want) the single highest-priority one:
processor = await registry.get_extension("processors")
# raises ValueError if the point is unknown or has no extensionsThe plugin system is auto-configured when pyfly.plugins.enabled=true:
# application.yaml
pyfly:
plugins:
enabled: truePluginsAutoConfiguration registers two beans:
| Bean type | Factory method |
|---|---|
ExtensionRegistry |
extension_registry() |
PluginManager |
plugin_manager(registry) |