diff --git a/app/modules/postgresql/__init__.py b/app/modules/postgresql/__init__.py index 9ceaa831a..806d07017 100644 --- a/app/modules/postgresql/__init__.py +++ b/app/modules/postgresql/__init__.py @@ -1,10 +1,12 @@ from typing import Tuple, Union -from app.runtime.config import settings from app.application.database import get_database_governance from app.modules import _ModuleBase +from app.runtime.settings import RuntimeSettingsCompat from app.schemas.types import ModuleType, OtherModulesType +settings = RuntimeSettingsCompat() + class PostgreSQLModule(_ModuleBase): """ @@ -12,10 +14,12 @@ class PostgreSQLModule(_ModuleBase): """ def init_module(self) -> None: + """PostgreSQL 连接由数据库 adapter 管理,无需模块级初始化。""" pass @staticmethod def get_name() -> str: + """返回模块展示名称。""" return "PostgreSQL" @staticmethod @@ -40,9 +44,11 @@ class PostgreSQLModule(_ModuleBase): return 0 def init_setting(self) -> Tuple[str, Union[str, bool]]: + """数据库类型由部署配置决定,不声明独立模块开关。""" pass def stop(self) -> None: + """连接池由数据库生命周期释放,本模块无独立资源。""" pass def test(self): diff --git a/app/modules/qbittorrent/__init__.py b/app/modules/qbittorrent/__init__.py index 33d366d75..6abcd7744 100644 --- a/app/modules/qbittorrent/__init__.py +++ b/app/modules/qbittorrent/__init__.py @@ -4,9 +4,9 @@ from typing import Set, Tuple, Optional, Union, List, Dict from qbittorrentapi import TorrentFilesList from app.schemas.dashboard import DownloaderInfo as _SchemaDownloaderInfo -from app.runtime.config import settings from app.domain.metainfo import MetaInfo from app.runtime.log import logger +from app.runtime.settings import RuntimeSettingsCompat from app.modules._base import _DownloaderModuleBase from app.modules.qbittorrent.qbittorrent import Qbittorrent from app.schemas.transfer import DownloaderTorrent @@ -21,6 +21,8 @@ from app.foundation import size as size_tools from app.foundation import temporal as time_tools from app.foundation import text as text_tools +settings = RuntimeSettingsCompat() + _QBITTORRENT_DOWNLOADING_STATES = { "allocating", "checkingdl", diff --git a/app/modules/redis/__init__.py b/app/modules/redis/__init__.py index d45584e95..dac3b312d 100644 --- a/app/modules/redis/__init__.py +++ b/app/modules/redis/__init__.py @@ -1,10 +1,12 @@ from typing import Tuple, Union -from app.runtime.config import settings from app.adapters.cache.redis import RedisHelper from app.modules import _ModuleBase +from app.runtime.settings import RuntimeSettingsCompat from app.schemas.types import ModuleType, OtherModulesType +settings = RuntimeSettingsCompat() + class RedisModule(_ModuleBase): """ @@ -12,10 +14,12 @@ class RedisModule(_ModuleBase): """ def init_module(self) -> None: + """Redis 客户端由缓存 adapter 惰性管理,无需模块级初始化。""" pass @staticmethod def get_name() -> str: + """返回模块展示名称。""" return "Redis缓存" @staticmethod @@ -40,9 +44,11 @@ class RedisModule(_ModuleBase): return 0 def init_setting(self) -> Tuple[str, Union[str, bool]]: + """缓存后端由部署配置决定,不声明独立模块开关。""" pass def stop(self) -> None: + """缓存 adapter 负责连接释放,本模块无独立资源。""" pass def test(self): diff --git a/app/modules/rtorrent/__init__.py b/app/modules/rtorrent/__init__.py index 48dc90b06..bca5fca86 100644 --- a/app/modules/rtorrent/__init__.py +++ b/app/modules/rtorrent/__init__.py @@ -2,9 +2,9 @@ from pathlib import Path from typing import Set, Tuple, Optional, Union, List, Dict from app.schemas.dashboard import DownloaderInfo as _SchemaDownloaderInfo -from app.runtime.config import settings from app.domain.metainfo import MetaInfo from app.runtime.log import logger +from app.runtime.settings import RuntimeSettingsCompat from app.modules._base import _DownloaderModuleBase from app.modules.rtorrent.rtorrent import Rtorrent from app.schemas.transfer import DownloaderTorrent @@ -19,8 +19,12 @@ from app.foundation import size as size_tools from app.foundation import temporal as time_tools from app.foundation import text as text_tools +settings = RuntimeSettingsCompat() + class RtorrentModule(_DownloaderModuleBase[Rtorrent]): + """rTorrent 下载器模块,负责任务添加、标签和文件状态转换。""" + def init_module(self) -> None: """ 初始化模块 @@ -31,6 +35,7 @@ class RtorrentModule(_DownloaderModuleBase[Rtorrent]): @staticmethod def get_name() -> str: + """返回模块展示名称。""" return "Rtorrent" @staticmethod @@ -55,9 +60,11 @@ class RtorrentModule(_DownloaderModuleBase[Rtorrent]): return 3 def stop(self): + """下载器客户端由服务基类管理,本模块无额外停止动作。""" pass def init_setting(self) -> Tuple[str, Union[str, bool]]: + """下载器实例由系统配置管理,不声明独立模块开关。""" pass def download( diff --git a/app/modules/transmission/__init__.py b/app/modules/transmission/__init__.py index cafd326a5..b7e01a7e2 100644 --- a/app/modules/transmission/__init__.py +++ b/app/modules/transmission/__init__.py @@ -4,9 +4,9 @@ from typing import Set, Tuple, Optional, Union, List, Dict from transmission_rpc import File from app.schemas.dashboard import DownloaderInfo as _SchemaDownloaderInfo -from app.runtime.config import settings from app.domain.metainfo import MetaInfo from app.runtime.log import logger +from app.runtime.settings import RuntimeSettingsCompat from app.modules._base import _DownloaderModuleBase from app.modules.transmission.transmission import Transmission from app.schemas.transfer import DownloaderTorrent @@ -20,6 +20,8 @@ from app.schemas.types import ( from app.foundation import size as size_tools from app.foundation import temporal as time_tools +settings = RuntimeSettingsCompat() + _TRANSMISSION_DOWNLOADING_STATES = { "download_pending", "downloading", @@ -30,6 +32,7 @@ _TRANSMISSION_PAUSED_STATES = { class TransmissionModule(_DownloaderModuleBase[Transmission]): + """Transmission 下载器模块,负责任务添加、标签和文件选择。""" def init_module(self) -> None: """ @@ -40,6 +43,7 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]): @staticmethod def get_name() -> str: + """返回模块展示名称。""" return "Transmission" @staticmethod @@ -64,9 +68,11 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]): return 2 def stop(self): + """下载器客户端由服务基类管理,本模块无额外停止动作。""" pass def init_setting(self) -> Tuple[str, Union[str, bool]]: + """下载器实例由系统配置管理,不声明独立模块开关。""" pass def download(self, content: Union[Path, str, bytes], download_dir: Path, cookie: str, diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index e34f67d19..61197d038 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -96,6 +96,9 @@ concrete `PluginManager`,不改变 V1/V2/V3 插件加载与自由响应 API。 Command 的 API 消费点也已完成原计划迁移:WebAgent 查询和站点认证后的刷新统一使用 `app.application.commands`,只有 startup 组合根注册 concrete Command;门面保留原命令对象与插件命令语义。 + 宿主 Module 的部署配置读取也已统一到 `RuntimeSettingsCompat`:PostgreSQL、Redis、qBittorrent、 + rTorrent 和 Transmission 不再直接导入全局 Settings,模块目录由架构测试保持零直连;兼容代理仍保留 + 启动早期与旧插件/测试的动态 Settings 注入语义。 ### P2:中长期可演进性债务 diff --git a/tests/fixtures/architecture/configuration-debt-baseline.json b/tests/fixtures/architecture/configuration-debt-baseline.json index 70f792df2..2ba30282f 100644 --- a/tests/fixtures/architecture/configuration-debt-baseline.json +++ b/tests/fixtures/architecture/configuration-debt-baseline.json @@ -10,16 +10,11 @@ "root": "app" }, "settings_imports": { - "count": 8, + "count": 3, "files": [ "app/db/base.py", "app/db/engine.py", - "app/db/session.py", - "app/modules/postgresql/__init__.py", - "app/modules/qbittorrent/__init__.py", - "app/modules/redis/__init__.py", - "app/modules/rtorrent/__init__.py", - "app/modules/transmission/__init__.py" + "app/db/session.py" ] }, "system_config_oper_constructions": { diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index dbbff83e7..dd3410928 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -14,7 +14,7 @@ "workflow_to_db": [] }, "edge_count": 6499, - "edge_sha256": "f21eea0878e60d4b955a87da15cf0653eaecf07d147dc43e9eabc9f9573bb793", + "edge_sha256": "54ddd2d6cd5669354a2d1303b24c95e573368e145fdd74e4240b01bb8ef1f687", "edges": [ "app -> app.runtime", "app -> app.runtime.compat", @@ -4808,7 +4808,7 @@ "app.modules.postgresql -> app.application.database", "app.modules.postgresql -> app.modules", "app.modules.postgresql -> app.runtime", - "app.modules.postgresql -> app.runtime.config", + "app.modules.postgresql -> app.runtime.settings", "app.modules.postgresql -> app.schemas", "app.modules.postgresql -> app.schemas.types", "app.modules.qbittorrent -> app.domain", @@ -4821,8 +4821,8 @@ "app.modules.qbittorrent -> app.modules._base", "app.modules.qbittorrent -> app.modules.qbittorrent.qbittorrent", "app.modules.qbittorrent -> app.runtime", - "app.modules.qbittorrent -> app.runtime.config", "app.modules.qbittorrent -> app.runtime.log", + "app.modules.qbittorrent -> app.runtime.settings", "app.modules.qbittorrent -> app.schemas", "app.modules.qbittorrent -> app.schemas.dashboard", "app.modules.qbittorrent -> app.schemas.transfer", @@ -4879,7 +4879,7 @@ "app.modules.redis -> app.adapters.cache.redis", "app.modules.redis -> app.modules", "app.modules.redis -> app.runtime", - "app.modules.redis -> app.runtime.config", + "app.modules.redis -> app.runtime.settings", "app.modules.redis -> app.schemas", "app.modules.redis -> app.schemas.types", "app.modules.rtorrent -> app.domain", @@ -4892,8 +4892,8 @@ "app.modules.rtorrent -> app.modules._base", "app.modules.rtorrent -> app.modules.rtorrent.rtorrent", "app.modules.rtorrent -> app.runtime", - "app.modules.rtorrent -> app.runtime.config", "app.modules.rtorrent -> app.runtime.log", + "app.modules.rtorrent -> app.runtime.settings", "app.modules.rtorrent -> app.schemas", "app.modules.rtorrent -> app.schemas.dashboard", "app.modules.rtorrent -> app.schemas.transfer", @@ -5248,8 +5248,8 @@ "app.modules.transmission -> app.modules._base", "app.modules.transmission -> app.modules.transmission.transmission", "app.modules.transmission -> app.runtime", - "app.modules.transmission -> app.runtime.config", "app.modules.transmission -> app.runtime.log", + "app.modules.transmission -> app.runtime.settings", "app.modules.transmission -> app.schemas", "app.modules.transmission -> app.schemas.dashboard", "app.modules.transmission -> app.schemas.transfer", diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py index 0d2a4bbbf..169b196b2 100644 --- a/tests/test_architecture_dependencies.py +++ b/tests/test_architecture_dependencies.py @@ -926,6 +926,21 @@ def test_runtime_consumers_use_command_application_facade(): assert violations == {} +def test_modules_read_deployment_settings_through_runtime_port(): + """宿主 Module 不得绕过 runtime 配置端口直接依赖 Settings 实例。""" + violations: list[str] = [] + for path in (APP_ROOT / "modules").rglob("*.py"): + tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) + for node in ast.walk(tree): + if not isinstance(node, ast.ImportFrom) or node.module != "app.runtime.config": + continue + if any(alias.name == "settings" for alias in node.names): + violations.append(path.relative_to(PROJECT_ROOT).as_posix()) + break + + assert violations == [] + + def test_api_does_not_import_factory(): """装配器(factory)只允许 app.main 使用,HTTP 端点不得回引。""" violations: dict[str, set[str]] = {} diff --git a/tests/test_downloader_path_mapping.py b/tests/test_downloader_path_mapping.py index 963e9ad71..2343a7eb4 100644 --- a/tests/test_downloader_path_mapping.py +++ b/tests/test_downloader_path_mapping.py @@ -13,8 +13,16 @@ def _load_downloader_base(): app_module.__path__ = [] helper_module = types.ModuleType("app.helper") helper_module.__path__ = [] + runtime_module = types.ModuleType("app.runtime") + runtime_module.__path__ = [] + runtime_extensions_module = types.ModuleType("app.runtime.extensions") + runtime_extensions_module.__path__ = [] service_module = types.ModuleType("app.runtime.extensions.service_config") + log_module = types.ModuleType("app.runtime.log") schemas_module = types.ModuleType("app.schemas") + schemas_module.__path__ = [] + schema_message_module = types.ModuleType("app.schemas.message") + schema_system_module = types.ModuleType("app.schemas.system") schema_types_module = types.ModuleType("app.schemas.types") utils_module = types.ModuleType("app.utils") utils_module.__path__ = [] @@ -27,6 +35,13 @@ def _load_downloader_base(): class _ConfigReloadMixin: pass + class _Logger: + """隔离模块基类加载时使用的无副作用日志桩。""" + + def error(self, *_args, **_kwargs): + """忽略测试范围外的错误日志输出。""" + pass + class _ServiceConfigHelper: @staticmethod def get_downloader_configs(): @@ -59,24 +74,37 @@ def _load_downloader_base(): ) service_module.ServiceConfigHelper = _ServiceConfigHelper + log_module.logger = _Logger() mixins_module.ConfigReloadMixin = _ConfigReloadMixin - schemas_module.Message = object - schemas_module.NotificationConf = object - schemas_module.MediaServerConf = object - schemas_module.DownloaderConf = object + schema_message_module.Message = object + schema_system_module.NotificationConf = object + schema_system_module.MediaServerConf = object + schema_system_module.DownloaderConf = object app_module.helper = helper_module + app_module.runtime = runtime_module app_module.schemas = schemas_module app_module.utils = utils_module helper_module.service = service_module + runtime_module.extensions = runtime_extensions_module + runtime_module.log = log_module + runtime_module.reload = mixins_module + runtime_extensions_module.service_config = service_module + schemas_module.message = schema_message_module + schemas_module.system = schema_system_module schemas_module.types = schema_types_module utils_module.mixins = mixins_module stub_modules = { "app": app_module, "app.helper": helper_module, + "app.runtime": runtime_module, + "app.runtime.extensions": runtime_extensions_module, "app.runtime.extensions.service_config": service_module, + "app.runtime.log": log_module, "app.schemas": schemas_module, + "app.schemas.message": schema_message_module, + "app.schemas.system": schema_system_module, "app.schemas.types": schema_types_module, "app.utils": utils_module, "app.runtime.reload": mixins_module, @@ -108,7 +136,10 @@ def _load_transmission_module(): torrent_rules_module = types.ModuleType("app.domain.torrent") size_tools_module = types.ModuleType("app.foundation.size") temporal_tools_module = types.ModuleType("app.foundation.temporal") + runtime_module = types.ModuleType("app.runtime") + runtime_module.__path__ = [] cache_module = types.ModuleType("app.runtime.cache") + runtime_settings_module = types.ModuleType("app.runtime.settings") base_module = types.ModuleType("app.modules._base") modules_module = types.ModuleType("app.modules") modules_module.__path__ = [] @@ -116,6 +147,9 @@ def _load_transmission_module(): transmission_package_module.__path__ = [] transmission_client_module = types.ModuleType("app.modules.transmission.transmission") schemas_module = types.ModuleType("app.schemas") + schemas_module.__path__ = [] + schema_dashboard_module = types.ModuleType("app.schemas.dashboard") + schema_transfer_module = types.ModuleType("app.schemas.transfer") schema_types_module = types.ModuleType("app.schemas.types") config_module = types.ModuleType("app.runtime.config") metainfo_module = types.ModuleType("app.domain.metainfo") @@ -225,12 +259,19 @@ def _load_transmission_module(): def get(self, *_args, **_kwargs): return None + class _RuntimeSettingsCompat: + """隔离测试用动态配置代理,保持生产模块的兼容读取语义。""" + + def __getattr__(self, key): + """从测试提供的旧 Settings 桩读取配置项。""" + return getattr(config_module.settings, key) + transmission_client_module.Transmission = object cache_module.FileCache = _FileCache - schemas_module.TransferTorrent = _TransferTorrent - schemas_module.DownloadingTorrent = _DownloadingTorrent - schemas_module.DownloaderTorrent = _DownloaderTorrent - schemas_module.DownloaderInfo = object + schema_transfer_module.TransferTorrent = _TransferTorrent + schema_transfer_module.DownloadingTorrent = _DownloadingTorrent + schema_transfer_module.DownloaderTorrent = _DownloaderTorrent + schema_dashboard_module.DownloaderInfo = object schema_types_module.TorrentStatus = TorrentStatus schema_types_module.TorrentQueryStatus = TorrentQueryStatus schema_types_module.DownloadTaskState = DownloadTaskState @@ -239,6 +280,7 @@ def _load_transmission_module(): "DownloaderType", {"Transmission": "Transmission"} ) config_module.settings = SimpleNamespace(TORRENT_TAG="moviepilot-tag") + runtime_settings_module.RuntimeSettingsCompat = _RuntimeSettingsCompat metainfo_module.MetaInfo = _MetaInfo log_module.logger = _Logger() modules_module._ModuleBase = _ModuleBase @@ -257,6 +299,7 @@ def _load_transmission_module(): app_module.domain = domain_module app_module.foundation = foundation_module app_module.modules = modules_module + app_module.runtime = runtime_module app_module.schemas = schemas_module domain_module.torrent = torrent_rules_module foundation_module.size = size_tools_module @@ -264,8 +307,14 @@ def _load_transmission_module(): core_module.cache = cache_module core_module.config = config_module core_module.metainfo = metainfo_module + runtime_module.cache = cache_module + runtime_module.config = config_module + runtime_module.log = log_module + runtime_module.settings = runtime_settings_module modules_module.transmission = transmission_package_module transmission_package_module.transmission = transmission_client_module + schemas_module.dashboard = schema_dashboard_module + schemas_module.transfer = schema_transfer_module schemas_module.types = schema_types_module torrentool_module.torrent = torrentool_torrent_module @@ -279,13 +328,17 @@ def _load_transmission_module(): "app.foundation.temporal": temporal_tools_module, "app.runtime.cache": cache_module, "app.runtime.config": config_module, + "app.runtime": runtime_module, "app.domain.metainfo": metainfo_module, "app.runtime.log": log_module, + "app.runtime.settings": runtime_settings_module, "app.modules": modules_module, "app.modules._base": base_module, "app.modules.transmission": transmission_package_module, "app.modules.transmission.transmission": transmission_client_module, "app.schemas": schemas_module, + "app.schemas.dashboard": schema_dashboard_module, + "app.schemas.transfer": schema_transfer_module, "app.schemas.types": schema_types_module, "transmission_rpc": transmission_rpc_module, "torrentool": torrentool_module, diff --git a/tests/test_qbittorrent_compat.py b/tests/test_qbittorrent_compat.py index cd3fdcb82..076ac6eed 100644 --- a/tests/test_qbittorrent_compat.py +++ b/tests/test_qbittorrent_compat.py @@ -26,11 +26,17 @@ def _load_qbittorrent_modules(): modules_module.__path__ = [] qbittorrent_package_module = types.ModuleType("app.modules.qbittorrent") qbittorrent_package_module.__path__ = [] + runtime_module = types.ModuleType("app.runtime") + runtime_module.__path__ = [] log_module = types.ModuleType("app.runtime.log") cache_module = types.ModuleType("app.runtime.cache") config_module = types.ModuleType("app.runtime.config") + runtime_settings_module = types.ModuleType("app.runtime.settings") metainfo_module = types.ModuleType("app.domain.metainfo") schemas_module = types.ModuleType("app.schemas") + schemas_module.__path__ = [] + schema_dashboard_module = types.ModuleType("app.schemas.dashboard") + schema_transfer_module = types.ModuleType("app.schemas.transfer") schema_types_module = types.ModuleType("app.schemas.types") torrentool_module = types.ModuleType("torrentool") torrentool_module.__path__ = [] @@ -78,6 +84,13 @@ def _load_qbittorrent_modules(): def get(self, *_args, **_kwargs): return None + class _RuntimeSettingsCompat: + """隔离测试用动态配置代理,保持生产模块的兼容读取语义。""" + + def __getattr__(self, key): + """从测试提供的旧 Settings 桩读取配置项。""" + return getattr(config_module.settings, key) + class _MetaInfo: def __init__(self, name): self.name = name @@ -160,11 +173,12 @@ def _load_qbittorrent_modules(): log_module.logger = _Logger() cache_module.FileCache = _FileCache config_module.settings = types.SimpleNamespace(TORRENT_TAG="moviepilot-tag") + runtime_settings_module.RuntimeSettingsCompat = _RuntimeSettingsCompat metainfo_module.MetaInfo = _MetaInfo - schemas_module.DownloaderInfo = object - schemas_module.TransferTorrent = object - schemas_module.DownloadingTorrent = object - schemas_module.DownloaderTorrent = _DownloaderTorrent + schema_dashboard_module.DownloaderInfo = object + schema_transfer_module.TransferTorrent = object + schema_transfer_module.DownloadingTorrent = object + schema_transfer_module.DownloaderTorrent = _DownloaderTorrent schema_types_module.TorrentStatus = TorrentStatus schema_types_module.TorrentQueryStatus = TorrentQueryStatus schema_types_module.DownloadTaskState = DownloadTaskState @@ -195,6 +209,7 @@ def _load_qbittorrent_modules(): app_module.foundation = foundation_module app_module.log = log_module app_module.modules = modules_module + app_module.runtime = runtime_module app_module.schemas = schemas_module domain_module.torrent = torrent_rules_module foundation_module.size = size_tools_module @@ -204,6 +219,12 @@ def _load_qbittorrent_modules(): core_module.cache = cache_module core_module.config = config_module core_module.metainfo = metainfo_module + runtime_module.cache = cache_module + runtime_module.config = config_module + runtime_module.log = log_module + runtime_module.settings = runtime_settings_module + schemas_module.dashboard = schema_dashboard_module + schemas_module.transfer = schema_transfer_module schemas_module.types = schema_types_module modules_module.qbittorrent = qbittorrent_package_module torrentool_module.torrent = torrentool_torrent_module @@ -220,12 +241,16 @@ def _load_qbittorrent_modules(): "app.foundation.url": url_tools_module, "app.runtime.cache": cache_module, "app.runtime.config": config_module, + "app.runtime": runtime_module, "app.domain.metainfo": metainfo_module, "app.runtime.log": log_module, + "app.runtime.settings": runtime_settings_module, "app.modules": modules_module, "app.modules._base": base_module, "app.modules.qbittorrent": qbittorrent_package_module, "app.schemas": schemas_module, + "app.schemas.dashboard": schema_dashboard_module, + "app.schemas.transfer": schema_transfer_module, "app.schemas.types": schema_types_module, "qbittorrentapi": qbittorrentapi_module, "qbittorrentapi.client": qbittorrentapi_client_module,