chore(lint): strip wheel/sdist size fields in the uv.lock normalizer (#3579)
pypi.org's simple index serves a size for every file while proxy indexes may not, so each re-lock added or stripped 'size = N' across ~2,900 lines depending on which index resolved it. Make the sizeless form canonical (the hash is the integrity check): the fixer now drops size fields and --check flags them, so re-locks from either side converge on one form. Co-authored-by: Isaac Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Normalize the package index in ``uv.lock`` to the public PyPI URL.
|
||||
"""Normalize ``uv.lock`` to the public PyPI index and canonical file metadata.
|
||||
|
||||
Local ``uv`` runs resolve against whatever index is configured on the
|
||||
developer's machine (e.g. the Databricks PyPI proxy via ``UV_INDEX_URL``
|
||||
@@ -9,14 +9,20 @@ PyPI (``https://pypi.org/simple``) so the lock is reproducible for
|
||||
contributors who don't have the proxy — CI already pins
|
||||
``UV_INDEX_URL: https://pypi.org/simple`` for the same reason.
|
||||
|
||||
This is a pre-commit *fixer*: it rewrites the registry URL in place and
|
||||
The index also leaks through file metadata: pypi.org serves a size for
|
||||
every wheel/sdist while proxy indexes may not, so each re-lock adds or
|
||||
strips ``size = N`` across thousands of lines depending on which index
|
||||
resolved it. The canonical form omits ``size`` (the hash is the
|
||||
integrity check), so re-locks from either side stop ping-ponging.
|
||||
|
||||
This is a pre-commit *fixer*: it rewrites the lockfile in place and
|
||||
exits non-zero when it changed anything, so the commit aborts and the
|
||||
developer re-stages the normalized lockfile (mirroring
|
||||
``end-of-file-fixer`` and friends). Only ``registry`` sources are
|
||||
touched; ``git`` / ``path`` / ``editable`` sources are left alone.
|
||||
|
||||
Pass ``--check`` to validate without writing: it exits non-zero (and
|
||||
names the offending URLs) when a file is *not* already canonical, but
|
||||
names the offending entries) when a file is *not* already canonical, but
|
||||
leaves it untouched. CI runs this mode against the committed lockfile
|
||||
*before* any ``uv`` command — a plain ``uv run pre-commit`` can't catch a
|
||||
committed proxy URL, because ``uv`` re-syncs the working tree to CI's
|
||||
@@ -53,12 +59,18 @@ _DIRECT_URL_RE = re.compile(
|
||||
r'(url = ")(https?://(?!files\.pythonhosted\.org)[^"]+?/packages/)([^"]*")'
|
||||
)
|
||||
|
||||
# Matches the optional file-size field after a wheel/sdist hash, e.g.
|
||||
# hash = "sha256:...", size = 116543, upload-time = "..."
|
||||
# pypi.org's index serves sizes, proxy indexes may not; canonical form omits them.
|
||||
_SIZE_RE = re.compile(r'(hash = "[^"]+"), size = \d+')
|
||||
|
||||
def non_canonical_registries(text: str) -> list[str]:
|
||||
"""Return the registry URLs and direct-URL hosts in *text* that are not canonical.
|
||||
|
||||
def non_canonical_entries(text: str) -> list[str]:
|
||||
"""Return the non-canonical registry URLs, direct-URL hosts, and size fields in *text*.
|
||||
|
||||
:param text: Full contents of a ``uv.lock`` file.
|
||||
:returns: Each non-canonical URL, in order, with duplicates preserved.
|
||||
:returns: Each non-canonical URL, in order, with duplicates preserved,
|
||||
plus one summary entry when ``size`` fields are present.
|
||||
"""
|
||||
bad: list[str] = [
|
||||
m.group(1)
|
||||
@@ -66,19 +78,21 @@ def non_canonical_registries(text: str) -> list[str]:
|
||||
if m.group(1) != _CANONICAL_INDEX
|
||||
]
|
||||
bad += [m.group(2) for m in _DIRECT_URL_RE.finditer(text)]
|
||||
sizes = len(_SIZE_RE.findall(text))
|
||||
if sizes:
|
||||
bad.append(f"size field on {sizes} file entr{'y' if sizes == 1 else 'ies'}")
|
||||
return bad
|
||||
|
||||
|
||||
def normalize_text(text: str) -> str:
|
||||
"""Return *text* with every registry and direct wheel/sdist URL rewritten to canonical hosts.
|
||||
"""Return *text* with URLs rewritten to canonical hosts and ``size`` fields dropped.
|
||||
|
||||
:param text: Full contents of a ``uv.lock`` file.
|
||||
:returns: The normalized text.
|
||||
"""
|
||||
return _DIRECT_URL_RE.sub(
|
||||
rf"\g<1>{_CANONICAL_FILES_HOST}/packages/\g<3>",
|
||||
_REGISTRY_RE.sub(rf"\g<1>{_CANONICAL_INDEX}\g<2>", text),
|
||||
)
|
||||
text = _REGISTRY_RE.sub(rf"\g<1>{_CANONICAL_INDEX}\g<2>", text)
|
||||
text = _DIRECT_URL_RE.sub(rf"\g<1>{_CANONICAL_FILES_HOST}/packages/\g<3>", text)
|
||||
return _SIZE_RE.sub(r"\1", text)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
@@ -89,7 +103,7 @@ def main(argv: list[str]) -> int:
|
||||
:returns: In fix mode, ``1`` when a file was modified (so the commit
|
||||
aborts and the change is re-staged) else ``0``. In ``--check``
|
||||
mode, ``1`` when any file is not already canonical (printing the
|
||||
offending URLs) else ``0``; no file is written.
|
||||
offenders) else ``0``; no file is written.
|
||||
"""
|
||||
check = "--check" in argv
|
||||
files = [a for a in argv if a != "--check"]
|
||||
@@ -97,13 +111,13 @@ def main(argv: list[str]) -> int:
|
||||
if check:
|
||||
ok = True
|
||||
for name in files:
|
||||
offenders = non_canonical_registries(Path(name).read_text())
|
||||
offenders = non_canonical_entries(Path(name).read_text())
|
||||
if offenders:
|
||||
ok = False
|
||||
unique = sorted(set(offenders))
|
||||
print(
|
||||
f"{name}: {len(offenders)} non-canonical registry "
|
||||
f"source(s) (expected {_CANONICAL_INDEX}): {', '.join(unique)}"
|
||||
f"{name}: {len(offenders)} non-canonical entry(s) "
|
||||
f"(expected {_CANONICAL_INDEX}, no size fields): {', '.join(unique)}"
|
||||
)
|
||||
print(
|
||||
"Fix with: python scripts/normalize_uv_lock_registry.py "
|
||||
@@ -118,7 +132,7 @@ def main(argv: list[str]) -> int:
|
||||
normalized = normalize_text(original)
|
||||
if normalized != original:
|
||||
path.write_text(normalized)
|
||||
print(f"{name}: normalized package index to {_CANONICAL_INDEX}")
|
||||
print(f"{name}: normalized to canonical form ({_CANONICAL_INDEX}, no size fields)")
|
||||
changed = True
|
||||
return 1 if changed else 0
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
The pre-commit fixer rewrites every ``source = { registry = "<url>" }``
|
||||
in ``uv.lock`` to public PyPI so a developer's local index/proxy never
|
||||
leaks into the committed lockfile. These tests pin that contract:
|
||||
arbitrary registry URLs are normalized, non-registry sources are left
|
||||
alone, the fixer is idempotent, and ``main`` signals modifications via
|
||||
its exit code (1 = changed → commit aborts and re-stages; 0 = clean).
|
||||
leaks into the committed lockfile, and strips wheel/sdist ``size``
|
||||
fields (served by pypi.org but not by proxy indexes) so re-locks from
|
||||
either side agree on one canonical form. These tests pin that contract:
|
||||
arbitrary registry URLs are normalized, size fields are dropped,
|
||||
non-registry sources are left alone, the fixer is idempotent, and
|
||||
``main`` signals modifications via its exit code (1 = changed → commit
|
||||
aborts and re-stages; 0 = clean).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -126,7 +129,7 @@ def test_main_handles_multiple_files(tmp_path: Path) -> None:
|
||||
assert b.read_text() == canonical
|
||||
|
||||
|
||||
def test_non_canonical_registries_lists_offenders() -> None:
|
||||
def test_non_canonical_entries_lists_offenders() -> None:
|
||||
"""The check helper reports each non-canonical registry URL, in order."""
|
||||
proxy = "https://pypi-proxy.cloud.databricks.com/simple"
|
||||
text = (
|
||||
@@ -134,13 +137,13 @@ def test_non_canonical_registries_lists_offenders() -> None:
|
||||
f'source = {{ registry = "{_CANONICAL}" }}\n'
|
||||
f'source = {{ registry = "{proxy}" }}\n'
|
||||
)
|
||||
assert _MOD.non_canonical_registries(text) == [proxy, proxy]
|
||||
assert _MOD.non_canonical_entries(text) == [proxy, proxy]
|
||||
|
||||
|
||||
def test_non_canonical_registries_empty_when_canonical() -> None:
|
||||
def test_non_canonical_entries_empty_when_canonical() -> None:
|
||||
"""A fully-canonical lockfile reports no offenders."""
|
||||
text = f'source = {{ registry = "{_CANONICAL}" }}\n'
|
||||
assert _MOD.non_canonical_registries(text) == []
|
||||
assert _MOD.non_canonical_entries(text) == []
|
||||
|
||||
|
||||
def test_main_check_fails_without_writing(tmp_path: Path) -> None:
|
||||
@@ -166,3 +169,46 @@ def test_main_check_flag_position_independent(tmp_path: Path) -> None:
|
||||
lock = tmp_path / "uv.lock"
|
||||
lock.write_text(f'source = {{ registry = "{_CANONICAL}" }}\n')
|
||||
assert _MOD.main([str(lock), "--check"]) == 0
|
||||
|
||||
|
||||
# A registry file entry as uv writes it when the index serves sizes (pypi.org).
|
||||
_SIZED_ENTRY = (
|
||||
'sdist = { url = "https://files.pythonhosted.org/packages/ab/cd/pkg.tar.gz", '
|
||||
'hash = "sha256:aaaa", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" }\n'
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_text_strips_size_fields() -> None:
|
||||
"""``size`` metadata is dropped; url, hash, and upload-time survive."""
|
||||
result = _MOD.normalize_text(_SIZED_ENTRY)
|
||||
assert "size = " not in result
|
||||
assert 'hash = "sha256:aaaa", upload-time = "2026-01-28T10:17:05.322Z"' in result
|
||||
|
||||
|
||||
def test_normalize_text_strips_size_field_without_upload_time() -> None:
|
||||
"""A trailing ``size`` with no ``upload-time`` after it is also stripped."""
|
||||
url = "https://files.pythonhosted.org/packages/ab/cd/p.whl"
|
||||
text = f' {{ url = "{url}", hash = "sha256:bb", size = 7 }},\n'
|
||||
assert _MOD.normalize_text(text) == f' {{ url = "{url}", hash = "sha256:bb" }},\n'
|
||||
|
||||
|
||||
def test_non_canonical_entries_flags_size_fields() -> None:
|
||||
"""Size fields are reported as one summary offender so ``--check`` fails."""
|
||||
assert _MOD.non_canonical_entries(_SIZED_ENTRY * 2) == ["size field on 2 file entries"]
|
||||
|
||||
|
||||
def test_main_check_fails_on_size_fields(tmp_path: Path) -> None:
|
||||
"""``--check`` returns 1 for a lockfile carrying size metadata, without writing."""
|
||||
lock = tmp_path / "uv.lock"
|
||||
lock.write_text(_SIZED_ENTRY)
|
||||
assert _MOD.main(["--check", str(lock)]) == 1
|
||||
assert lock.read_text() == _SIZED_ENTRY
|
||||
|
||||
|
||||
def test_main_strips_size_fields_and_returns_one(tmp_path: Path) -> None:
|
||||
"""Fix mode strips sizes in place, returns 1, and is idempotent after."""
|
||||
lock = tmp_path / "uv.lock"
|
||||
lock.write_text(_SIZED_ENTRY)
|
||||
assert _MOD.main([str(lock)]) == 1
|
||||
assert "size = " not in lock.read_text()
|
||||
assert _MOD.main([str(lock)]) == 0
|
||||
|
||||
Reference in New Issue
Block a user