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