Files
MoviePilot/app/doctor/dependencies.py
T
InfinityPacer 326b5cf3ad feat: 新增 Python 3.14t 自由线程镜像 (#6434)
* fix(resource): select free-threaded extension ABI

* chore(deps): require moviepilot-rust 0.2.9

* perf: add free-threaded runtime comparison

* feat: add free-threaded runtime profile

* chore(deps): require moviepilot-rust 0.3.0

* test: isolate system endpoint import graph

* fix(docker): preserve runtime profile during recovery

* feat(runtime): expose active GIL state

* feat(plugin): log GIL fallback attribution

* test: refresh runtime observability dependency baseline

* fix(plugin): validate the active uv runtime profile

* test(runtime): expand free-threaded benchmark evidence

* docs(runtime): define v3t governance gates

* test(runtime): separate rust benchmark modes

* docs: sync free-threaded architecture baseline

* perf: add PostgreSQL driver comparison

* docs(runtime): record final free-threaded evidence

* fix(runtime): converge dual-profile dependency verification

* fix(runtime): scope Python 3.14 warning filter

* fix(runtime): match actual oss2 syntax warning

* docs(runtime): refresh free-threaded benchmark evidence

* test(architecture): refresh runtime dependency baseline

* ci: skip unused Trivy Java database

* build: exclude local verification artifacts

* docs(runtime): refresh PostgreSQL driver benchmarks

* docs(runtime): record plugin restore acceptance

* docs(runtime): record amd64 candidate acceptance

* ci: pin beta image publisher action

* test(architecture): merge runtime dependency baseline

* feat(runtime): expose Python GIL status

* docs(runtime): document Python runtime status fields
2026-08-24 17:48:14 +08:00

93 lines
2.9 KiB
Python

"""主程序运行依赖的轻量可用性探针。"""
import argparse
from importlib import import_module
import sys
import sysconfig
import warnings
CORE_MODULES = (
"alembic",
"cloakbrowser",
"fastapi",
"pydantic",
"pydantic_core",
"pydantic_settings",
"sqlalchemy",
"starlette",
"uvicorn",
)
NATIVE_MODULES = (
("asyncpg", "asyncpg"),
("bcrypt", "bcrypt._bcrypt"),
("brotli", "brotli"),
("crcmod", "crcmod.crcmod"),
("cryptography", "cryptography.hazmat.bindings._rust"),
("greenlet", "greenlet._greenlet"),
("lxml", "lxml.etree"),
("orjson", "orjson"),
("oss2", "oss2"),
("pillow", "PIL._imaging"),
("pillow-avif-plugin", "pillow_avif"),
("pydantic-core", "pydantic_core._pydantic_core"),
("zstandard", "zstandard"),
)
def _verify_text_capabilities(*, free_threaded: bool) -> None:
"""验证标准与 free-threaded profile 共同依赖的文本能力。"""
moviepilot_rust = import_module("moviepilot_rust")
if not moviepilot_rust.is_available() or not moviepilot_rust.jieba_cut("中文分词"):
raise RuntimeError("中文分词运行依赖不可用")
if free_threaded:
converted = moviepilot_rust.zhconv_fast("后台", "zh-hant")
else:
converted = import_module("zhconv_rs").zhconv("后台", "zh-hant")
if not converted:
raise RuntimeError("中文转换运行依赖不可用")
def _verify_native_profile(*, free_threaded: bool) -> None:
"""验证 ABI 敏感依赖提供预期的原生能力。"""
warnings.filterwarnings(
"ignore",
message=r'"\\&" is an invalid escape sequence\..*',
category=SyntaxWarning,
)
imported = {}
profile_modules = (
(("psycopg", "psycopg"),)
if free_threaded
else (("psycopg2", "psycopg2"), ("zhconv-rs", "zhconv_rs"))
)
for name, module_name in (*NATIVE_MODULES, *profile_modules):
imported[name] = import_module(module_name)
if free_threaded and sys._is_gil_enabled():
raise RuntimeError(f"原生依赖 {name} 启用了 GIL")
if not imported["crcmod"]._usingExtension:
raise RuntimeError("crcmod 原生实现不可用")
if free_threaded and imported["psycopg"].pq.__impl__ != "c":
raise RuntimeError("psycopg C 实现不可用")
def main(*, full: bool = False) -> None:
"""验证 Web 栈和启动关键能力可导入、可执行。"""
free_threaded = sysconfig.get_config_var("Py_GIL_DISABLED") == 1
for module_name in CORE_MODULES:
import_module(module_name)
_verify_text_capabilities(free_threaded=free_threaded)
if full:
_verify_native_profile(free_threaded=free_threaded)
if free_threaded and sys._is_gil_enabled():
raise RuntimeError("核心运行依赖启用了 GIL")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--full", action="store_true")
main(full=parser.parse_args().full)