Coverage for src/symphra_modules/manager.py: 70.28%

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

2

3 from typing import Any

4

5 from symphra_modules.abc import ModuleInterface

6 from symphra_modules.config import ModuleState

7 from symphra_modules.exceptions import ModuleNotFoundException

8 from symphra_modules.loader import DirectoryLoader, PackageLoader

9 from symphra_modules.registry import ModuleRegistry

10 from symphra_modules.utils import get_logger

11

12 logger = get_logger()

13

14

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