Source code for masterpiece.plugmaster
"""
Classes for loading and managing plugins.
See also the 'plugin' module.
Note: Referring to 'MasterPiece' modules as plugins may not fully do them justice,
as the term 'plugin' could imply that plugin classes are somehow less capable than
core, built-in classes. However, this is far from the case. Don't let the terminology
mislead you — plugin classes are not second-class citizens.
The only difference between plugin modules and the core set of MasterPiece modules is that
plugins are dynamic by nature. They can be added or removed without altering a single
line of code in the main application. Plugin modules have the same level of control and
capability as the core classes, offering 100% control over the application.
Author: Juha Meskanen
Date: 2024-10-26
"""
import sys
from typing import Type, Dict, Union, Optional
import importlib.metadata
from .masterpiece import MasterPiece
from .composite import Composite
from .plugin import Plugin
[docs]
class PlugMaster(MasterPiece):
"""
The `Plugmaster` class is responsible for managing and loading plugins into an application.
The `Plugmaster` is designed to work with plugins that are `Masterpiece` objects or subclasses
thereof. Plugins can optionally be derived from the `Plugin` class.
If a plugin implements the `Plugin` interface, it is responsible for determining what objects
should be added to the application.
If a plugin is not a `Plugin` class, it is simply loaded, and it is the responsibility
of the application configuration file or the application code to determine how to utilize
the plugin.
"""
[docs]
def __init__(self, name: str) -> None:
"""Instantiates and initializes the Plugmaster for the given application name. This
name refers to the list of plugins, as defined in the application 'pyproject.toml'.
For more information on 'pyproject.toml' consult the Python documentation.
Args:
name (str): Name determining the plugins to be loaded.
"""
super().__init__(name)
# self.plugins: List[Type[MasterPiece]] = []
self.plugins: Dict[str, Type[MasterPiece]] = {}
self.app: Optional[Composite] = None
[docs]
def load(self, name: str) -> None:
"""Fetch the entry points associated with the 'name', call their 'load()' methods
and insert to the list of plugins.
Note: Python's 'importlib.metadata' API has been redesigned
a couple of times in the past. The current implementation has been tested with
Python 3.8, 3.9 and 3.12.
Args:
name (str): Name determining the plugins to be loaded.
"""
if sys.version_info >= (3, 10):
entry_points = importlib.metadata.entry_points().select(
group=f"{name}.plugins"
)
elif sys.version_info >= (3, 9):
# TODO: pylint error if python version different
entry_points = importlib.metadata.entry_points().get(f"{name}.plugins", [])
else:
# For Python 3.8 and below
entry_points = importlib.metadata.entry_points()[f"{name}.plugins"]
for entry_point in entry_points:
try:
entry = entry_point.load()
self.plugins[entry.__name__] = entry
self.info(f"Plugin {entry.__name__} loaded")
except Exception as e:
self.error(f"Failed to load plugin {entry_point.name}: {e}")
[docs]
def find_class_by_name(self, name: str) -> Union[Type[MasterPiece], None]:
"""Find and return a plugin class by its name.
Returns:
plugin class (MasterPiece) or none if not found
"""
return self.plugins.get(name)
[docs]
def instantiate_class_by_name(
self, app: Composite, name: str
) -> Union[MasterPiece, None]:
"""Instantiate and add the plugin into the application.
Args:
app (Composite) : parent object, for hosting the instances createdby the plugin.
name (str) : name of the plugin to be instantiated
"""
entry = self.find_class_by_name(name)
if entry is not None:
# install to host application
if issubclass(entry, Plugin):
obj: Plugin = entry()
obj.install(app)
self.info(
f"Plugin {obj.name}:{str(type(obj))} plugged in to {app.name}"
)
return obj
else:
self.info(f"Class {entry.__name__} imported into {app.name}")
return entry()
return None
[docs]
def install(self, app: Composite) -> None:
"""Instantiate and add all the registered plugins to the application.
Typically is up to the application or the configuration to define the instances
to be added. This method is provided for testing purposes only.
Args:
app (Composite) : parent object (application), for hosting the instances
created by the plugin.
"""
for name, entry in self.plugins.items():
# install to host application
if issubclass(entry, Plugin):
plugin: Plugin = entry()
plugin.install(app)
self.info(
f"Plugin {name} {plugin.name}:{str(type(plugin))} added to {app.name}"
)
else:
self.info(f"Class {entry.__name__} imported to {app.name}")
[docs]
def get(self) -> Dict[str, Type[MasterPiece]]:
"""Fetch the list of plugins classes.
Returns:
List[Type[MasterPiece]]: List of plugins
"""
return self.plugins