mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
refactor(architecture): 修复模块依赖违规并强化架构守护
- 字幕编排上移 DownloadChain.download_site_subtitles,SubtitleModule 仅保留站点链接解析 - TransferChain.recommend_name 上移 TV episodes_info 获取,filemanager 模块不再导入 TmdbChain - endpoint 穿透修复:WXBizMsgCrypt3 迁至 adapters/external/wechat_crypt.py; music/tmdb 缓存管理、listenbrainz 常量、TMDbException、WechatClawBot 辅助统一经 chain 包装 - RuleParser 与 builtin_rules 合并为 application/filter_rules.py; fsproxy/fsworker 迁至 adapters/system/ - chain/__init__.py 删除 qbittorrentapi/transmission_rpc 导入,消除后端协议类型泄漏 - 架构守护测试新增三项检查:模块间隔离、入口层穿透、下载器 SDK 泄漏 - 文档同步:05-architecture.md 记录 DB/Oper 聚合例外与迁移文件位置,AGENTS.md 更新所有权表
This commit is contained in:
@@ -428,3 +428,65 @@ def test_resource_adapter_does_not_restart_process():
|
||||
set(modules),
|
||||
)
|
||||
assert "app.runtime.state" not in dependencies
|
||||
|
||||
|
||||
def test_modules_do_not_import_other_modules_or_chain():
|
||||
"""模块之间以及模块对链层的直接依赖被禁止,跨模块编排归链层。"""
|
||||
modules = _discover_modules()
|
||||
known_modules = set(modules)
|
||||
violations: dict[str, set[str]] = {}
|
||||
for module_name, path in modules.items():
|
||||
if not module_name.startswith("app.modules."):
|
||||
continue
|
||||
own_package = module_name.split(".")[2]
|
||||
dependencies = _resolve_imports(module_name, path, known_modules)
|
||||
forbidden = {
|
||||
dependency
|
||||
for dependency in dependencies
|
||||
if dependency.startswith("app.chain")
|
||||
or (
|
||||
dependency.startswith("app.modules.")
|
||||
and dependency.split(".")[2] != own_package
|
||||
)
|
||||
}
|
||||
if forbidden:
|
||||
violations[module_name] = forbidden
|
||||
assert violations == {}
|
||||
|
||||
|
||||
def test_entrypoints_do_not_import_module_internals():
|
||||
"""入口层不得穿透导入具体模块实现,应经由链层或应用服务。"""
|
||||
modules = _discover_modules()
|
||||
known_modules = set(modules)
|
||||
entrypoint_roots = ("app.api", "app.agent", "app.monitor", "app.workflow", "app.doctor")
|
||||
violations: dict[str, set[str]] = {}
|
||||
for module_name, path in modules.items():
|
||||
if not module_name.startswith(entrypoint_roots):
|
||||
continue
|
||||
dependencies = _resolve_imports(module_name, path, known_modules)
|
||||
forbidden = {
|
||||
dependency
|
||||
for dependency in dependencies
|
||||
if dependency.startswith("app.modules.")
|
||||
}
|
||||
if forbidden:
|
||||
violations[module_name] = forbidden
|
||||
assert violations == {}
|
||||
|
||||
|
||||
def test_chain_does_not_import_downloader_sdks():
|
||||
"""链层不得引入下载器后端协议类型,避免后端细节泄漏到编排层。"""
|
||||
forbidden_sdks = {"qbittorrentapi", "transmission_rpc"}
|
||||
violations: list[str] = []
|
||||
for path in (APP_ROOT / "chain").rglob("*.py"):
|
||||
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
|
||||
for node in ast.walk(tree):
|
||||
names: list[str] = []
|
||||
if isinstance(node, ast.Import):
|
||||
names.extend(alias.name for alias in node.names)
|
||||
elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
|
||||
names.append(node.module)
|
||||
if any(name.split(".")[0] in forbidden_sdks for name in names):
|
||||
violations.append(str(path.relative_to(PROJECT_ROOT)))
|
||||
break
|
||||
assert violations == []
|
||||
|
||||
Reference in New Issue
Block a user