src.extra.plugins_loader
Loads all plugins
1""" 2Loads all plugins 3""" 4 5import importlib 6import inspect 7import logging 8import pkgutil 9import sys 10from sys import stdout 11from typing import Any 12 13import src.constants as cst 14from src.cmd_types.commands import ExecutableCommand, UndoableCommand 15from src.cmd_types.meta import CommandMetadata 16from src.cmd_types.plugins import PluginMetadata 17 18RESTRICTED = (ExecutableCommand, UndoableCommand) 19 20class PluginLoader: 21 """ 22 Class to load plugins 23 24 :param pkg_dir: dir with plugins 25 :type pkg_dir: str 26 27 :param prefix: prefix of plugins files 28 :type prefix: str 29 30 :param strict: Whether raise an error on import or not 31 :type strict: bool 32 33 :raise ImportError: if plugin cannot be loaded. Rather the command was already loaded or plugins has some errors on import 34 35 """ 36 def __init__(self, pkg_dir: str = cst.PLUGINS_DIR, prefix: str = cst.PLUGINS_PREFIX, strict: bool = cst.STRICT_PLUGIN_LOADING): 37 self.pkg_dir = pkg_dir 38 self.prefix = prefix 39 self.logger = logging.Logger(__name__) 40 41 self.commands: dict[str, CommandMetadata] = {} 42 """dict of loaded commands""" 43 44 self.strict = strict 45 self.__init_logger() 46 self.non_default: dict[str, PluginMetadata] = {} 47 """dict of non-default plugins(will be loaded only after default ones)""" 48 49 def __init_logger(self): 50 handlers = [ 51 logging.FileHandler(cst.LOG_FILE, mode="a", encoding="utf-8"), 52 logging.StreamHandler(stdout), 53 ] 54 formatter = logging.Formatter(cst.FORMAT_LOADER) 55 for handler in handlers: 56 handler.setFormatter(formatter) 57 self.logger.addHandler(handler) 58 self.logger.setLevel(cst.LOGGING_LEVEL) 59 60 def load_plugins(self): 61 """ 62 Loads all plugins 63 :return: 64 """ 65 plugins_pkg = importlib.import_module(self.pkg_dir) 66 for importer, module_name, is_pkg in pkgutil.iter_modules(plugins_pkg.__path__): 67 if module_name.startswith(self.prefix) and not is_pkg: 68 self._load_module(module_name) 69 for k in self.non_default.keys(): 70 self._load_module(k, False) 71 72 def warn_or_error(self, *, warn_msg: str = "", exc: Any = ImportError): 73 """ 74 Will warn if strict set to False else will raise given exception 75 :param warn_msg: message for warning 76 :param exc: exception to raise 77 """ 78 if not self.strict: 79 self.logger.warning(warn_msg) 80 81 else: 82 raise exc 83 84 85 def _load_module(self, module_name: str, defaults: bool = True): 86 """ 87 Imports one module. 88 :param module_name: name of the module to import 89 :param defaults: if set to True, will only load if it is non-default. Otherwise, will load commands from self.non_default 90 :return: 91 """ 92 full_module_name = defaults * f'{self.pkg_dir}.' + f"{module_name}" 93 if defaults: 94 try: 95 if full_module_name in sys.modules: 96 module = sys.modules[full_module_name] 97 importlib.reload(module) 98 self.logger.debug(f"Module {module.__name__} already imported: reloading") 99 else: 100 module = importlib.import_module(full_module_name) 101 except Exception as e: 102 self.warn_or_error(warn_msg=f"Failed to load module {full_module_name}: {e}", exc=e) 103 return 104 author = getattr(module, "__author__", None) 105 version = getattr(module, "__version__", None) 106 if author != "default" : 107 self.non_default[full_module_name] = PluginMetadata(module = module, author = author, version = version) 108 return 109 else: 110 obj_import = self.non_default[full_module_name] 111 module = obj_import.module 112 author = obj_import.author 113 version = obj_import.version 114 self.logger.debug(f"Loading {'non-default' if not defaults else 'default'} module {full_module_name}...") 115 for name, obj in inspect.getmembers(module): 116 if inspect.isclass(obj): 117 if issubclass(obj, ExecutableCommand) and obj not in RESTRICTED and getattr(obj, "name", None): 118 cmd_name = obj.name 119 if self.commands.get(cmd_name): 120 121 warn_msg = f"Command {cmd_name} in module {full_module_name} already exists, skipping" 122 123 exc = ImportError(f"{cmd_name} imported twice", name = module_name, path = full_module_name) 124 125 self.warn_or_error(warn_msg=warn_msg, exc=exc) 126 127 elif " " in cmd_name: 128 warn_msg = f"Command {cmd_name} in module {full_module_name} has spaces in it" 129 exc = ImportError(warn_msg, name = module_name, path = full_module_name) 130 warn_msg+=", skipping" 131 132 self.warn_or_error(warn_msg=warn_msg, exc=exc) 133 134 else: 135 self.logger.debug(f"Loading command {cmd_name}...") 136 self.commands[cmd_name] = CommandMetadata( 137 name = cmd_name, 138 plugin_name = module_name, 139 plugin_author=author, 140 plugin_version=version, 141 cmd = obj 142 ) 143 self.logger.debug(f"Command {cmd_name} in module {full_module_name} loaded") 144 145 self.logger.debug(f"Loaded module {full_module_name}")
RESTRICTED =
(<class 'src.cmd_types.commands.ExecutableCommand'>, <class 'src.cmd_types.commands.UndoableCommand'>)
class
PluginLoader:
21class PluginLoader: 22 """ 23 Class to load plugins 24 25 :param pkg_dir: dir with plugins 26 :type pkg_dir: str 27 28 :param prefix: prefix of plugins files 29 :type prefix: str 30 31 :param strict: Whether raise an error on import or not 32 :type strict: bool 33 34 :raise ImportError: if plugin cannot be loaded. Rather the command was already loaded or plugins has some errors on import 35 36 """ 37 def __init__(self, pkg_dir: str = cst.PLUGINS_DIR, prefix: str = cst.PLUGINS_PREFIX, strict: bool = cst.STRICT_PLUGIN_LOADING): 38 self.pkg_dir = pkg_dir 39 self.prefix = prefix 40 self.logger = logging.Logger(__name__) 41 42 self.commands: dict[str, CommandMetadata] = {} 43 """dict of loaded commands""" 44 45 self.strict = strict 46 self.__init_logger() 47 self.non_default: dict[str, PluginMetadata] = {} 48 """dict of non-default plugins(will be loaded only after default ones)""" 49 50 def __init_logger(self): 51 handlers = [ 52 logging.FileHandler(cst.LOG_FILE, mode="a", encoding="utf-8"), 53 logging.StreamHandler(stdout), 54 ] 55 formatter = logging.Formatter(cst.FORMAT_LOADER) 56 for handler in handlers: 57 handler.setFormatter(formatter) 58 self.logger.addHandler(handler) 59 self.logger.setLevel(cst.LOGGING_LEVEL) 60 61 def load_plugins(self): 62 """ 63 Loads all plugins 64 :return: 65 """ 66 plugins_pkg = importlib.import_module(self.pkg_dir) 67 for importer, module_name, is_pkg in pkgutil.iter_modules(plugins_pkg.__path__): 68 if module_name.startswith(self.prefix) and not is_pkg: 69 self._load_module(module_name) 70 for k in self.non_default.keys(): 71 self._load_module(k, False) 72 73 def warn_or_error(self, *, warn_msg: str = "", exc: Any = ImportError): 74 """ 75 Will warn if strict set to False else will raise given exception 76 :param warn_msg: message for warning 77 :param exc: exception to raise 78 """ 79 if not self.strict: 80 self.logger.warning(warn_msg) 81 82 else: 83 raise exc 84 85 86 def _load_module(self, module_name: str, defaults: bool = True): 87 """ 88 Imports one module. 89 :param module_name: name of the module to import 90 :param defaults: if set to True, will only load if it is non-default. Otherwise, will load commands from self.non_default 91 :return: 92 """ 93 full_module_name = defaults * f'{self.pkg_dir}.' + f"{module_name}" 94 if defaults: 95 try: 96 if full_module_name in sys.modules: 97 module = sys.modules[full_module_name] 98 importlib.reload(module) 99 self.logger.debug(f"Module {module.__name__} already imported: reloading") 100 else: 101 module = importlib.import_module(full_module_name) 102 except Exception as e: 103 self.warn_or_error(warn_msg=f"Failed to load module {full_module_name}: {e}", exc=e) 104 return 105 author = getattr(module, "__author__", None) 106 version = getattr(module, "__version__", None) 107 if author != "default" : 108 self.non_default[full_module_name] = PluginMetadata(module = module, author = author, version = version) 109 return 110 else: 111 obj_import = self.non_default[full_module_name] 112 module = obj_import.module 113 author = obj_import.author 114 version = obj_import.version 115 self.logger.debug(f"Loading {'non-default' if not defaults else 'default'} module {full_module_name}...") 116 for name, obj in inspect.getmembers(module): 117 if inspect.isclass(obj): 118 if issubclass(obj, ExecutableCommand) and obj not in RESTRICTED and getattr(obj, "name", None): 119 cmd_name = obj.name 120 if self.commands.get(cmd_name): 121 122 warn_msg = f"Command {cmd_name} in module {full_module_name} already exists, skipping" 123 124 exc = ImportError(f"{cmd_name} imported twice", name = module_name, path = full_module_name) 125 126 self.warn_or_error(warn_msg=warn_msg, exc=exc) 127 128 elif " " in cmd_name: 129 warn_msg = f"Command {cmd_name} in module {full_module_name} has spaces in it" 130 exc = ImportError(warn_msg, name = module_name, path = full_module_name) 131 warn_msg+=", skipping" 132 133 self.warn_or_error(warn_msg=warn_msg, exc=exc) 134 135 else: 136 self.logger.debug(f"Loading command {cmd_name}...") 137 self.commands[cmd_name] = CommandMetadata( 138 name = cmd_name, 139 plugin_name = module_name, 140 plugin_author=author, 141 plugin_version=version, 142 cmd = obj 143 ) 144 self.logger.debug(f"Command {cmd_name} in module {full_module_name} loaded") 145 146 self.logger.debug(f"Loaded module {full_module_name}")
Class to load plugins
Parameters
pkg_dir: dir with plugins
prefix: prefix of plugins files
strict: Whether raise an error on import or not
:raise ImportError: if plugin cannot be loaded. Rather the command was already loaded or plugins has some errors on import
PluginLoader( pkg_dir: str = 'src.plugins', prefix: str = 'plugin', strict: bool = False)
37 def __init__(self, pkg_dir: str = cst.PLUGINS_DIR, prefix: str = cst.PLUGINS_PREFIX, strict: bool = cst.STRICT_PLUGIN_LOADING): 38 self.pkg_dir = pkg_dir 39 self.prefix = prefix 40 self.logger = logging.Logger(__name__) 41 42 self.commands: dict[str, CommandMetadata] = {} 43 """dict of loaded commands""" 44 45 self.strict = strict 46 self.__init_logger() 47 self.non_default: dict[str, PluginMetadata] = {} 48 """dict of non-default plugins(will be loaded only after default ones)"""
non_default: dict[str, src.cmd_types.plugins.PluginMetadata]
dict of non-default plugins(will be loaded only after default ones)
def
load_plugins(self):
61 def load_plugins(self): 62 """ 63 Loads all plugins 64 :return: 65 """ 66 plugins_pkg = importlib.import_module(self.pkg_dir) 67 for importer, module_name, is_pkg in pkgutil.iter_modules(plugins_pkg.__path__): 68 if module_name.startswith(self.prefix) and not is_pkg: 69 self._load_module(module_name) 70 for k in self.non_default.keys(): 71 self._load_module(k, False)
Loads all plugins
Returns
def
warn_or_error(self, *, warn_msg: str = '', exc: Any = <class 'ImportError'>):
73 def warn_or_error(self, *, warn_msg: str = "", exc: Any = ImportError): 74 """ 75 Will warn if strict set to False else will raise given exception 76 :param warn_msg: message for warning 77 :param exc: exception to raise 78 """ 79 if not self.strict: 80 self.logger.warning(warn_msg) 81 82 else: 83 raise exc
Will warn if strict set to False else will raise given exception
Parameters
- warn_msg: message for warning
- exc: exception to raise