Coverage for src/paperap/plugin_manager.py: 81%
75 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-11 21:37 -0400
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-11 21:37 -0400
1"""
2----------------------------------------------------------------------------
4 METADATA:
6 File: plugin_manager.py
7 Project: paperap
8 Created: 2025-03-04
9 Version: 0.0.5
10 Author: Jess Mann
11 Email: jess@jmann.me
12 Copyright (c) 2025 Jess Mann
14----------------------------------------------------------------------------
16 LAST MODIFIED:
18 2025-03-04 By Jess Mann
20"""
22from __future__ import annotations
24import importlib
25import inspect
26import logging
27import pkgutil
28from pathlib import Path
29from typing import Any, Optional, Set, TypedDict
31from paperap.plugins.base import Plugin
33logger = logging.getLogger(__name__)
36class PluginConfig(TypedDict):
37 """
38 Configuration settings for a plugin.
39 """
41 enabled_plugins: list[str]
42 settings: dict[str, Any]
45class PluginManager:
46 """Manages the discovery, configuration and initialization of plugins."""
48 plugins: dict[str, type[Plugin]]
49 instances: dict[str, Plugin]
50 config: PluginConfig
51 dependencies: dict[str, Set[str]]
53 def __init__(self) -> None:
54 self.plugins = {}
55 self.instances = {}
56 self.config = {
57 "enabled_plugins": [],
58 "settings": {},
59 }
60 self.dependencies = {}
61 super().__init__()
63 @property
64 def enabled_plugins(self) -> list[str]:
65 """
66 Get the list of enabled plugins.
68 Returns:
69 List of enabled plugin names
71 """
72 # TODO: There's a bug here... disabling every plugin will then enable every plugin
73 if enabled := self.config.get("enabled_plugins"):
74 return enabled
76 return list(self.plugins.keys())
78 def discover_plugins(self, package_name: str = "paperap.plugins") -> None:
79 """
80 Discover available plugins in the specified package.
82 Args:
83 package_name: Dotted path to the package containing plugins.
85 """
86 try:
87 package = importlib.import_module(package_name)
88 except ImportError:
89 logger.warning("Could not import plugin package: %s", package_name)
90 return
92 # Find all modules in the package
93 for _, module_name, is_pkg in pkgutil.iter_modules(package.__path__, package.__name__ + "."):
94 if is_pkg:
95 # Recursively discover plugins in subpackages
96 self.discover_plugins(module_name)
97 continue
99 try:
100 module = importlib.import_module(module_name)
102 # Find plugin classes in the module
103 for _name, obj in inspect.getmembers(module, inspect.isclass):
104 if issubclass(obj, Plugin) and obj is not Plugin and obj.__module__ == module_name:
105 plugin_name = obj.__name__
106 self.plugins[plugin_name] = obj
107 logger.debug("Discovered plugin: %s", plugin_name)
108 except Exception as e:
109 logger.error("Error loading plugin module %s: %s", module_name, e)
111 def configure(self, config: PluginConfig) -> None:
112 """
113 Configure the plugin manager with plugin-specific configurations.
115 Args:
116 config: dictionary mapping plugin names to their configurations.
118 """
119 self.config = config
121 def get_plugin_config(self, plugin_name: str) -> dict[str, Any]:
122 """Get the configuration for a specific plugin."""
123 return self.config["settings"].get(plugin_name, {})
125 def initialize_plugin(self, plugin_name: str, client: Any) -> Plugin | None:
126 """
127 Initialize a specific plugin.
129 Args:
130 plugin_name: Name of the plugin to initialize.
131 client: The PaperlessClient instance.
133 Returns:
134 The initialized plugin instance or None if initialization failed.
136 """
137 if plugin_name in self.instances:
138 return self.instances[plugin_name]
140 if plugin_name not in self.plugins:
141 logger.warning("Plugin not found: %s", plugin_name)
142 return None
144 plugin_class = self.plugins[plugin_name]
145 plugin_config = self.get_plugin_config(plugin_name)
147 try:
148 # Initialize the plugin with client and any plugin-specific config
149 plugin_instance = plugin_class(client, **plugin_config)
150 self.instances[plugin_name] = plugin_instance
151 logger.info("Initialized plugin: %s", plugin_name)
152 return plugin_instance
153 except Exception as e:
154 logger.error("Failed to initialize plugin %s: %s", plugin_name, e)
155 return None
157 def initialize_all_plugins(self, client: Any) -> dict[str, Plugin]:
158 """
159 Initialize all discovered plugins.
161 Args:
162 client: The PaperlessClient instance.
164 Returns:
165 Dictionary mapping plugin names to their initialized instances.
167 """
168 # Get enabled plugins from config
169 enabled_plugins = self.enabled_plugins
171 # Initialize plugins in dependency order (if we had dependencies)
172 initialized = {}
173 for plugin_name in enabled_plugins:
174 instance = self.initialize_plugin(plugin_name, client)
175 if instance:
176 initialized[plugin_name] = instance
178 return initialized