Coverage for src/paperap/plugin_manager.py: 81%

75 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-12 23:40 -0400

1""" 

2---------------------------------------------------------------------------- 

3 

4 METADATA: 

5 

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 

13 

14---------------------------------------------------------------------------- 

15 

16 LAST MODIFIED: 

17 

18 2025-03-04 By Jess Mann 

19 

20""" 

21 

22from __future__ import annotations 

23 

24import importlib 

25import inspect 

26import logging 

27import pkgutil 

28from pathlib import Path 

29from typing import Any, Optional, Set, TypedDict 

30 

31from paperap.plugins.base import Plugin 

32 

33logger = logging.getLogger(__name__) 

34 

35 

36class PluginConfig(TypedDict): 

37 """ 

38 Configuration settings for a plugin. 

39 """ 

40 

41 enabled_plugins: list[str] 

42 settings: dict[str, Any] 

43 

44 

45class PluginManager: 

46 """Manages the discovery, configuration and initialization of plugins.""" 

47 

48 plugins: dict[str, type[Plugin]] 

49 instances: dict[str, Plugin] 

50 config: PluginConfig 

51 dependencies: dict[str, Set[str]] 

52 

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__() 

62 

63 @property 

64 def enabled_plugins(self) -> list[str]: 

65 """ 

66 Get the list of enabled plugins. 

67 

68 Returns: 

69 List of enabled plugin names 

70 

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 

75 

76 return list(self.plugins.keys()) 

77 

78 def discover_plugins(self, package_name: str = "paperap.plugins") -> None: 

79 """ 

80 Discover available plugins in the specified package. 

81 

82 Args: 

83 package_name: Dotted path to the package containing plugins. 

84 

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 

91 

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 

98 

99 try: 

100 module = importlib.import_module(module_name) 

101 

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) 

110 

111 def configure(self, config: PluginConfig) -> None: 

112 """ 

113 Configure the plugin manager with plugin-specific configurations. 

114 

115 Args: 

116 config: dictionary mapping plugin names to their configurations. 

117 

118 """ 

119 self.config = config 

120 

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, {}) 

124 

125 def initialize_plugin(self, plugin_name: str, client: Any) -> Plugin | None: 

126 """ 

127 Initialize a specific plugin. 

128 

129 Args: 

130 plugin_name: Name of the plugin to initialize. 

131 client: The PaperlessClient instance. 

132 

133 Returns: 

134 The initialized plugin instance or None if initialization failed. 

135 

136 """ 

137 if plugin_name in self.instances: 

138 return self.instances[plugin_name] 

139 

140 if plugin_name not in self.plugins: 

141 logger.warning("Plugin not found: %s", plugin_name) 

142 return None 

143 

144 plugin_class = self.plugins[plugin_name] 

145 plugin_config = self.get_plugin_config(plugin_name) 

146 

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 

156 

157 def initialize_all_plugins(self, client: Any) -> dict[str, Plugin]: 

158 """ 

159 Initialize all discovered plugins. 

160 

161 Args: 

162 client: The PaperlessClient instance. 

163 

164 Returns: 

165 Dictionary mapping plugin names to their initialized instances. 

166 

167 """ 

168 # Get enabled plugins from config 

169 enabled_plugins = self.enabled_plugins 

170 

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 

177 

178 return initialized