refactor(runtime): govern dependency module naming

This commit is contained in:
jxxghp
2026-08-29 08:14:21 +08:00
parent 8f03935d86
commit 13fba96fd8
28 changed files with 216 additions and 56 deletions

View File

@@ -10,7 +10,7 @@ on:
- 'app/__init__.py'
- 'app/doctor/dependencies.py'
- 'app/foundation/environment.py'
- 'app/runtime/dependencies.py'
- 'app/runtime/dependencies/profile.py'
- 'scripts/verify_runtime_profile.py'
- 'docker/Dockerfile'
- 'docker/**'
@@ -24,7 +24,7 @@ on:
- 'app/__init__.py'
- 'app/doctor/dependencies.py'
- 'app/foundation/environment.py'
- 'app/runtime/dependencies.py'
- 'app/runtime/dependencies/profile.py'
- 'scripts/verify_runtime_profile.py'
- 'docker/Dockerfile'
- 'docker/**'

View File

@@ -8,7 +8,10 @@ on:
- 'app/adapters/external/market.py'
- 'app/adapters/system/host.py'
- 'app/adapters/system/package.py'
- 'app/runtime/dependencies.py'
- 'app/adapters/system/plugin/package.py'
- 'app/application/plugin/install.py'
- 'app/runtime/dependencies/native.py'
- 'app/runtime/dependencies/profile.py'
- 'scripts/probe_native_dependency_update.py'
- '.github/workflows/native-dependency-update.yml'
push:
@@ -18,7 +21,10 @@ on:
- 'app/adapters/external/market.py'
- 'app/adapters/system/host.py'
- 'app/adapters/system/package.py'
- 'app/runtime/dependencies.py'
- 'app/adapters/system/plugin/package.py'
- 'app/application/plugin/install.py'
- 'app/runtime/dependencies/native.py'
- 'app/runtime/dependencies/profile.py'
- 'scripts/probe_native_dependency_update.py'
- '.github/workflows/native-dependency-update.yml'
workflow_dispatch:

View File

@@ -50,7 +50,7 @@ from app.foundation.singleton import WeakSingleton
from app.foundation.url import UrlUtils
from app.foundation.version import compare_version
from app.runtime.cache import cached, is_fresh
from app.runtime.dependencies import (
from app.runtime.dependencies.profile import (
iter_runtime_profile_requirement_strings,
iter_runtime_requirement_strings,
runtime_excluded_dependency_pairs,

View File

@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit
from app.runtime.dependencies import runtime_sync_arguments
from app.runtime.dependencies.profile import runtime_sync_arguments
@dataclass(frozen=True)

View File

@@ -12,16 +12,16 @@ from typing import Any, Optional, cast
from app.adapters.external.market import PluginHelper as _PluginHelper
from app.adapters.system.host import SystemUtils
from app.runtime.execution import (
run_in_threadpool_to_completion as _await_thread_operation,
)
from app.runtime.log import logger
from app.runtime.native_dependencies import (
from app.runtime.dependencies.native import (
LoadedNativeDependencySnapshot,
NativeDependencyChange,
capture_loaded_native_dependencies,
detect_changed_native_dependencies,
)
from app.runtime.execution import (
run_in_threadpool_to_completion as _await_thread_operation,
)
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting

View File

@@ -17,9 +17,9 @@ from app.application.plugin.transaction import (
PluginInstallationRecord,
PluginPersistenceService,
)
from app.runtime.dependencies.native import NativeDependencyChange
from app.runtime.execution import await_task_to_terminal
from app.runtime.log import logger
from app.runtime.native_dependencies import NativeDependencyChange
from app.schemas.exception import (
PersistenceUnavailableError,
PluginMutationRejectedError,

View File

@@ -0,0 +1 @@
"""运行依赖 profile 与原生载荷激活边界。"""

View File

@@ -11,7 +11,6 @@ from packaging.utils import canonicalize_name
from app.foundation.environment import is_free_threaded_runtime
RUNTIME_STANDARD_GROUP = "runtime-standard"
RUNTIME_FREE_THREADED_GROUP = "runtime-free-threaded"

View File

@@ -151,6 +151,7 @@ function update_pending_state() {
function sync_project_dependencies_for() {
local project_dir="$1"
local legacy_runtime_selector
local runtime_group
local runtime_selector
local -a uv_cmd=(
@@ -164,9 +165,21 @@ function sync_project_dependencies_for() {
)
runtime_group=""
if grep -Eq '^runtime-(standard|free-threaded)[[:space:]]*=' "${project_dir}/pyproject.toml"; then
runtime_selector="${project_dir}/app/runtime/dependencies.py"
[ -f "${runtime_selector}" ] || return 1
runtime_group="$("${VENV_PATH}/bin/python3" "${runtime_selector}")" || return 1
runtime_selector="${project_dir}/app/runtime/dependencies/profile.py"
legacy_runtime_selector="${project_dir}/app/runtime/dependencies.py"
if [ -f "${runtime_selector}" ]; then
runtime_group="$(
PYTHONPATH="${project_dir}${PYTHONPATH:+:${PYTHONPATH}}" \
"${VENV_PATH}/bin/python3" -m app.runtime.dependencies.profile
)" || return 1
elif [ -f "${legacy_runtime_selector}" ]; then
runtime_group="$(
PYTHONPATH="${project_dir}${PYTHONPATH:+:${PYTHONPATH}}" \
"${VENV_PATH}/bin/python3" "${legacy_runtime_selector}"
)" || return 1
else
return 1
fi
[ -n "${runtime_group}" ] || return 1
fi
if [ -n "${runtime_group}" ]; then

View File

@@ -70,7 +70,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
| 指标 | 当前值 | 解释 |
|---|---:|---|
| 宿主 Python 模块 / 内部依赖边 | 932 / 7,886 | `dependency-baseline.json` 当前快照 |
| 宿主 Python 模块 / 内部依赖边 | 933 / 7,890 | `dependency-baseline.json` 当前快照 |
| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 |
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |
@@ -79,7 +79,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
| Python 源码量 | 约 271,400 行 | 60 个文件超过 1,000 行,14 个超过 2,000 行 |
| 长方法 | 281 个超过 80 行 | 67 个超过 150 行,23 个超过 250 行;大量是私有方法 |
| 全量 mypy 历史债务 | 10,982 / 599 文件 | strict frontier 当前覆盖 41 个文件,本批迁移路径的类型债务已清零 |
| Ruff 历史诊断 | 671 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
| Ruff 历史诊断 | 667 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
| 覆盖率低水位 | Application 80.75%,Domain 79.32% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |
### 3.3 热点文件

View File

@@ -192,7 +192,7 @@ SDK/Compat。领域查询与写入继续使用 Application 所属的冻结 DTO/P
|---|---|---|
| `app/foundation/` | 无状态、无配置、无 I/O 的底层原语:反射/动态导入、加密、DOM、单例、文本、URL、版本比较 | `reflection.py`、`crypto.py`、`singleton.py` |
| `app/domain/` | 纯 MoviePilot 业务语义:媒体上下文、识别解析、站点状态解释、磁力语义、NFO 刮削 | `context.py`、`metainfo.py`、`meta/`、`scraper.py` |
| `app/runtime/` | 进程级运行机制:配置、进程拓扑、事件、完整日志、缓存契约与内存后端、任务所有权、执行/关联上下文、并发、调度、限流、本地化、GC、重启状态 | `config.py`、`events.py`、`event/`、`tasks.py`、`execution.py`、`correlation.py`、`log.py`、`cache.py` |
| `app/runtime/` | 进程级运行机制:配置、进程拓扑、事件、完整日志、缓存契约与内存后端、运行依赖 profile 与原生载荷激活检测、任务所有权、执行/关联上下文、并发、调度、限流、本地化、GC、重启状态 | `config.py`、`events.py`、`event/`、`dependencies/`、`tasks.py`、`execution.py`、`correlation.py`、`log.py`、`cache.py` |
| `app/runtime/extensions/` | 模块 / 插件 / 配置化服务 / 托管资源的发现、注册与生命周期适配;旧管理器文件保留稳定 ABI 门面,具体实现拆在主题子包 | `module_manager.py`、`plugin_manager.py`、`plugin/` |
| `app/runtime/compat/` | 仅标准库的精确旧模块、包与符号导入路由;不是业务实现,也不是通用 re-export 层 | `manifest.py`、`imports.py` |
| `app/adapters/network/` | 通用 HTTP、浏览器、DNS、Cloudflare、IP 传输机制 | `http.py`、`browser.py` |
@@ -220,6 +220,8 @@ SDK/Compat。领域查询与写入继续使用 Application 所属的冻结 DTO/P
| `app/workflow/` | 工作流引擎 | — |
| `app/plugins/` | 插件运行时副本/覆盖目录,由插件管理器加载;不是官方插件源码或宿主架构实现,架构审计以插件仓库与宿主边界为准 | — |
`app/runtime/dependencies/` 是一个同名能力包:`profile.py` 只负责解释器 ABI 对应的依赖组,`native.py` 只负责已加载原生发行包的快照与变更检测。包根不重复导出实现,宿主直接导入职责子模块;旧的平级 `dependencies.py` 与 `native_dependencies.py` 已退出规范实现。
---
## 四、启动与生命周期
@@ -726,8 +728,8 @@ flowchart LR
| 指标 | 当前值 |
|---|---:|
| Python 模块 | 932 |
| 内部导入边 | 7,886 |
| Python 模块 | 933 |
| 内部导入边 | 7,890 |
| 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) |
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
| Direct egress | 55(债务已清零,55 条精确 containment) |

View File

@@ -139,7 +139,7 @@ canonical 主程序;兼容只经统一 Compat/SDK 门面提供。
| S3-L3 Scheduler | `VERIFIED` | S2-L3 | 旧 `app/scheduler.py` 已退役并迁入 `app.scheduler` 同名包;catalog、execution/bridge/progress、`ExecutionRegistry`、reconciler、lifecycle 与 maintenance 已分离,业务 callable 由 startup 经 `SchedulerServices` 注入,包内无参 Chain 构造清零;功能/生命周期 215 项、架构/兼容/文档 189 项通过,官方插件基线语义未变化 |
| S3-L4 DownloadChain | `DELIVERED` | S1-L6 | `7941fd921`:旧单体退役;selection、submission、batch、history、post-processing 等职责进入同名包单一 owner,提交后动作遵守新完成语义;锁定全量 `7162 passed, 9 skipped`,远端 `0/0` |
| S3-L5 SearchChain | `DELIVERED` | S2-L7 | `1c145d716`:旧单体退役并由同名职责包承接;稳定 ABI、官方插件兼容、类型/复杂度门禁和锁定全量 `7222 passed, 9 skipped` 通过;远端 `0/0` |
| S3-L5.1 Runtime dependencies 命名治理 | `ACTIVE` | S3-L5 | `dependencies.py` 与 `native_dependencies.py` 退役为 `runtime/dependencies/` 同名包;单词子模块、无包根重复导出,并把规则写入 `docs/rules/07-naming-conventions.md` 及机器门禁 |
| S3-L5.1 Runtime dependencies 命名治理 | `VERIFIED` | S3-L5 | `dependencies.py` 与 `native_dependencies.py` 退役为 `runtime/dependencies/` 同名包;单词子模块、无包根重复导出,并把规则写入 `docs/rules/07-naming-conventions.md` 及机器门禁 |
| S3-L6 MediaChain | `PLANNED` | S2-L2 | recognition、source projection、music alignment、cache 分离,兼容仅经统一层 |
| S3-L7 Agent/System/Plugin API | `PLANNED` | S2,S1-L6 | WebAgent SSE/file/audio、nettest/log/update/market 用例进入 Application,endpoint 只做传输适配 |
@@ -176,7 +176,7 @@ canonical 主程序;兼容只经统一 Compat/SDK 门面提供。
### S3-L5.1 Runtime dependencies 命名治理
**Status:** `ACTIVE`
**Status:** `VERIFIED`
**Outcome**
@@ -199,6 +199,14 @@ canonical 主程序;兼容只经统一 Compat/SDK 门面提供。
- 运行依赖分组、插件原生依赖激活、插件安装/包管理专项及架构/质量门禁全部通过。
- 提交推送后确认远端 SHA、祖先关系和 ahead/behind `0/0`。
**Evidence(2026-08-29)**
- 平铺实现已删除,`app/runtime/dependencies/` 仅有 `__init__.py`、`profile.py`、`native.py`;包根只有包说明,宿主导入全部直达职责子模块。
- Docker 更新边界同时覆盖新包载荷与旧版本 `dependencies.py` 快照回滚,未把旧实现恢复到主程序。
- 官方插件仓最新远端 `aa40b961a2093474a77edd9eed88844ff07425ac` 对旧路径及全部公开符号零命中,因此未新增无消费者的 SDK/Compat 债务。
- 运行依赖、原生激活、插件安装/包管理及架构专项 `406 passed`;最终架构/CI/Docker 门禁 `182 passed`。
- 锁定全量 `7224 passed, 9 skipped`;Pylint `10.00/10` 且重复率 `0.000%`;mypy、Ruff(历史诊断降至 `667`)、复杂度和依赖基线门禁全部通过。
### S3-L5 SearchChain
**Status:** `DELIVERED`

View File

@@ -100,6 +100,7 @@ directory categories.
| `app/runtime/execution.py` | Shared sync/async execution and cross-thread submission boundary with correlation propagation |
| `app/runtime/correlation.py` | Request/cross-thread correlation context and safe propagation into logs and child work |
| `app/runtime/state.py` | Process restart and update state |
| `app/runtime/dependencies/` | Runtime dependency profile selection in `profile.py` and loaded native payload activation detection in `native.py`; the package root contains no implementation or host re-exports |
| `app/runtime/extensions/` | Module, plugin, configured-service and managed-resource discovery/registration/lifecycle adapters |
| `app/runtime/compat/` | Standard-library-only exact legacy import routing, resource preflight scanning and DEBUG diagnostics |
@@ -114,6 +115,12 @@ API dependencies must narrow that object to a domain runtime (for example,
`AgentChatRuntime`) instead of adding a string key to a global service map.
Legacy registries may delegate the same object while domains migrate, but they
must not construct a second set of service instances.
The retired flat `app/runtime/dependencies.py` and
`app/runtime/native_dependencies.py` modules must not return. Canonical host
callers import `app.runtime.dependencies.profile` or
`app.runtime.dependencies.native` directly; the package root is not an export
facade, and plugin compatibility is added only through an exact SDK/Compat route
when a verified plugin contract requires it.
Canonical host consumers of the process-wide module, plugin, scheduler and
system-configuration runtimes must call `get_module_manager()`,
`get_plugin_manager()`, `get_scheduler()` and `get_configured_system_config()`
@@ -883,4 +890,4 @@ imports, entrypoint (`api`/`agent`/`monitor`/`workflow`/`doctor`) imports of
modules only through `run_module` dispatch), and downloader SDK
(`qbittorrentapi`, `transmission_rpc`) imports inside `app/chain`.
*Last Updated: 2026-08-28*
*Last Updated: 2026-08-29*

View File

@@ -16,6 +16,17 @@ All new code must follow these conventions. Consistent naming is how the codebas
| Alembic migrations | Auto-generated by Alembic; do not rename | `20240101_add_column.py` |
| Skill directories | `<kebab-case>/` | `transfer-failed-retry/`, `moviepilot-cli/` |
### Production Module Creation Gate
新增或拆分生产模块时必须按以下顺序决策,代码评审与架构测试以此为准:
1. 优先扩展当前职责 owner;只因文件变长或需要一个私有辅助类,不得新建平级模块。
2. 一个能力需要第二个生产文件时,必须在同一次变更中改为同名目录包,禁止继续增加 `<capability>_<role>.py` 平级文件。
3. 包内子文件使用准确、可独立说明职责的单个小写名词,如 `dependencies/profile.py` 与 `dependencies/native.py`;不得使用 `native_dependencies.py`、`transfer_execution.py` 这类重复能力名。
4. 包根 `__init__.py` 只允许包说明和确有外部契约证据的公开门面,不得复制实现,也不得为宿主代码重复导出子模块符号;宿主必须直接导入 owner 子模块。
5. 旧路径兼容只能在确认真实插件消费者后,通过 `app/sdk/` 或 `app/runtime/compat/` 做精确映射;不得为假设消费者保留旧源码、通配映射或双份导出。
6. 新增文件前必须同步更新本规则对应的机器门禁;若现有门禁不能表达该命名约束,应在同一变更中补充 `tests/test_architecture_dependencies.py`。
---
## Classes
@@ -120,8 +131,9 @@ All new code must follow these conventions. Consistent naming is how the codebas
| `configuration.get("RssUrls")` | `configuration.get(SystemConfigKey.RssUrls)` |
| `class subscribe_oper:` | `class SubscribeOper:` |
| `transfer.py` + `transfer_execution.py` | `transfer/workflow.py` + `transfer/execution.py` |
| `dependencies.py` + `native_dependencies.py` | `dependencies/profile.py` + `dependencies/native.py` |
| Package-root host re-exports for an old path | Exact SDK/Compat mapping; host code imports the owning child module |
| `MessageChannel.Telegram`(新代码) | `NotificationChannel.Telegram` |
| `Notification(title=...)`(新代码) | `Message(title=...)` |
*Last Updated: 2026-08-27*
*Last Updated: 2026-08-29*

View File

@@ -28,7 +28,7 @@ V3t 的目标不是让所有请求都更快。它主要改善可并行的 Python
- `runtime-free-threaded`
`tool.uv.conflicts` 保证两个组不能同时解析。Docker 的两个构建 stage 分别固定选择对应组;源码升级、
启动恢复和插件安装后的宿主恢复由 `app.runtime.dependencies.runtime_dependency_group()` 根据当前
启动恢复和插件安装后的宿主恢复由 `app.runtime.dependencies.profile.runtime_dependency_group()` 根据当前
解释器的 `Py_GIL_DISABLED` 构建标志选择 profile。
依赖选择不得改为镜像标签判断,也不得在业务模块中按包名散落 V3/V3t 分支。Dockerfile 只选择运行

View File

@@ -17,7 +17,7 @@ from packaging.requirements import InvalidRequirement, Requirement
from packaging.utils import canonicalize_name
from app.doctor import dependencies as dependency_doctor
from app.runtime.dependencies import runtime_excluded_dependency_pairs
from app.runtime.dependencies.profile import runtime_excluded_dependency_pairs
_UV_MISSING_DEPENDENCY_PATTERN = re.compile(
r"The package `(?P<package>[^`]+)` requires `(?P<requirement>[^`]+)`, "

View File

@@ -1119,8 +1119,8 @@
"runtime_only": true
}
},
"edge_count": 7886,
"edge_sha256": "52a65e7b862f6f0ed63c59bd0abeca8d7400ca566a4481f71ab556559664d515",
"edge_count": 7890,
"edge_sha256": "d184c440fcec26e316115b99871803e96a0e4938597d4ef603fac79a92640888",
"edges": [
"app -> app.foundation",
"app -> app.foundation.environment",
@@ -1170,6 +1170,7 @@
"app.adapters.external.market -> app.runtime",
"app.adapters.external.market -> app.runtime.cache",
"app.adapters.external.market -> app.runtime.dependencies",
"app.adapters.external.market -> app.runtime.dependencies.profile",
"app.adapters.external.market -> app.runtime.execution",
"app.adapters.external.market -> app.runtime.log",
"app.adapters.external.market -> app.runtime.observability",
@@ -1250,6 +1251,7 @@
"app.adapters.system.host -> app.schemas.dashboard",
"app.adapters.system.package -> app.runtime",
"app.adapters.system.package -> app.runtime.dependencies",
"app.adapters.system.package -> app.runtime.dependencies.profile",
"app.adapters.system.plugin.dependency -> app.adapters",
"app.adapters.system.plugin.dependency -> app.adapters.external",
"app.adapters.system.plugin.dependency -> app.adapters.external.market",
@@ -1267,9 +1269,10 @@
"app.adapters.system.plugin.package -> app.adapters.system",
"app.adapters.system.plugin.package -> app.adapters.system.host",
"app.adapters.system.plugin.package -> app.runtime",
"app.adapters.system.plugin.package -> app.runtime.dependencies",
"app.adapters.system.plugin.package -> app.runtime.dependencies.native",
"app.adapters.system.plugin.package -> app.runtime.execution",
"app.adapters.system.plugin.package -> app.runtime.log",
"app.adapters.system.plugin.package -> app.runtime.native_dependencies",
"app.adapters.system.plugin.package -> app.runtime.settings",
"app.adapters.system.resource -> app.adapters",
"app.adapters.system.resource -> app.adapters.network",
@@ -3919,9 +3922,10 @@
"app.application.plugin.install -> app.application.plugin.source",
"app.application.plugin.install -> app.application.plugin.transaction",
"app.application.plugin.install -> app.runtime",
"app.application.plugin.install -> app.runtime.dependencies",
"app.application.plugin.install -> app.runtime.dependencies.native",
"app.application.plugin.install -> app.runtime.execution",
"app.application.plugin.install -> app.runtime.log",
"app.application.plugin.install -> app.runtime.native_dependencies",
"app.application.plugin.install -> app.schemas",
"app.application.plugin.install -> app.schemas.exception",
"app.application.plugin.install -> app.schemas.plugin",
@@ -7768,8 +7772,10 @@
"app.runtime.config -> app.schemas.types",
"app.runtime.debounce -> app.runtime",
"app.runtime.debounce -> app.runtime.log",
"app.runtime.dependencies -> app.foundation",
"app.runtime.dependencies -> app.foundation.environment",
"app.runtime.dependencies.native -> app.runtime",
"app.runtime.dependencies.native -> app.runtime.log",
"app.runtime.dependencies.profile -> app.foundation",
"app.runtime.dependencies.profile -> app.foundation.environment",
"app.runtime.deprecation.policy -> app.runtime",
"app.runtime.deprecation.policy -> app.runtime.deprecation",
"app.runtime.deprecation.policy -> app.runtime.deprecation.notices",
@@ -7970,8 +7976,6 @@
"app.runtime.extensions.service_config -> app.schemas",
"app.runtime.extensions.service_config -> app.schemas.system",
"app.runtime.extensions.service_config -> app.schemas.types",
"app.runtime.native_dependencies -> app.runtime",
"app.runtime.native_dependencies -> app.runtime.log",
"app.runtime.observability -> app.runtime",
"app.runtime.observability -> app.runtime.deprecation",
"app.runtime.observability -> app.runtime.deprecation.policy",
@@ -9009,7 +9013,7 @@
"app.workflow.actions.transfer_file -> app.workflow",
"app.workflow.actions.transfer_file -> app.workflow.actions"
],
"module_count": 932,
"module_count": 933,
"modules": [
"app",
"app.adapters",
@@ -9759,6 +9763,8 @@
"app.runtime.correlation",
"app.runtime.debounce",
"app.runtime.dependencies",
"app.runtime.dependencies.native",
"app.runtime.dependencies.profile",
"app.runtime.deprecation",
"app.runtime.deprecation.notices",
"app.runtime.deprecation.policy",
@@ -9804,7 +9810,6 @@
"app.runtime.localization",
"app.runtime.log",
"app.runtime.loop",
"app.runtime.native_dependencies",
"app.runtime.observability",
"app.runtime.progress",
"app.runtime.rate",

View File

@@ -5623,9 +5623,9 @@
}
},
"provenance": {
"head": "aa69b3a5bd21148bf443cb69eb3095d4f557ea62",
"head": "aa40b961a2093474a77edd9eed88844ff07425ac",
"python_file_count": 284,
"source_sha256": "4c87d1625dffc8e84dbd8eb5c3fc75eb7457e64afaf43509af652461afa3e817"
"source_sha256": "c9621ee326c82a47f7c79725249b8adb3cf5176d604e868547a913e6cd77c5cb"
},
"schema_version": 3,
"scope": {

View File

@@ -708,9 +708,6 @@
"app/runtime/debounce.py": {
"I001": 1
},
"app/runtime/dependencies.py": {
"I001": 1
},
"app/runtime/event/errors.py": {
"I001": 1
},
@@ -982,10 +979,6 @@
"tests/test_db_session_lifecycle.py": {
"I001": 1
},
"tests/test_docker_bootstrap.py": {
"F841": 2,
"I001": 1
},
"tests/test_docker_entrypoint_permissions.py": {
"I001": 1
},

View File

@@ -186,7 +186,10 @@ def test_native_dependency_update_probe_has_narrow_automatic_triggers():
"app/adapters/external/market.py",
"app/adapters/system/host.py",
"app/adapters/system/package.py",
"app/runtime/dependencies.py",
"app/adapters/system/plugin/package.py",
"app/application/plugin/install.py",
"app/runtime/dependencies/native.py",
"app/runtime/dependencies/profile.py",
"scripts/probe_native_dependency_update.py",
".github/workflows/native-dependency-update.yml",
]
@@ -218,6 +221,7 @@ def test_dependency_compatibility_covers_windows_free_threaded_profile():
assert "MOVIEPILOT_SAFE_MODE" in str(workflow["jobs"]["install"]["steps"])
expected_paths = {
"app/__init__.py",
"app/runtime/dependencies/profile.py",
"scripts/verify_runtime_profile.py",
}
assert expected_paths <= set(workflow["on"]["pull_request"]["paths"])

View File

@@ -79,6 +79,8 @@ RETIRED_CANONICAL_FILES = (
"app/foundation/structures.py",
"app/foundation/zhconv.py",
"app/runtime/runtime.py",
"app/runtime/dependencies.py",
"app/runtime/native_dependencies.py",
"app/adapters/network/rss.py",
"app/adapters/network/sites.pyi",
"app/application/plugins.py",
@@ -298,6 +300,43 @@ def test_retired_canonical_filenames_do_not_return():
assert leftovers == []
def test_runtime_dependencies_use_same_named_single_word_package() -> None:
"""运行依赖能力必须使用同名包,且宿主直接依赖单一职责子模块。"""
package = APP_ROOT / "runtime" / "dependencies"
assert {path.name for path in package.glob("*.py")} == {
"__init__.py",
"native.py",
"profile.py",
}
init_tree = ast.parse(
(package / "__init__.py").read_text(encoding="utf-8"),
filename=str(package / "__init__.py"),
)
assert ast.get_docstring(init_tree)
assert len(init_tree.body) == 1
violations: list[str] = []
for path in APP_ROOT.rglob("*.py"):
relative = path.relative_to(APP_ROOT)
if relative.parts[0] == "plugins":
continue
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
for node in ast.walk(tree):
if (
isinstance(node, ast.ImportFrom)
and node.module == "app.runtime.dependencies"
):
violations.append(f"{relative}:{node.lineno}:from-package-root")
elif isinstance(node, ast.Import):
violations.extend(
f"{relative}:{node.lineno}:package-root"
for alias in node.names
if alias.name == "app.runtime.dependencies"
)
assert violations == []
def test_workflow_query_contract_returns_only_typed_snapshots():
"""工作流正式查询端口不得退化为 Any 或 ORM 返回值。"""
path = APP_ROOT / "application" / "workflow.py"

View File

@@ -9,7 +9,6 @@ from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
LAUNCHER = ROOT / "docker" / "launcher.sh"
UPDATER = ROOT / "docker" / "update.sh"
@@ -75,7 +74,9 @@ runtime-free-threaded = ["free-threaded"]
encoding="utf-8",
)
(project / "uv.lock").write_text("version = 1\n", encoding="utf-8")
runtime_selector = project / "app" / "runtime" / "dependencies.py"
runtime_selector = (
project / "app" / "runtime" / "dependencies" / "profile.py"
)
runtime_selector.parent.mkdir(parents=True)
runtime_selector.write_text("print('runtime-free-threaded')\n", encoding="utf-8")
venv_bin = tmp_path / "venv" / "bin"
@@ -123,6 +124,78 @@ runtime-free-threaded = ["free-threaded"]
assert "--no-default-groups --group runtime-free-threaded" in command
def test_update_sync_supports_legacy_runtime_selector_during_rollback(
tmp_path: Path,
) -> None:
"""更新回滚必须能读取改名前旧快照中的依赖 profile 选择器。"""
project = tmp_path / "legacy-project"
project.mkdir()
(project / "pyproject.toml").write_text(
"""
[project]
name = "moviepilot"
version = "0"
[dependency-groups]
runtime-standard = ["standard"]
runtime-free-threaded = ["free-threaded"]
""",
encoding="utf-8",
)
(project / "uv.lock").write_text("version = 1\n", encoding="utf-8")
legacy_selector = project / "app" / "runtime" / "dependencies.py"
legacy_selector.parent.mkdir(parents=True)
legacy_selector.write_text("print('runtime-standard')\n", encoding="utf-8")
venv_bin = tmp_path / "venv" / "bin"
venv_bin.mkdir(parents=True)
python_log = tmp_path / "python.log"
python_bin = venv_bin / "python3"
python_bin.write_text(
"#!/bin/bash\nprintf '%s\\n' \"$*\" > "
f"{shlex.quote(str(python_log))}\nprintf '%s\\n' runtime-standard\n",
encoding="utf-8",
)
python_bin.chmod(0o755)
uv_bin = tmp_path / "uv"
uv_log = tmp_path / "uv.log"
uv_bin.write_text(
f"#!/bin/bash\nprintf '%s\\n' \"$*\" > {shlex.quote(str(uv_log))}\n",
encoding="utf-8",
)
uv_bin.chmod(0o755)
script = textwrap.dedent(
f"""\
CONFIG_DIR="$1"
VENV_PATH="$2"
UV_BIN="$3"
source {UPDATER!s}
PACKAGE_ENV=()
UV_OPTIONS=()
sync_project_dependencies_for "$4"
"""
)
subprocess.run(
[
"bash",
"-c",
script,
"legacy-runtime-profile-test",
str(tmp_path / "config"),
str(tmp_path / "venv"),
str(uv_bin),
str(project),
],
check=True,
)
assert python_log.read_text(encoding="utf-8").strip() == str(legacy_selector)
assert "--no-default-groups --group runtime-standard" in uv_log.read_text(
encoding="utf-8"
)
def test_build_profiles_use_full_runtime_capability_probe() -> None:
"""两套依赖 profile 必须经过统一的完整运行能力验证。"""
dockerfile = (ROOT / "docker" / "Dockerfile").read_text(encoding="utf-8")
@@ -1222,7 +1295,6 @@ def test_pending_update_recovers_previous_payload_on_next_start(tmp_path: Path)
def test_update_transaction_keeps_marker_when_backup_cleanup_fails(tmp_path: Path) -> None:
config_dir = tmp_path / "config"
pending_file = config_dir / "temp" / "__update_pending__"
log_file = tmp_path / "cleanup.log"
script = textwrap.dedent(
f"""\
@@ -1368,7 +1440,6 @@ def test_staged_payload_swap_failure_restores_previous_generation(tmp_path: Path
(stage_app / "app" / "new.py").write_text("new", encoding="utf-8")
(stage_public / "index.html").write_text("new-front", encoding="utf-8")
config_dir = tmp_path / "config"
uv_log = tmp_path / "uv.log"
script = textwrap.dedent(
f"""\
CONFIG_DIR="$1"

View File

@@ -2,7 +2,7 @@
from pathlib import Path
from app.runtime import native_dependencies
import app.runtime.dependencies.native as native_dependencies
class _Distribution:

View File

@@ -1394,7 +1394,7 @@ demo = { index = "private" }
requirements_file = Path(temp_dir) / "requirements.txt"
requirements_file.write_text(f"{requirement}\n", encoding="utf-8")
with patch(
"app.runtime.dependencies.runtime_dependency_group",
"app.runtime.dependencies.profile.runtime_dependency_group",
return_value=runtime_group,
):
success, message = PluginHelper._PluginHelper__validate_runtime_dependency_conflicts(

View File

@@ -33,7 +33,7 @@ from app.application.plugin.transaction import (
PluginInstallationPhase,
PluginInstallationRecord,
)
from app.runtime.native_dependencies import NativeDependencyChange
from app.runtime.dependencies.native import NativeDependencyChange
from app.schemas.exception import (
DatabaseWorkerClosedError,
PersistenceUnavailableError,

View File

@@ -6,7 +6,7 @@ from unittest.mock import Mock
import pytest
from app.adapters.system.plugin.package import PluginPackageManager
from app.runtime.native_dependencies import LoadedNativeDependencySnapshot
from app.runtime.dependencies.native import LoadedNativeDependencySnapshot
def _manager(monkeypatch, tmp_path: Path) -> PluginPackageManager:

View File

@@ -4,9 +4,9 @@ from types import SimpleNamespace
import pytest
import app.runtime.dependencies.profile as dependencies
from app.doctor import dependencies as dependency_doctor
from app.foundation import environment
from app.runtime import dependencies
from scripts import verify_runtime_profile