mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 03:18:14 +08:00
refactor(runtime): govern dependency module naming
This commit is contained in:
4
.github/workflows/dependency-compat.yml
vendored
4
.github/workflows/dependency-compat.yml
vendored
@@ -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/**'
|
||||
|
||||
10
.github/workflows/native-dependency-update.yml
vendored
10
.github/workflows/native-dependency-update.yml
vendored
@@ -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:
|
||||
|
||||
2
app/adapters/external/market.py
vendored
2
app/adapters/external/market.py
vendored
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
1
app/runtime/dependencies/__init__.py
Normal file
1
app/runtime/dependencies/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""运行依赖 profile 与原生载荷激活边界。"""
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 热点文件
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -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 只选择运行
|
||||
|
||||
@@ -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>[^`]+)`, "
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5623,9 +5623,9 @@
|
||||
}
|
||||
},
|
||||
"provenance": {
|
||||
"head": "aa69b3a5bd21148bf443cb69eb3095d4f557ea62",
|
||||
"head": "aa40b961a2093474a77edd9eed88844ff07425ac",
|
||||
"python_file_count": 284,
|
||||
"source_sha256": "4c87d1625dffc8e84dbd8eb5c3fc75eb7457e64afaf43509af652461afa3e817"
|
||||
"source_sha256": "c9621ee326c82a47f7c79725249b8adb3cf5176d604e868547a913e6cd77c5cb"
|
||||
},
|
||||
"schema_version": 3,
|
||||
"scope": {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from app.runtime import native_dependencies
|
||||
import app.runtime.dependencies.native as native_dependencies
|
||||
|
||||
|
||||
class _Distribution:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user