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

1"""模块管理器实现.""" 

2 

3from typing import Any 

4 

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 

11 

12logger = get_logger() 

13 

14 

15class ModuleManager: 

16 """模块管理器 - 统一的模块管理门面.""" 

17 

18 def __init__( 

19 self, 

20 module_dirs: list[str] | None = None, 

21 exclude_modules: set[str] | None = None, 

22 ) -> None: 

23 """初始化模块管理器. 

24 

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]] = {} 

38 

39 def _invalidate_directory_cache(self, directory: str | None = None) -> None: 

40 """失效目录缓存,支持单目录或全部清理. 

41 

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

51 

52 def _get_modules_from_directory(self, directory: str) -> dict[str, type[ModuleInterface]]: 

53 """获取目录中的模块,带缓存. 

54 

55 Args: 

56 directory: 目录路径 

57 

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] 

65 

66 def _discover_from_directory(self, directory: str) -> list[str]: 

67 """发现目录模块名称,带缓存. 

68 

69 Args: 

70 directory: 目录路径 

71 

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] 

79 

80 @staticmethod 

81 def _match_module_by_name( 

82 modules: dict[str, type[ModuleInterface]], 

83 target_name: str, 

84 ) -> type[ModuleInterface] | None: 

85 """在模块字典中按名称匹配模块类(忽略大小写与 Module 后缀). 

86 

87 Args: 

88 modules: 模块类字典 

89 target_name: 目标模块名 

90 

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 

100 

101 def load_module(self, name: str, source: str | None = None, source_type: str = "directory") -> ModuleInterface: 

102 """加载模块并注册到注册表,返回模块实例. 

103 

104 Args: 

105 name: 模块名称 

106 source: 模块源(可选) 

107 source_type: 源类型("directory" 或 "package") 

108 

109 Returns: 

110 模块实例 

111 

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 

120 

121 found_module: type[ModuleInterface] | None = None 

122 

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}") 

140 

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) 

143 

144 # 注册模块(将类传入注册表,由注册表负责实例化与生命周期) 

145 module_instance = self.registry.register(name, found_module) # type: ignore[arg-type] 

146 return module_instance 

147 

148 def load_all_modules(self) -> dict[str, ModuleInterface]: 

149 """加载所有可用模块. 

150 

151 Returns: 

152 模块名到模块实例的映射 

153 """ 

154 modules: dict[str, ModuleInterface] = {} 

155 available_modules = self.discover_modules() 

156 

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}") 

167 

168 return modules 

169 

170 def discover_modules(self, source: str | None = None, source_type: str = "directory") -> list[str]: 

171 """发现可用模块名称列表. 

172 

173 Args: 

174 source: 模块源(可选) 

175 source_type: 源类型 

176 

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) 

195 

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) 

206 

207 def get_module(self, name: str) -> ModuleInterface: 

208 """获取已加载的模块. 

209 

210 Args: 

211 name: 模块名称 

212 

213 Returns: 

214 模块实例 

215 

216 Raises: 

217 ModuleNotFoundException: 模块未找到时抛出 

218 """ 

219 module = self.registry.get(name) 

220 if module is None: 

221 raise ModuleNotFoundException(f"模块未找到: {name}") 

222 return module 

223 

224 def unload_module(self, name: str) -> None: 

225 """卸载模块. 

226 

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}") 

236 

237 def list_modules(self) -> list[str]: 

238 """列出所有已加载的模块. 

239 

240 Returns: 

241 模块名称列表 

242 """ 

243 return self.registry.list_modules() 

244 

245 def is_module_loaded(self, name: str) -> bool: 

246 """检查模块是否已加载. 

247 

248 Args: 

249 name: 模块名称 

250 

251 Returns: 

252 是否已加载 

253 """ 

254 return self.registry.is_loaded(name) 

255 

256 def list_installed_modules(self) -> list[str]: 

257 """列出已安装(包含 installed/started/stopped)的模块名称. 

258 

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) 

270 

271 def install_module(self, name: str, config: dict[str, Any] | None = None, source: str | None = None) -> None: 

272 """安装指定模块:必要时自动加载后再安装. 

273 

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

282 

283 def uninstall_module(self, name: str) -> None: 

284 """卸载模块(若在运行则先停止由注册表处理). 

285 

286 Args: 

287 name: 模块名称 

288 """ 

289 self.registry.uninstall(name) 

290 

291 def start_module(self, name: str) -> None: 

292 """启动模块. 

293 

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) 

301 

302 def stop_module(self, name: str) -> None: 

303 """停止模块. 

304 

305 Args: 

306 name: 模块名称 

307 """ 

308 self.registry.stop(name) 

309 

310 def reload_module(self, name: str) -> None: 

311 """重载模块. 

312 

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) 

321 

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}") 

329 

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}") 

337 

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}")