from abc import abstractclassmethod, abstractmethod from functools import cache from importlib.metadata import entry_points import logging from typing import Any, Callable, List from pronoun_update import PronounUpdate class Plugin: def __init__(self, config: dict[str, str]): logging.info(f"Loading plugin {self.name()}") self._load_from_config(config) @abstractmethod def _load_from_config(self, config: dict[str, Any]) -> None: pass @classmethod @abstractmethod def name(cls) -> str: pass @abstractmethod def update_bio(self, update_pronouns: PronounUpdate) -> None: pass @cache def load_plugins() -> dict[str, type[Plugin]]: plugins: dict[str, type[Plugin]] = {} # Ignore the following two lines due to incorrect type definition for entry_points # See: https://github.com/python/typeshed/pull/7331 for plugin in entry_points(group="pronounomatic.plugins"): # type: ignore pl = plugin.load() # type: ignore assert issubclass(pl, Plugin) plugins[pl.name()] = pl return plugins class InvalidPluginDefinitionException(Exception): pass def load_plugins_for_config(config: list[dict[str, Any]]) -> List[Plugin]: plugins = [] plugin_classes = load_plugins() for plugin_def in config: if not plugin_def["name"]: raise InvalidPluginDefinitionException("Missing plugin_name field") plugin_name = plugin_def["name"] if not plugin_name in load_plugins(): raise InvalidPluginDefinitionException( f"No plugin with name {plugin_name} loaded" ) plugin_cls = plugin_classes[plugin_name] plugin = plugin_cls(plugin_def) plugins.append(plugin) return plugins