test(deps): guard pyproject<->lazy_deps pin consistency

Adds two checks to tests/test_packaging_metadata.py:

1. No package is exact-pinned to two different versions across
   pyproject.toml's [project.dependencies] / extras.
2. Every package pinned in BOTH the pyproject extras and the LAZY_DEPS
   allowlist in tools/lazy_deps.py uses the same version.

This is the regression guard for the drift the rest of this PR fixes: the
two pin sources are hand-maintained mirrors (lazy_deps even documents
"update both this map AND the corresponding extra"), and they have silently
diverged on aiohttp and anthropic. Run against the pre-fix tree, check (2)
fails on `anthropic: pyproject=['0.86.0'] lazy_deps=['0.87.0']`.

The lazy_deps side is parsed via AST (not imported) so the test stays free
of tools/lazy_deps.py runtime imports; only exact `==` pins are compared.
This commit is contained in:
Wing Huang 2026-06-09 03:06:12 +08:00 committed by Teknium
parent db57cbbaf6
commit 6f956d7405

View file

@ -1,6 +1,7 @@
from pathlib import Path
import ast
import re
import tomllib
from pathlib import Path
import pytest
@ -265,3 +266,113 @@ def test_locale_catalogs_ship_in_both_wheel_and_sdist():
on_disk = list((REPO_ROOT / "locales").glob("*.yaml"))
assert on_disk, "expected locales/*.yaml catalogs on disk"
# ---------------------------------------------------------------------------
# Dependency-pin consistency: pyproject extras <-> tools/lazy_deps.py
#
# The same package is exact-pinned in two hand-maintained places: the
# [project.optional-dependencies] extras in pyproject.toml and the LAZY_DEPS
# allowlist in tools/lazy_deps.py (the lazy-install path deliberately mirrors
# the extras — see the comments on LAZY_DEPS: "match the corresponding extra
# in pyproject.toml ... update both this map AND the corresponding extra").
#
# They have silently drifted more than once: the aiohttp Slack pin (3.13.3 in
# the extras vs 3.13.4 in lazy_deps) and the anthropic pin (0.86.0 vs 0.87.0).
# The version a user ends up with then depends on whether the backend was
# installed eagerly (extra) or lazily (lazy_deps) — and for a CVE bump applied
# to only one side, that divergence is a latent security regression. These two
# tests assert the documented contract: the two sources agree, in lockstep.
# ---------------------------------------------------------------------------
# Matches "name==version" and "name[extra]==version", ignoring any trailing
# environment marker / comment. Only exact pins are collected; ranged specs
# (">=", "<") can't be compared for equality and are skipped.
_PIN_RE = re.compile(
r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*==\s*([^\s;,#]+)"
)
def _canonical(name: str) -> str:
# PEP 503 normalization so e.g. discord.py / discord-py compare equal.
return re.sub(r"[-_.]+", "-", name).lower()
def _pins_from_specs(specs):
"""Map canonical package name -> set of exact-pinned versions seen."""
pins: dict[str, set[str]] = {}
for spec in specs:
m = _PIN_RE.match(spec)
if not m:
continue
pins.setdefault(_canonical(m.group(1)), set()).add(m.group(2))
return pins
def _pyproject_pinned_specs():
data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
specs = list(data["project"].get("dependencies", []))
for extra in data["project"].get("optional-dependencies", {}).values():
specs.extend(extra)
return specs
def _lazy_deps_pinned_specs():
"""Extract every string literal inside the LAZY_DEPS dict via AST.
Parsing rather than importing keeps this test free of
tools/lazy_deps.py's runtime imports and side effects.
"""
src = (REPO_ROOT / "tools" / "lazy_deps.py").read_text(encoding="utf-8")
tree = ast.parse(src)
specs: list[str] = []
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
targets = node.targets
elif isinstance(node, ast.AnnAssign):
targets = [node.target]
else:
continue
if not any(isinstance(t, ast.Name) and t.id == "LAZY_DEPS" for t in targets):
continue
for sub in ast.walk(node.value):
if isinstance(sub, ast.Constant) and isinstance(sub.value, str):
specs.append(sub.value)
assert specs, "could not extract specs from LAZY_DEPS — the AST parser drifted"
return specs
def test_pyproject_pins_are_internally_consistent():
"""No package may be exact-pinned to two different versions in pyproject.
A package legitimately appearing in several extras (e.g. aiohttp in
messaging/slack/homeassistant/sms) must use the SAME version everywhere.
"""
pins = _pins_from_specs(_pyproject_pinned_specs())
conflicts = {name: sorted(v) for name, v in pins.items() if len(v) > 1}
assert not conflicts, (
"pyproject.toml exact-pins the same package to different versions "
"across [project.dependencies] / extras: " + str(conflicts)
)
def test_pyproject_and_lazy_deps_pins_agree():
"""Every package pinned in BOTH places must use the same version.
Regression guard for the aiohttp / anthropic extras-vs-lazy drift:
tools/lazy_deps.py mirrors the pyproject extras, so a CVE bump applied to
one and not the other leaves users on a vulnerable version depending on
the install path. Bump both in lockstep.
"""
py = _pins_from_specs(_pyproject_pinned_specs())
lazy = _pins_from_specs(_lazy_deps_pinned_specs())
mismatches = [
f"{name}: pyproject={sorted(py[name])} lazy_deps={sorted(lazy[name])}"
for name in sorted(set(py) & set(lazy))
if py[name] != lazy[name]
]
assert not mismatches, (
"pyproject.toml extras and tools/lazy_deps.py disagree on the pinned "
"version of the same package — bump both in lockstep:\n "
+ "\n ".join(mismatches)
)