Source code for pyzeta.framework.plugins.plugin_loader
"""
Module plugin_loader.py from the PyZeta project.
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 pyzeta.framework.ioc.container import Container
from pyzeta.framework.plugins.pyzeta_plugin import PyZetaPlugin, tPluggable
from pyzeta.framework.pyzeta_logging.loggable import Loggable
# 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(
container: Container,
path: str = PLUGIN_INSTALL_DIR,
) -> List[PyZetaPlugin[tPluggable]]:
"""
Load plugins present in `path`.
:param container: Container to load the discovered plugins into
:param path: Path to search for plugins, defaults to PLUGIN_INSTALL_DIR
:return: List of loaded plugins.
"""
instance = PluginLoader.getInstance()
plugins: List[PyZetaPlugin[tPluggable]] = []
for plugin in instance.locateAndLoadPlugins(path):
pluginInstance = plugin.getInstance()
container.registerAsTransient(
pluginInstance.pluginType, plugin.initialize()
)
plugins.append(pluginInstance)
return plugins
[docs]
def locateAndLoadPlugins(
self, path: str = PLUGIN_INSTALL_DIR
) -> List[Type[PyZetaPlugin[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[PyZetaPlugin[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="pyzeta.framework.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[PyZetaPlugin[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 == PyZetaPlugin:
continue
if isinstance(attribute, ABCMeta):
if issubclass(attribute, PyZetaPlugin):
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