fix(plugin): filter declared uv dependency exclusions (#6437)

This commit is contained in:
InfinityPacer
2026-08-24 20:06:57 +08:00
committed by GitHub
parent 7d3b5e129e
commit 34f59bc9f4
5 changed files with 128 additions and 13 deletions
+36 -6
View File
@@ -24,8 +24,9 @@ import aioshutil
import httpx2
from anyio import Path as AsyncPath
from packaging.markers import default_environment
from packaging.requirements import Requirement
from packaging.requirements import InvalidRequirement, Requirement
from packaging.specifiers import SpecifierSet, InvalidSpecifier
from packaging.utils import canonicalize_name
from packaging.version import Version, InvalidVersion
from importlib.metadata import distributions
from requests import Response
@@ -34,6 +35,7 @@ from app.runtime.cache import cached, is_fresh
from app.runtime.dependencies import (
iter_runtime_profile_requirement_strings,
iter_runtime_requirement_strings,
runtime_excluded_dependency_pairs,
)
from app.runtime.settings import RuntimeSettingsCompat
from app.adapters.system.package import (
@@ -1733,15 +1735,36 @@ class PluginHelper(metaclass=WeakSingleton):
@staticmethod
def __runtime_health_error_lines(check_name: str, message: str) -> set[str]:
"""提取稳定诊断项,忽略执行器附加的命令摘要"""
"""提取未被项目依赖策略排除的稳定诊断项。"""
lines = {line.strip() for line in message.splitlines() if line.strip()}
if check_name != "uv check":
return lines
package_errors = set(re.findall(
r"The package `[^`]+` requires `[^`]+`, but [^\r\n;]+",
matches = list(re.finditer(
r"The package `(?P<package>[^`]+)` requires `(?P<requirement>[^`]+)`, "
r"but [^\r\n;]+",
message,
))
return package_errors or lines
if not matches:
return lines
excluded_pairs = runtime_excluded_dependency_pairs(
Path(settings.ROOT_PATH) / "pyproject.toml"
)
package_errors = set()
for match in matches:
try:
dependency_name = Requirement(match.group("requirement")).name
except InvalidRequirement:
package_errors.add(match.group(0))
continue
pair = (
canonicalize_name(match.group("package")),
canonicalize_name(dependency_name),
)
if pair not in excluded_pairs:
package_errors.add(match.group(0))
return package_errors
@staticmethod
def __runtime_health_regression_message(
@@ -1755,7 +1778,14 @@ class PluginHelper(metaclass=WeakSingleton):
for check_name, (success, message) in current_health.items():
baseline_success, baseline_message = baseline_health.get(check_name, (True, ""))
if baseline_success and not success:
regressions.append(f"{check_name}失败:{message}")
current_lines = PluginHelper.__runtime_health_error_lines(
check_name,
message,
)
if current_lines:
regressions.append(
f"{check_name}失败:{' | '.join(sorted(current_lines))}"
)
elif not baseline_success and not success:
baseline_lines = PluginHelper.__runtime_health_error_lines(
check_name,
+31
View File
@@ -6,6 +6,9 @@ import tomllib
from collections.abc import Iterable
from pathlib import Path
from packaging.requirements import InvalidRequirement, Requirement
from packaging.utils import canonicalize_name
from app.foundation.environment import is_free_threaded_runtime
@@ -52,5 +55,33 @@ def iter_runtime_profile_requirement_strings(project_file: Path) -> Iterable[str
yield requirement
def runtime_excluded_dependency_pairs(project_file: Path) -> set[tuple[str, str]]:
"""返回项目明确排除的“依赖包 -> 传递依赖”规范化名称对。"""
with project_file.open("rb") as file:
document = tomllib.load(file)
exclusions = (
((document.get("tool") or {}).get("uv") or {}).get("exclude-dependencies")
or ()
)
pairs: set[tuple[str, str]] = set()
for exclusion in exclusions:
if not isinstance(exclusion, dict):
continue
package = exclusion.get("package") or {}
package_name = package.get("name") if isinstance(package, dict) else None
if not isinstance(package_name, str) or not package_name.strip():
continue
for dependency in exclusion.get("dependencies") or ():
if not isinstance(dependency, str):
continue
try:
dependency_name = Requirement(dependency).name
except InvalidRequirement:
continue
pairs.add((canonicalize_name(package_name), canonicalize_name(dependency_name)))
return pairs
if __name__ == "__main__":
print(runtime_dependency_group())
+3 -2
View File
@@ -81,8 +81,9 @@ profile,依赖名称、版本和 source 语义全部由 `pyproject.toml` 与 `
3. 恢复完成后重新执行依赖诊断与核心能力探针;
4. 宿主即使恢复成功,本次插件安装仍返回失败,不能把被回滚的安装报告为成功。
依赖诊断使用 `uv pip check`并按安装前后的稳定错误集合识别新增问题;这样既不会把
`oss2` 对旧 `crcmod` 的陈旧元数据误归因于本次安装,也不会让既有告警遮蔽其他新增错误。核心能力
依赖诊断使用 `uv pip check`项目在 `tool.uv.exclude-dependencies` 中明确排除的传递依赖不进入健康
异常集合,其余诊断按安装前后的稳定错误集合识别新增问题;这样既不会反复报告 `oss2` 对旧 `crcmod`
的陈旧元数据,也不会让既有告警遮蔽其他新增错误。核心能力
探针统一由 `app.doctor.dependencies` 执行。普通启动使用轻量模式验证 Web 栈、中文分词与转换;
镜像构建及插件安装前后使用完整模式,继续验证 ABI 敏感原生扩展、CRC C 实现、PostgreSQL C 实现及
导入后的 GIL 状态。插件允许的非核心依赖升级不要求与宿主锁文件逐版本相同,因此插件健康检查不得
+40 -5
View File
@@ -1547,6 +1547,7 @@ demo = { index = "private" }
side_effect=health_snapshots,
), \
patch.object(PluginHelper, "_PluginHelper__repair_main_runtime_dependencies") as repair_mock, \
patch("app.adapters.external.market.logger.warning") as warning_mock, \
patch(
"app.adapters.external.market.SystemUtils.execute_with_subprocess",
return_value=(True, "installed"),
@@ -1556,6 +1557,10 @@ demo = { index = "private" }
assert success
assert message == "installed"
repair_mock.assert_not_called()
assert not any(
"安装前运行环境已存在异常" in str(call.args[0])
for call in warning_mock.call_args_list
)
def test_preexisting_healthcheck_failure_does_not_hide_new_core_failure(self):
"""
@@ -1615,14 +1620,34 @@ demo = { index = "private" }
existing_error = "The package `oss2` requires `crcmod>=1.7`, but it's not installed"
added_error = "The package `demo` requires `missing>=1`, but it's not installed"
message = PluginHelper._PluginHelper__runtime_health_regression_message(
{"uv check": (False, existing_error)},
{"uv check": (False, f"{existing_error}\n{added_error}")},
)
with patch(
"app.adapters.external.market.runtime_excluded_dependency_pairs",
return_value={("oss2", "crcmod")},
):
message = PluginHelper._PluginHelper__runtime_health_regression_message(
{"uv check": (False, existing_error)},
{"uv check": (False, f"{existing_error}\n{added_error}")},
)
assert added_error in message
assert existing_error not in message
def test_expected_uv_diagnostic_does_not_create_baseline_warning(self):
"""项目明确排除的传递依赖不得形成插件安装前告警。"""
from app.adapters.external.market import PluginHelper
expected_error = "The package `oss2` requires `crcmod>=1.7`, but it's not installed"
with patch(
"app.adapters.external.market.runtime_excluded_dependency_pairs",
return_value={("oss2", "crcmod")},
):
message = PluginHelper._PluginHelper__runtime_health_regression_message(
{},
{"uv check": (False, expected_error)},
)
assert message == ""
def test_uv_diagnostic_parser_handles_executor_prefix(self):
"""执行器把首条错误拼在命令摘要后时仍应识别完整诊断项。"""
from app.adapters.external.market import PluginHelper
@@ -1801,8 +1826,9 @@ demo = { index = "private" }
find_links_dirs,
)
expected_error = "The package `oss2` requires `crcmod>=1.7`, but it's not installed"
health = {
"uv check": (True, "ok"),
"uv check": (False, expected_error),
"核心依赖导入检查": (True, "ok"),
}
strategy = Mock(
@@ -1834,6 +1860,11 @@ demo = { index = "private" }
), patch.object(
PluginHelper,
"_PluginHelper__refresh_import_system",
), patch(
"app.adapters.external.market.logger.warning",
) as warning_mock, patch(
"app.adapters.external.market.runtime_excluded_dependency_pairs",
return_value={("oss2", "crcmod")},
), patch(
"app.adapters.external.market.SystemUtils.execute_with_subprocess_async",
new=AsyncMock(return_value=(True, "ok")),
@@ -1846,6 +1877,10 @@ demo = { index = "private" }
assert execute_mock.await_args.kwargs["timeout"] == (
PluginHelper.PLUGIN_DEPENDENCY_INSTALL_TIMEOUT
)
assert not any(
"安装前运行环境已存在异常" in str(call.args[0])
for call in warning_mock.call_args_list
)
def test_async_package_install_cancellation_closes_full_lifecycle(self, tmp_path):
"""取消真实安装进程后必须回收进程树、临时约束和安装锁。"""
+18
View File
@@ -79,6 +79,24 @@ def test_runtime_profiles_share_gil_safe_crcmod_distribution():
} in document["tool"]["uv"]["exclude-dependencies"]
def test_runtime_excluded_dependency_pairs_reads_uv_policy(tmp_path: Path):
"""运行时诊断应复用 uv 排除配置,不维护第二份包名特判。"""
project_file = tmp_path / "pyproject.toml"
project_file.write_text(
"""
[tool.uv]
exclude-dependencies = [
{ package = { name = "Demo_Package" }, dependencies = ["Legacy-Dep>=1"] },
]
""",
encoding="utf-8",
)
assert dependencies.runtime_excluded_dependency_pairs(project_file) == {
("demo-package", "legacy-dep")
}
def test_full_dependency_probe_rejects_psycopg_python_fallback(monkeypatch):
"""V3t 构建不得把 psycopg 纯 Python 实现误认为可发布能力。"""
modules = {