from abc import abstractclassmethod, abstractmethod from pronoun_update import PronounUpdate from typing import Any, Callable, List from importlib.metadata import entry_points from functools import cache class Plugin: @classmethod @abstractmethod def name(cls) -> str: pass @abstractmethod def __init__(self, config: dict[str, str]): pass @abstractmethod async 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