DEV Community

Dakota Huang
Dakota Huang

Posted on

Refuse Versions the Index Never Served

You are three comments into a pull request that looks harmless. A helper grew a timeout. Someone added a new pinned require to pyproject.toml, and the rest of the diff is a short retry loop.

The retry code may even be fine. The version is the landmine. If your index never served that release, you are about to lock a ghost and teach every later installer to chase it.

This is not a style nit. Installers trust the lock. Humans trust the diff. Those two oracles disagree more often once a coding assistant starts proposing dependency lines.

Diffs do not speak for an index

A unified diff only proves that a string appeared in a file. It does not prove that a wheel, an sdist, or a yanked flag exists behind that string. You cannot infer PEP 440 identity from a green CI cache either, because caches happily reuse a previous resolve.

Think of the index as a notary. The notary keeps a list of filenames it has actually offered. Your job in review is to ask that notary a boring question: did you ever serve this name and version to anyone?

If the answer is no, the new require is a claim. Treat it like an unsigned commit message. Polite, confident, and worthless as evidence.

What a witness file is allowed to claim

A witness is a small JSON document committed next to the lock change. It records only what the index returned at resolve time. It is not a supply-chain attestation, and it is not SLSA. It is a snapshot of served versions plus a yes or no on the candidate.

Keep the claims tiny on purpose. Package name, normalized PEP 503 path, requested version, whether that version appeared in the simple index, and the filename that matched. If you cannot fill those fields from the index HTML or JSON, you do not have a witness. You have a story.

Do not let the witness claim extras. A simple index page does not list extras. Extras live in distribution metadata, which you have not downloaded yet. A model that writes hourglass-demo[otel] is asserting a feature extra you have not seen.

Keep a teaching index in the repo

Live public indexes are moving targets. Yanked files appear. Filenames change. A check that hits the network during a test is not a test. You want a local simple index that you control, checked in beside the witness script.

The layout below is a teaching fixture only. It is not a mirror of any real package, and the versions are invented so you never confuse them with a current public release.

tests/fake_index/
  hourglass-demo/
    index.html
  paperclip-synth/
    index.html
Enter fullscreen mode Exit fullscreen mode
<!-- tests/fake_index/hourglass-demo/index.html -->
<!DOCTYPE html>
<html>
  <body>
    <a href="./hourglass_demo-1.2.0-py3-none-any.whl">hourglass_demo-1.2.0-py3-none-any.whl</a>
    <a href="./hourglass_demo-1.2.1-py3-none-any.whl">hourglass_demo-1.2.1-py3-none-any.whl</a>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
<!-- tests/fake_index/paperclip-synth/index.html -->
<!DOCTYPE html>
<html>
  <body>
    <a href="./paperclip_synth-0.4.0-py3-none-any.whl#yanked=broken%20wheel">paperclip_synth-0.4.0-py3-none-any.whl</a>
    <a href="./paperclip_synth-0.4.1-py3-none-any.whl">paperclip_synth-0.4.1-py3-none-any.whl</a>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

PEP 503 normalizes names by replacing runs of [-_.] with a single - and lowercasing. Hourglass.Demo and hourglass-demo share a page. Your witness must normalize before it looks up a directory, or a second spelling can sneak past review.

A resolver check you can run offline

The script below is a teaching fixture. It is not a replacement for pip-tools, uv lock, or a corporate proxy. It answers one question: does this require line match a filename the local simple index already offered?

# tools/index_witness.py
from __future__ import annotations

import argparse
import json
import re
import sys
from html.parser import HTMLParser
from pathlib import Path

WHEEL_RE = re.compile(
    r"(?P<dist>.+)-(?P<version>\d+(?:\.\d+)*)-py3-none-any\.whl",
    re.IGNORECASE,
)


class HrefCollector(HTMLParser):
    def __init__(self) -> None:
        super().__init__()
        self.hrefs: list[str] = []

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        if tag != "a":
            return
        for key, value in attrs:
            if key == "href" and value:
                self.hrefs.append(value)


def pep503_name(name: str) -> str:
    return re.sub(r"[-_.]+", "-", name).lower()


def versions_from_index(page: Path) -> dict[str, dict[str, str | None]]:
    parser = HrefCollector()
    parser.feed(page.read_text(encoding="utf-8"))
    found: dict[str, dict[str, str | None]] = {}
    for href in parser.hrefs:
        filename = href.split("#", 1)[0].rstrip("/").split("/")[-1]
        match = WHEEL_RE.search(filename)
        if not match:
            continue
        yanked = None
        if "#yanked=" in href:
            yanked = href.split("#yanked=", 1)[1].split("&", 1)[0]
        found[match.group("version")] = {
            "filename": filename,
            "yanked": yanked,
        }
    return found


def parse_require(line: str) -> tuple[str, str] | None:
    text = line.split("#", 1)[0].strip()
    if not text or text.startswith(("-", ".")):
        return None
    if "==" not in text:
        return None
    name, version = text.split("==", 1)
    name = name.split("[", 1)[0].strip()
    version = version.strip()
    if not name or not version:
        return None
    return name, version


def line_declares_extra(line: str) -> bool:
    head = line.split("==", 1)[0]
    return "[" in head and "]" in head


def witness_one(index_root: Path, name: str, version: str) -> dict:
    page = index_root / pep503_name(name) / "index.html"
    if not page.exists():
        return {
            "name": name,
            "version": version,
            "served": False,
            "reason": "no_simple_page",
        }
    served = versions_from_index(page)
    hit = served.get(version)
    if hit is None:
        return {
            "name": name,
            "normalized": pep503_name(name),
            "version": version,
            "served": False,
            "reason": "version_not_listed",
            "served_versions": sorted(served),
        }
    return {
        "name": name,
        "normalized": pep503_name(name),
        "version": version,
        "served": True,
        "filename": hit["filename"],
        "yanked": hit["yanked"],
        "served_versions": sorted(served),
    }


def main(argv: list[str]) -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--index-root", type=Path, required=True)
    parser.add_argument("--requires", type=Path, required=True)
    parser.add_argument("--out", type=Path, required=True)
    args = parser.parse_args(argv)

    rows = []
    ok = True
    for raw in args.requires.read_text(encoding="utf-8").splitlines():
        parsed = parse_require(raw)
        if parsed is None:
            continue
        row = witness_one(args.index_root, parsed[0], parsed[1])
        row["extra_declared"] = line_declares_extra(raw)
        rows.append(row)
        if not row["served"] or row.get("yanked") or row["extra_declared"]:
            ok = False

    args.out.write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8")
    if not ok:
        print("index witness failed; refuse the lock line", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
Enter fullscreen mode Exit fullscreen mode

Feed it a fragment that looks like a suggestion, not like a lockfile you already trust.

# /tmp/suggested.in
hourglass-demo==1.2.1
paperclip-synth==0.4.0
never-served-pkg==9.9.9
Enter fullscreen mode Exit fullscreen mode
python tools/index_witness.py \
  --index-root tests/fake_index \
  --requires /tmp/suggested.in \
  --out /tmp/witness.json
echo $?
Enter fullscreen mode Exit fullscreen mode

The first line should pass against the teaching index. The second is present on the page and yanked in the fragment above; this run refuses it because a yank is not an ordinary serve. The third has no page at all. That last result is the one you refuse without negotiation.

Notice the gap that the JSON is built to keep visible. Served is not the same as installable. Yanked files can still appear as links. Missing names do not appear at all. If you collapse those into one boolean, review will bargain with the wrong failure.

A tiny test that does not touch a network

The tests below are teaching fixtures. They are not a measured benchmark of any installer, and they do not claim anything about public package history. Run them against the fake index in git.

# tests/test_index_witness.py
from pathlib import Path

from tools.index_witness import witness_one

ROOT = Path("tests/fake_index")


def test_served_version_is_true():
    row = witness_one(ROOT, "hourglass-demo", "1.2.1")
    assert row["served"] is True
    assert row["filename"] == "hourglass_demo-1.2.1-py3-none-any.whl"
    assert row.get("yanked") is None


def test_ghost_version_is_false():
    row = witness_one(ROOT, "hourglass-demo", "9.9.9")
    assert row["served"] is False
    assert row["reason"] == "version_not_listed"


def test_unknown_project_has_no_page():
    row = witness_one(ROOT, "never-served-pkg", "1.0.0")
    assert row["served"] is False
    assert row["reason"] == "no_simple_page"


def test_yanked_file_is_still_listed():
    row = witness_one(ROOT, "paperclip-synth", "0.4.0")
    assert row["served"] is True
    assert row["yanked"] == "broken%20wheel"
Enter fullscreen mode Exit fullscreen mode
python -m pytest tests/test_index_witness.py -q
Enter fullscreen mode Exit fullscreen mode

A green suite here only means the teaching notary still tells the same story. It does not mean a future public index will. If you later point the same functions at a snapshot of a real simple page, commit that snapshot in the same change as the require.

Markers and extras are a second grammar

Models love environment markers. They will write hourglass-demo==1.2.1; python_version < "3.9" even when your floor is already 3.11. They will invent extras that read well in English and mean nothing in METADATA.

Your witness script ignores markers on purpose. A marker is a predicate over the installing machine, not a fact about the index. If you need marker coverage, generate a matrix of environments and run a real resolver in each one. Do not ask an HTML page to evaluate sys_platform == "win32".

A cheap local check still helps. Strip extras before lookup, then fail the review if the original line contained an extra and you have no METADATA file in the same commit. That rule is harsh. It is also how you stop [otel, grpc, magic] from landing because it looked serious.

You are not proving the extra works. You are proving someone saw it in a file the index could have served. Until that file exists in the review, the extra is literature.

Yanked is not missing

Missing means the notary has no record. Yanked means the notary served it and later hung a warning on the link. Installers may still fetch a yanked wheel if a pin points at it. That is why a lock that freezes paperclip-synth==0.4.0 can keep working in CI for months after the yank.

Fail those two cases with different reasons so the reviewer can choose. Sometimes you want the yank because it is the last wheel that still imports on an old branch. Sometimes you do not. Record the yank reason in the witness JSON. Humans read reasons. Scripts should not treat a reason string as a license to ignore the flag.

Decision table

Observation Witness field Merge?
Version listed, not yanked served=true, no yank Yes, if a later lock step records hashes
Version absent from the page reason=version_not_listed No
No simple page for the name reason=no_simple_page No
Version listed and yanked served=true plus yank reason No unless a human names the exception
Require uses >= or no pin parser returns None No; ask for == first
Extra declared, no METADATA extra_declared=true No
Marker-only change ignored by this script Use a real resolver matrix
Direct URL or VCS require not parsed Out of scope; refuse this workflow

If two rows in the right column say no, stop. Do not bargain with the second failure because the first one almost passed. Ghost names and yanked pins are different bugs. Mixing them in one "fix the file" commit hides which one you meant to accept.

A free model can draft rows; it cannot sign them

Drafting marker matrices by hand is slow. You will forget platform_machine, then discover it on the one laptop that still uses x86_64. A coding model is useful for proposing extra require lines and extra environments to feed the witness.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option. That pair is enough to generate candidate rows when you do not want the drafting step on your laptop. The witness still runs where your index lives. Accept no suggested version that the witness did not mark served.

Keep secrets out of those prompts. Index URLs with embedded tokens do not belong in a chat. Paste the teaching fixture, not your private simple-index credentials.

Limits of a witness

This workflow does not prove a wheel is safe. It does not verify hashes, signatures, or license text. It will not notice a replacement of a filename that keeps the same version string. It will not notice a yanked file that your corporate mirror stripped from HTML while still holding the blob.

The wheel regex is intentionally naive. Real versions include epochs, local versions, and pre-releases. 1.2.0rc1 will not match. That is a limitation, not a clever filter. Extend the parser with a real PEP 440 library before you point this at production names.

Simple indexes also do not tell you which Python tag a wheel needs beyond what the filename encodes. py3-none-any is a teaching convenience. Many projects ship manylinux tags. If you need that, parse wheel filenames with a dedicated library and still refuse anything your index page did not link.

Do not run the teaching script against the public internet and call it policy. Public pages change while your PR is open. If you need a real gate, snapshot the simple pages you consulted into the same commit as the lock, then verify the witness against that snapshot. PEP 691 JSON simple API is a cleaner parse than HTML if your index offers it. This fixture does not implement that API.

Skip this if you already have a real resolver gate

Skip it when Pants, Bazel, or a locked uv pipeline already fails closed on unknown versions. You would only duplicate a better check.

Skip it for VCS URLs, local path extras, and direct file requires. A simple index has nothing to say about a git commit.

Skip it when the change is an intentional yank exception on a maintenance branch. Write that exception in the review notes. Do not weaken the script so the exception looks normal.

Skip it if nobody will read witness.json. An unread snapshot is a junk drawer. The next invented version will hide in that drawer with a green check.

A new require line is a claim about an index, not a description of your code. Ask the index first, in a form you can keep in git. Refuse the lock when the notary has no record.

Commit the witness beside the require. A clean diff with a missing witness is not done work. The notary already answered. Your job is to listen before the lock file does.

Top comments (0)