Source code for pyzeal.plugins.plugin_loader
"""
Module plugin_loader.py from the package PyZEAL.
This module handles discovering, loading and registering of plugins.
Authors:\n
- Philipp Schuette\n
"""
from __future__ import annotations
from abc import ABCMeta
from importlib import import_module
from os import listdir
from os.path import dirname, join, splitext
from re import compile as compileRegex
from types import ModuleType
from typing import Final, List, Optional, Type
from pyzeal.plugins.pyzeal_plugin import PyZEALPlugin, tPluggable
from pyzeal.pyzeal_logging.loggable import Loggable
from pyzeal.utils.service_locator import ServiceLocator
# default location where custom plugins are installed
PLUGIN_INSTALL_DIR: Final[str] = join(dirname(__file__), "custom_plugins")
[docs]
class PluginLoader(Loggable):
"""
This class handles plugin discovery, loading and registration.
"""
_instance: Optional[PluginLoader] = None
[docs]
@staticmethod
def getInstance() -> PluginLoader:
"""
Return the global `PluginLoader` instance. If no instance exists,
a new one is created and returned.
:return: `PluginLoader` singleton instance.
"""
return (
PluginLoader._instance
if PluginLoader._instance is not None
else PluginLoader()
)
[docs]
@staticmethod
def loadPlugins(
path: str = PLUGIN_INSTALL_DIR,
) -> List[PyZEALPlugin[tPluggable]]:
"""
Load plugins present in `path`.
:param path: Path to search for plugins, defaults to PLUGIN_INSTALL_DIR
:return: List of loaded plugins.
"""
instance = PluginLoader.getInstance()
plugins: List[PyZEALPlugin[tPluggable]] = []
for plugin in instance.locateAndLoadPlugins(path):
pluginInstance = plugin.getInstance()
ServiceLocator.registerAsTransient(
pluginInstance.pluginType, plugin.initialize()
)
plugins.append(pluginInstance)
return plugins
[docs]
def locateAndLoadPlugins(
self, path: str = PLUGIN_INSTALL_DIR
) -> List[Type[PyZEALPlugin[tPluggable]]]:
"""
Discover plugins at a given path and load them.
:param path: Path to search for plugins, defaults to PLUGIN_INSTALL_DIR
:return: List of found plugins.
"""
plugins: List[Type[PyZEALPlugin[tPluggable]]] = []
self.logger.info("starting plugin discovery in %s...", path)
candidates = PluginLoader.discoverModules(path)
self.logger.debug("contents of plugin directory: %s", str(candidates))
for candidate in candidates:
if not (candidate := PluginLoader.isPyFile(candidate)):
continue
self.logger.info("module %s might contain a plugin...", candidate)
module = import_module(
candidate, package="pyzeal_plugins.custom_plugins"
)
plugin = self.loadPlugin(module)
if plugin is not None:
plugins.append(plugin)
return plugins
[docs]
def loadPlugin(
self, candidateModule: ModuleType
) -> Optional[Type[PyZEALPlugin[tPluggable]]]:
"""
Try to load a candidate plugin and return it if successful.
:param candidateModule: Candidate plugin module
:return: Plugin if an implementation has been found, else `None`.
"""
attributeNames = PluginLoader.discoverAttributes(candidateModule)
self.logger.debug(
"module attributes %s found during plugin discovery!",
str(attributeNames),
)
for attributeName in attributeNames:
attribute = getattr(candidateModule, attributeName)
if attribute == PyZEALPlugin:
continue
if isinstance(attribute, ABCMeta):
if issubclass(attribute, PyZEALPlugin):
self.logger.info(
"plugin implementation [ %s ] found!",
str(attribute),
)
return attribute
return None
[docs]
@staticmethod
def isPyFile(filename: str) -> str:
"""
Returns `True` if `filename` corresponds to a python file.
:param filename: File to evaluate
:return: `True` if `filename` corresponds to a python file.
"""
if filename == "__init__.py":
return ""
name, extension = splitext(filename)
return "." + name if extension.lower() == ".py" else ""
[docs]
@staticmethod
def discoverModules(path: str = PLUGIN_INSTALL_DIR) -> List[str]:
"""
Discover all possible plugin files in `path`. Ignore files starting
with `__`.
:param path: Path to search, defaults to PLUGIN_INSTALL_DIR
:return: List of plugin candidates.
"""
regex = compileRegex(r"__.*")
candidates = [c for c in listdir(path) if not regex.match(c)]
return candidates
[docs]
@staticmethod
def discoverAttributes(candidateModule: ModuleType) -> List[str]:
"""
Discover attributes of a candidate module. Ignores attributes
starting with `__`.
:param candidateModule: Candidate module
:return: List of attributes.
"""
regex = compileRegex(r"__.*")
candidates = [c for c in dir(candidateModule) if not regex.match(c)]
return candidates