Source code for coder_plugin.base_plugin_manager
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Base plugin manager for the coder_plugin system.
Provides plugin discovery, dynamic loading, and context management
for organized hierarchical plugin systems.
"""
# coder_plugin/base_plugin_manager.py
from __future__ import annotations
import importlib.metadata
from typing import Any, Optional, Self, Type
from .base_plugin_unit import BasePluginUnit
[docs]
class BasePluginManager(BasePluginUnit):
"""
Manages discovery and loading of sub-plugins for a specific plugin group.
This base class provides dynamic plugin discovery using Python entry points,
hierarchical plugin management, and context management support.
"""
def __enter__(self) -> Self:
"""
Context manager entry.
Prepares the plugin manager for context-managed execution.
Returns:
Self: Enables context management with 'with' blocks.
"""
self.logger.debug(f"Prepare the {self.__class__.__name__} plugin manager for context-managed execution.")
return self # pylint: disable=useless-return
# pylint: disable=useless-return
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[Any],
) -> Optional[bool]:
"""
Context manager exit.
Cleans up resources when exiting the plugin manager context-managed execution.
Args:
exc_type (Optional[Type[BaseException]]): Exception type, if any.
exc_value (Optional[BaseException]): Exception instance, if any.
traceback (Optional[Any]): Traceback object, if any.
Returns:
Optional[bool]:
- True to suppress the exception.
- False or None to propagate the exception.
"""
self.logger.debug(f"Cleaning up the {self.__class__.__name__} plugin manager after context-managed execution.")
# Example logic: suppress all exceptions
# return True
# Example logic: suppress only specific exception types
# if exc_type and issubclass(exc_type, SomeExpectedException):
# self.logger.warning(f"Suppressed {exc_type.__name__}: {exc_value}")
# return True
# Default: do not suppress
return None
[docs]
def __init__(self, *args: Any, auto_load_children: bool = False, **kwargs: Any) -> None:
"""
Initialize the PluginManager.
Args:
auto_load_children (bool):
If True, automatically load children upon initialization.
"""
super().__init__(*args, **kwargs)
if auto_load_children:
self.load_children()
[docs]
def load_children(self) -> None:
"""
Discover and load child plugins dynamically via this plugin's plugin_group.
Defensive coding:
- If no plugin group is defined, loading is skipped.
- In the future, consider raising an exception if no plugin_group is set.
"""
plugin_group = type(self).plugin_group
if plugin_group is None:
self.logger.debug(f"{self.__class__.__name__}: Plugin group was None; skipping load.")
return
self.logger.debug(f"Loading {self.__class__.__name__} children plugins")
entry_points = importlib.metadata.entry_points()
children_eps = entry_points.select(group=plugin_group)
for ep in children_eps:
plugin_cls = ep.load()
plugin_instance = plugin_cls()
if plugin_instance != self:
self.logger.debug(
f" ╰─▶ Loading {ep.__class__.__name__} entry_point with {plugin_instance.__class__.__name__}."
)
plugin_instance.parent = self
self.children.append(plugin_instance)
# pylint: disable=useless-return
[docs]
def run(self, *args: Any, **kwargs: Any) -> Any:
"""
Run the plugin manager's main logic.
Args:
\\*args (Any): Positional arguments forwarded to each child plugin.
\\*\\*kwargs (Any): Keyword arguments forwarded to each child plugin.
Returns:
Any: The result of the plugin manager's execution, as defined by the subclass.
"""
self.logger.debug(f"Running managed task(s) for {self.__class__.__name__} plugins")
for plugin in self.children:
self.logger.debug(f" ╰─▶ Running the {plugin.__class__.__name__} plugin")
plugin.run(*args, **kwargs)
return None
[docs]
def set_up(self, *args: Any, **kwargs: Any) -> Self:
"""
Perform setup task(s) for all loaded child plugins.
Returns:
Self: Enables fluent chaining after set up.
"""
self.logger.debug(f"Performing managed setup task(s) for {self.__class__.__name__} plugins")
for plugin in self.children:
self.logger.debug(f" ╰─▶ Setting up {plugin.__class__.__name__} plugin")
plugin.set_up(*args, **kwargs)
return self
[docs]
@classmethod
def load_from_parent_group(cls: Type[Self], parent_group: str, parent_name: str, *args: Any, **kwargs: Any) -> Self:
"""
Discover and load a plugin from a parent group by its registered name.
Args:
parent_group (str): The entry point group where the parent plugin is registered.
parent_name (str): The registered name of the plugin to load.
\\*args (Any): Positional arguments to pass to the plugin constructor.
\\*\\*kwargs (Any): Keyword arguments to pass to the plugin constructor.
Returns:
Self: An instantiated plugin object.
Raises:
LookupError: If no plugin with the specified name is found.
Notes:
- If no additional arguments are needed, the plugin class will be instantiated with no arguments.
- If arguments are needed, they will be passed through \\*args and \\*\\*kwargs.
"""
entry_points = importlib.metadata.entry_points()
candidates = entry_points.select(group=parent_group)
for ep in candidates:
if ep.name == parent_name:
plugin_cls = ep.load()
return plugin_cls(*args, **kwargs)
raise LookupError(f"Plugin {parent_name} not found in group {parent_group}")