Coverage for src/symphra_modules/manager.py: 70.28%
160 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-26 18:16 +0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-26 18:16 +0800
1"""模块管理器实现."""
3from typing import Any
5from symphra_modules.abc import ModuleInterface
6from symphra_modules.config import ModuleState
7from symphra_modules.exceptions import ModuleNotFoundException
8from symphra_modules.loader import DirectoryLoader, PackageLoader
9from symphra_modules.registry import ModuleRegistry
10from symphra_modules.utils import get_logger
12logger = get_logger()
15class ModuleManager:
16 """模块管理器 - 统一的模块管理门面."""
18 def __init__(
19 self,
20 module_dirs: list[str] | None = None,
21 exclude_modules: set[str] | None = None,
22 ) -> None:
23 """初始化模块管理器.
25 Args:
26 module_dirs: 模块目录列表,默认为 ["modules"]
27 exclude_modules: 排除的模块名称集合(不区分大小写)
28 """
29 self.registry = ModuleRegistry()
30 self.module_dirs = module_dirs if module_dirs is not None else ["modules"]
31 # 排除列表:用于忽略并非真正模块的目录/包(例如 common 为通用基类集合)
32 _exmods = exclude_modules or {"common"}
33 self.exclude_modules = {m.lower() for m in _exmods}
34 self._directory_loader = DirectoryLoader()
35 self._package_loader = PackageLoader()
36 self._modules_cache: dict[str, dict[str, type[ModuleInterface]]] = {}
37 self._discover_cache: dict[str, list[str]] = {}
39 def _invalidate_directory_cache(self, directory: str | None = None) -> None:
40 """失效目录缓存,支持单目录或全部清理.
42 Args:
43 directory: 要清理的目录,None 表示清理所有缓存
44 """
45 if directory is not None:
46 self._modules_cache.pop(directory, None)
47 self._discover_cache.pop(directory, None)
48 else:
49 self._modules_cache.clear()
50 self._discover_cache.clear()
52 def _get_modules_from_directory(self, directory: str) -> dict[str, type[ModuleInterface]]:
53 """获取目录中的模块,带缓存.
55 Args:
56 directory: 目录路径
58 Returns:
59 模块类字典
60 """
61 if directory not in self._modules_cache: 61 ↛ 64line 61 didn't jump to line 64 because the condition on line 61 was always true
62 modules = self._directory_loader.load(directory)
63 self._modules_cache[directory] = modules
64 return self._modules_cache[directory]
66 def _discover_from_directory(self, directory: str) -> list[str]:
67 """发现目录模块名称,带缓存.
69 Args:
70 directory: 目录路径
72 Returns:
73 模块名称列表
74 """
75 if directory not in self._discover_cache: 75 ↛ 78line 75 didn't jump to line 78 because the condition on line 75 was always true
76 names = self._directory_loader.discover(directory)
77 self._discover_cache[directory] = names
78 return self._discover_cache[directory]
80 @staticmethod
81 def _match_module_by_name(
82 modules: dict[str, type[ModuleInterface]],
83 target_name: str,
84 ) -> type[ModuleInterface] | None:
85 """在模块字典中按名称匹配模块类(忽略大小写与 Module 后缀).
87 Args:
88 modules: 模块类字典
89 target_name: 目标模块名
91 Returns:
92 匹配的模块类,未找到则返回 None
93 """
94 name_lower = target_name.lower()
95 candidates = {name_lower, f"{name_lower}module"}
96 for module_name, module_class in modules.items():
97 if module_name.lower() in candidates:
98 return module_class
99 return None
101 def load_module(self, name: str, source: str | None = None, source_type: str = "directory") -> ModuleInterface:
102 """加载模块并注册到注册表,返回模块实例.
104 Args:
105 name: 模块名称
106 source: 模块源(可选)
107 source_type: 源类型("directory" 或 "package")
109 Returns:
110 模块实例
112 Raises:
113 ModuleNotFoundException: 模块未找到时抛出
114 """
115 # 已加载则直接返回
116 if self.registry.is_loaded(name):
117 existing = self.registry.get(name)
118 assert existing is not None
119 return existing
121 found_module: type[ModuleInterface] | None = None
123 if source: 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true
124 if source_type == "directory":
125 modules = self._get_modules_from_directory(source)
126 found_module = self._match_module_by_name(modules, name)
127 elif source_type == "package":
128 modules = self._package_loader.load(source)
129 found_module = modules.get(name) or modules.get(f"{name}Module")
130 else:
131 # 从配置的目录查找
132 for module_dir in self.module_dirs:
133 try:
134 modules = self._get_modules_from_directory(module_dir)
135 found_module = self._match_module_by_name(modules, name)
136 if found_module:
137 break
138 except Exception as e:
139 logger.warning(f"无法从目录 {module_dir} 加载模块: {e}")
141 if not found_module: 141 ↛ 145line 141 didn't jump to line 145 because the condition on line 141 was always true
142 raise ModuleNotFoundException(f"找不到模块: {name}", module_name=name)
144 # 注册模块(将类传入注册表,由注册表负责实例化与生命周期)
145 module_instance = self.registry.register(name, found_module) # type: ignore[arg-type]
146 return module_instance
148 def load_all_modules(self) -> dict[str, ModuleInterface]:
149 """加载所有可用模块.
151 Returns:
152 模块名到模块实例的映射
153 """
154 modules: dict[str, ModuleInterface] = {}
155 available_modules = self.discover_modules()
157 for module_name in available_modules:
158 try:
159 module = self.load_module(module_name)
160 modules[module_name] = module
161 except Exception as e:
162 # 对于被排除的"非模块目录",仅记录告警以降低噪音
163 if module_name.lower() in self.exclude_modules:
164 logger.warning(f"忽略非模块目录: {module_name}")
165 else:
166 logger.error(f"加载模块失败: {module_name} - {e}")
168 return modules
170 def discover_modules(self, source: str | None = None, source_type: str = "directory") -> list[str]:
171 """发现可用模块名称列表.
173 Args:
174 source: 模块源(可选)
175 source_type: 源类型
177 Returns:
178 模块名称列表
179 """
180 discovered: list[str] = []
181 if source:
182 try:
183 if source_type == "directory": 183 ↛ 185line 183 didn't jump to line 185 because the condition on line 183 was always true
184 names = self._discover_from_directory(source)
185 elif source_type == "package":
186 names = self._package_loader.discover(source)
187 else:
188 names = []
189 discovered.extend(names)
190 except Exception as e:
191 logger.warning(f"发现模块失败: {e}")
192 # 过滤排除列表
193 filtered = [n for n in set(discovered) if n.lower() not in self.exclude_modules]
194 return sorted(filtered)
196 # 遍历默认目录
197 for module_dir in self.module_dirs:
198 try:
199 names = self._discover_from_directory(module_dir)
200 discovered.extend(names)
201 except Exception as e:
202 logger.warning(f"无法在目录 {module_dir} 中发现模块: {e}")
203 # 过滤排除列表
204 filtered = [n for n in set(discovered) if n.lower() not in self.exclude_modules]
205 return sorted(filtered)
207 def get_module(self, name: str) -> ModuleInterface:
208 """获取已加载的模块.
210 Args:
211 name: 模块名称
213 Returns:
214 模块实例
216 Raises:
217 ModuleNotFoundException: 模块未找到时抛出
218 """
219 module = self.registry.get(name)
220 if module is None:
221 raise ModuleNotFoundException(f"模块未找到: {name}")
222 return module
224 def unload_module(self, name: str) -> None:
225 """卸载模块.
227 Args:
228 name: 模块名称
229 """
230 try:
231 self.registry.unregister(name)
232 logger.info(f"模块已卸载: {name}")
233 except ModuleNotFoundException:
234 # 模块未加载,忽略错误
235 logger.debug(f"尝试卸载未加载的模块: {name}")
237 def list_modules(self) -> list[str]:
238 """列出所有已加载的模块.
240 Returns:
241 模块名称列表
242 """
243 return self.registry.list_modules()
245 def is_module_loaded(self, name: str) -> bool:
246 """检查模块是否已加载.
248 Args:
249 name: 模块名称
251 Returns:
252 是否已加载
253 """
254 return self.registry.is_loaded(name)
256 def list_installed_modules(self) -> list[str]:
257 """列出已安装(包含 installed/started/stopped)的模块名称.
259 Returns:
260 已安装模块名称列表
261 """
262 names: set[str] = set()
263 for st in (ModuleState.INSTALLED, ModuleState.STARTED, ModuleState.STOPPED):
264 try:
265 names.update(self.registry.list_modules_by_state(st))
266 except Exception as e:
267 logger.debug(f"列出某状态模块失败: {st} - {e}")
268 continue
269 return sorted(names)
271 def install_module(self, name: str, config: dict[str, Any] | None = None, source: str | None = None) -> None:
272 """安装指定模块:必要时自动加载后再安装.
274 Args:
275 name: 模块名称
276 config: 配置字典(可选)
277 source: 模块源(可选)
278 """
279 if not self.registry.is_loaded(name): 279 ↛ 280line 279 didn't jump to line 280 because the condition on line 279 was never true
280 self.load_module(name, source)
281 self.registry.install(name, config or {})
283 def uninstall_module(self, name: str) -> None:
284 """卸载模块(若在运行则先停止由注册表处理).
286 Args:
287 name: 模块名称
288 """
289 self.registry.uninstall(name)
291 def start_module(self, name: str) -> None:
292 """启动模块.
294 Args:
295 name: 模块名称
296 """
297 # 若未加载,尝试加载
298 if not self.registry.is_loaded(name): 298 ↛ 299line 298 didn't jump to line 299 because the condition on line 298 was never true
299 self.load_module(name)
300 self.registry.start(name)
302 def stop_module(self, name: str) -> None:
303 """停止模块.
305 Args:
306 name: 模块名称
307 """
308 self.registry.stop(name)
310 def reload_module(self, name: str) -> None:
311 """重载模块.
313 Args:
314 name: 模块名称
315 """
316 # 失效缓存
317 self._invalidate_directory_cache()
318 if not self.registry.is_loaded(name): 318 ↛ 319line 318 didn't jump to line 319 because the condition on line 318 was never true
319 self.load_module(name)
320 self.registry.reload(name)
322 def start_all_modules(self) -> None:
323 """启动所有已安装/已停止的模块."""
324 for name in self.list_installed_modules():
325 try:
326 self.registry.start(name)
327 except Exception as e:
328 logger.warning(f"启动模块失败: {name} - {e}")
330 def stop_all_modules(self) -> None:
331 """停止所有运行中的模块."""
332 for name in self.registry.list_modules_by_state(ModuleState.STARTED):
333 try:
334 self.registry.stop(name)
335 except Exception as e:
336 logger.warning(f"停止模块失败: {name} - {e}")
338 def reload_all_modules(self) -> None:
339 """重载所有已加载的模块."""
340 for name in self.list_modules():
341 try:
342 self.registry.reload(name)
343 except Exception as e:
344 logger.warning(f"重载模块失败: {name} - {e}")