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

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

22 from __future__ import annotations

23

24 import importlib

25 import inspect

26 import logging

27 import pkgutil

28 from pathlib import Path

29 from typing import Any, Optional, Set, TypedDict

30

31 from paperap.plugins.base import Plugin

32

33 logger = logging.getLogger(__name__)

34

35

36 class PluginConfig(TypedDict):

37 """

38 Configuration settings for a plugin.

39 """

40

41 enabled_plugins: list[str]

42 settings: dict[str, Any]

43

44

45 class 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