Source code for masterpiece.core.plugmaster
"""
Classes for loading and managing plugins. Isn't the name 'PlugMaster' cool, even
if I say so my self? :)
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.
"""
import sys
from typing import Type, Dict, Union
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.
"""
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]] = {}
[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:
entry = entry_point.load()
print(f"Plugin {entry.__class__}:{entry.__name__} added")
self.plugins[entry.__name__] = entry
[docs]
def find_class_by_name(self, name) -> 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:
# instantiate the plugin
obj: MasterPiece = entry()
# install to host application
if isinstance(obj, Plugin):
plugin: Plugin = obj
plugin.install(app)
self.info(
f"Plugin {obj.name}:{str(type(plugin))} plugged in to {app.name}"
)
elif isinstance(obj, MasterPiece):
app.add(obj)
self.info(f"Object {obj.name}:{str(type(obj))} added into {app.name}")
else:
self.error(f"{type(obj)} is not a MasterPiece, skipped")
return obj
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():
# instantiate the plugin
obj = entry()
# install to host application
if isinstance(obj, Plugin):
plugin: Plugin = obj
plugin.install(app)
self.info(
f"Plugin {name} {obj.name}:{str(type(plugin))} added to {app.name}"
)
elif isinstance(obj, MasterPiece):
app.add(obj)
self.info(f"Object {obj.name}:{str(type(obj))} installed to {app.name}")
else:
self.error(f"{type(obj)} is not a MasterPiece, skipped")
[docs]
def get(self) -> Dict[str, Type[MasterPiece]]:
"""Fetch the directionary of plugins classes.
Returns:
List[Type[MasterPiece]]: List of plugins
"""
return self.plugins