From 8088fb8d59b7bd5e68e26db2dd1e35c1121d7a04 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 07:11:12 -0400 Subject: [PATCH 1/2] Install PolicyBench from GitHub in the README quick start PolicyBench is not on PyPI (pypi.org/simple/policybench/ and the JSON API returned 404 on 2026-10-06), so `pip install policybench` failed and would install a squatter's package if the name were ever registered. The quick start now installs from git+https://github.com/PolicyEngine/policybench with uv (pinned to Python 3.12) or pip, and the development path clones the repo and syncs the locked environment as CI does. tests/test_install_docs.py fails if any reader-facing doc installs the policybench distribution from a package index by name. Co-Authored-By: Claude Opus 5.5 --- README.md | 22 ++- tests/test_install_docs.py | 278 +++++++++++++++++++++++++++++++++++++ 2 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 tests/test_install_docs.py diff --git a/README.md b/README.md index 63c7b83c..6eb5333c 100644 --- a/README.md +++ b/README.md @@ -48,16 +48,30 @@ information. ## Quick start +PolicyBench is not published on PyPI, so install the command-line tool from +GitHub. With [uv](https://docs.astral.sh/uv/): + +```bash +uv tool install --python 3.12 git+https://github.com/PolicyEngine/policybench +policybench --help +``` + +Or with pip, in a standard (not free-threaded) Python 3.11 to 3.14 virtual +environment: + ```bash -pip install policybench +pip install git+https://github.com/PolicyEngine/policybench policybench --help ``` -For repository development, clone the full Git repository before running tests: +For repository development, clone the full Git repository and install the +locked environment before running tests: ```bash -pip install -e ".[dev]" -pytest +git clone https://github.com/PolicyEngine/policybench +cd policybench +uv sync --locked --extra dev --python 3.12 +uv run pytest ``` Verify the dashboard with its bundled data prepared automatically: diff --git a/tests/test_install_docs.py b/tests/test_install_docs.py new file mode 100644 index 00000000..57eafdd0 --- /dev/null +++ b/tests/test_install_docs.py @@ -0,0 +1,278 @@ +"""Install commands in the docs must name a source that exists. + +PolicyBench is not published on PyPI: on 2026-10-06 both +https://pypi.org/pypi/policybench/json and https://pypi.org/simple/policybench/ +returned 404. A command that installs the ``policybench`` distribution from a +package index by name (``pip install policybench``, ``uvx policybench``) fails +today, and would install whatever a third party later uploads under the name. +The README installs from GitHub and the development docs from a clone; these +tests hold every surface a reader follows to that. If PolicyBench is ever +published to PyPI, delete this module with the README's "not published" line. +""" + +import html +import re +import shlex +import tomllib +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +DISTRIBUTION = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]["name"] +REPO_URL = "https://github.com/PolicyEngine/policybench" + +# Where readers and agents look for install instructions. Dated records +# (docs/adds0928/, docs/gpt61sol/, reference_audit/) are left out: they quote +# what happened rather than tell anyone what to run. +SURFACES = ( + "*.md", + "docs/*.md", + "docs/requirements.txt", + "paper/README.md", + "paper/index.qmd", + "app/public/paper/web/index.html", + "sensitivity/*.md", + "app/src/**/*.ts", + "app/src/**/*.tsx", + "app/src/notes/*.json", + ".github/workflows/*.yml", +) + +# Each installer and what its positional arguments are. +# requirements: every positional argument is a requirement (pip install). +# tool: the first positional names the tool's package, unless --from or +# --spec names it, and later tokens are the tool's own arguments (uvx). +# command: positionals are a command to run; only --with adds a package. +# `pip install` also matches inside `uv pip install` and `python -m pip install`. +INSTALLERS = ( + (re.compile(r"(?)]") +SENTENCE_END = ",.;:!?" + + +def _normalize(name: str) -> str: + """PEP 503 name normalization.""" + return re.sub(r"[-_.]+", "-", name).lower() + + +def _index_name(token: str) -> str | None: + """The distribution ``token`` fetches from an index by name, if any. + + Paths, archives, URLs and PEP 508 direct references (``name @ url``) are + not fetched from an index by name, so they return None. + """ + if not token or token[0] in "-./~$" or "/" in token or "\\" in token: + return None + if token.startswith(("git+", "file:")) or "://" in token: + return None + if token.endswith((".whl", ".tar.gz", ".zip")): + return None + match = re.match(r"([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(.*)$", token) + if match is None: + return None + name, rest = match.groups() + if re.match(r"(\[[^\]]*\])?\s*@\s*(git\+|file:|\w+://)", rest): + return None + return _normalize(name) + + +def _tokens(segment: str) -> list[str]: + try: + tokens = shlex.split(segment) + except ValueError: # an unbalanced quote, e.g. a command inside a JSON string + tokens = segment.split() + return [token.strip("'\"") for token in tokens] + + +def _scan(tokens: list[str], role: str) -> list[str]: + """Distributions the installer's arguments fetch from an index by name.""" + names = [] + named_by_option = False + i = 0 + while i < len(tokens): + raw = tokens[i] + token = raw if raw.startswith(".") else raw.rstrip(SENTENCE_END) + # Prose after an inline command: "pip install uv, then clone the repo". + ends_sentence = token != raw + if token.startswith("-"): + flag, has_value, value = token.partition("=") + if flag in REQUIREMENT_OPTIONS: + if not has_value and i + 1 < len(tokens): + i += 1 + value = tokens[i] + named_by_option = True + names += [n for n in map(_index_name, value.split(",")) if n] + elif flag in VALUE_OPTIONS and not has_value: + i += 1 + elif role == "command" or (role == "tool" and named_by_option): + break + elif i + 1 < len(tokens) and tokens[i + 1].startswith("@"): + # A PEP 508 direct reference split by spaces: "name @ url". + i += 2 if tokens[i + 1] == "@" else 1 + else: + name = _index_name(token) + if name: + names.append(name) + if role == "tool": + break + if ends_sentence: + break + i += 1 + return names + + +def index_installs(text: str) -> list[str]: + """Every distribution an install command in ``text`` fetches by name.""" + names = [] + for line in text.replace("\\\n", " ").splitlines(): + for pattern, role in INSTALLERS: + for match in pattern.finditer(line): + segment = COMMAND_END.split(line[match.end() :], maxsplit=1)[0] + names += _scan(_tokens(segment), role) + return names + + +def surface_installs(path: Path) -> list[str]: + """Every distribution ``path`` fetches from an index by name.""" + text = path.read_text(encoding="utf-8") + if path.name.endswith("requirements.txt"): + # Each line of a requirements file is itself a requirement. + lines = (line.split("#", 1)[0].strip() for line in text.splitlines()) + return [n for n in map(_index_name, lines) if n] + if path.suffix == ".html": + # Highlighted code splits a command across spans; read it as shown. + text = html.unescape(re.sub(r"<[^>]+>", "", text)) + return index_installs(text) + + +@pytest.mark.parametrize( + "command", + [ + "pip install policybench", + "pip3 install -U policybench", + "python -m pip install 'policybench[dev]>=2'", + "uv pip install PolicyBench==2.0.0", + "uv tool install --python 3.12 policybench", + "uv add policybench", + "pipx install policybench", + "conda install -c conda-forge policybench", + "pip install numpy policybench", + "pip install policybench.", + "uvx policybench --help", + "uvx policybench@latest --help", + "uv tool run policybench --help", + "pipx run policybench", + "uvx --with policybench python", + "uv run --with numpy,policybench python", + "uvx --from policybench policybench --help", + "Run `pip install policybench` first.", + '"body": "Run pip install policybench to start",', + "pip install \\\n policybench", + ], +) +def test_detector_flags_index_installs_of_policybench(command): + assert DISTRIBUTION in index_installs(command) + + +@pytest.mark.parametrize( + "command", + [ + f"pip install git+{REPO_URL}", + f"pip install git+{REPO_URL}.git@main", + f"uv tool install --python 3.12 git+{REPO_URL}", + f"uvx --from git+{REPO_URL} policybench --help", + f"pip install 'policybench @ git+{REPO_URL}'", + f"pip install policybench@git+{REPO_URL}", + 'pip install -e ".[dev]"', + "pip install -e policybench", + "pip install ./policybench", + "pip install dist/policybench-2.0.0-py3-none-any.whl", + "pip install policybench-tools", + "pip install uv", + "Run pip install uv, then clone policybench.", + "uv sync --locked --extra dev", + "uv run policybench onboard", + "uv run python -m policybench.cli reference-outputs", + "policybench --help", + ], +) +def test_detector_passes_installs_from_source(command): + assert DISTRIBUTION not in index_installs(command) + + +@pytest.mark.parametrize( + ("lines", "flagged"), + [ + ("policybench>=2\n", True), + ("jupyter-book>=2.0\nPolicyBench # the benchmark\n", True), + (f"git+{REPO_URL}\n-e .\n# policybench\n", False), + ], +) +def test_detector_reads_requirements_files(tmp_path, lines, flagged): + path = tmp_path / "requirements.txt" + path.write_text(lines) + assert (DISTRIBUTION in surface_installs(path)) is flagged + + +@pytest.mark.parametrize("pattern", SURFACES) +def test_no_surface_installs_policybench_from_an_index(pattern): + paths = sorted(ROOT.glob(pattern)) + assert paths, f"{pattern} matches no file; update SURFACES" + for path in paths: + assert DISTRIBUTION not in surface_installs(path), ( + f"{path.relative_to(ROOT)} installs {DISTRIBUTION} from a package " + f"index by name, but it is not published there; install from " + f"git+{REPO_URL} or a clone instead" + ) + + +def test_readme_installs_policybench_from_github(): + """The quick start installs the package from this repository, so a reader + can run ``policybench --help`` without a package index.""" + readme = (ROOT / "README.md").read_text(encoding="utf-8") + quick_start = readme.split("## Quick start", 1)[1].split("\n## ", 1)[0] + installs_from_github = re.search( + r"(?:pip3?\s+install|uv\s+tool\s+install|uv\s+add|pipx\s+install|--from)" + rf"\s+(?:\S+\s+)*?git\+{re.escape(REPO_URL)}(?:\.git)?(?:@\S+)?(?=\s|$)", + quick_start, + ) + assert installs_from_github, quick_start + assert "policybench --help" in quick_start From 91f37b757b6e8b5886809d4444815b964bd1e952 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 07:52:32 -0400 Subject: [PATCH 2/2] Read install commands per format; note what the run commands assume An adversarial pass on #199 found the detector missed commands wrapped across lines, JSON-escaped note text and `uvx --with X policybench`, and failed prose such as "uv tool install puts policybench on your PATH" and trailing `# ... policybench` comments. It now decodes each surface first (JSON strings, HTML/TSX tags), reads code (fences,
, inline spans and
 joined across soft wraps) as full commands and prose only as an
installer followed by the name, and tokenizes with shlex comments and
operators. The README check reuses the tokenizer.

README: the dashboard block runs `bun install --frozen-lockfile` first, as
CI and the runbook do, and the benchmark run section says its commands are
for a clone and need `uv run` there.

Co-Authored-By: Claude Opus 5.5 
---
 README.md                  |   7 +
 tests/test_install_docs.py | 285 +++++++++++++++++++++++++------------
 2 files changed, 202 insertions(+), 90 deletions(-)

diff --git a/README.md b/README.md
index 6eb5333c..e06e1cb0 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,7 @@ Verify the dashboard with its bundled data prepared automatically:
 
 ```bash
 cd app
+bun install --frozen-lockfile
 bun run lint
 bun run test
 bun run build
@@ -91,6 +92,12 @@ The short version is: generate fixed reference-output manifests first, run
 Claude models serially, run non-Claude models in parallel, then do a final merge
 and export pass.
 
+Run these commands from a clone of the repository: they write under
+`results/local/`, and `analyze` also writes the dashboard payload to
+`app/src/data.json`. They call `policybench` as `uv tool install` puts it on
+your `PATH`; in a clone set up with `uv sync`, prefix each with `uv run`, as the
+runbook does.
+
 ```bash
 # Generate reference outputs for 100 sampled households using headline outputs
 policybench reference-outputs -n 100 --seed 42
diff --git a/tests/test_install_docs.py b/tests/test_install_docs.py
index 57eafdd0..19ba1b4e 100644
--- a/tests/test_install_docs.py
+++ b/tests/test_install_docs.py
@@ -5,12 +5,15 @@
 returned 404. A command that installs the ``policybench`` distribution from a
 package index by name (``pip install policybench``, ``uvx policybench``) fails
 today, and would install whatever a third party later uploads under the name.
-The README installs from GitHub and the development docs from a clone; these
-tests hold every surface a reader follows to that. If PolicyBench is ever
-published to PyPI, delete this module with the README's "not published" line.
+The README installs it from GitHub and the development docs from a clone; these
+tests hold every surface a reader follows to that. A doc that quotes the index
+command, even to warn against it, fails too: readers copy commands. If
+PolicyBench is ever published to PyPI, delete this module with the README's
+"not published" line.
 """
 
 import html
+import json
 import re
 import shlex
 import tomllib
@@ -40,7 +43,7 @@
 )
 
 # Each installer and what its positional arguments are.
-#   requirements: every positional argument is a requirement (pip install).
+#   requirements: each positional is a requirement (pip install).
 #   tool: the first positional names the tool's package, unless --from or
 #     --spec names it, and later tokens are the tool's own arguments (uvx).
 #   command: positionals are a command to run; only --with adds a package.
@@ -54,38 +57,63 @@
     (re.compile(r"\buvx\b|\buv\s+tool\s+run\b|\bpipx\s+run\b"), "tool"),
     (re.compile(r"\buv\s+run\b"), "command"),
 )
-# Options whose value is a requirement, and options whose value is anything
-# else (a path, an index, a Python version, a channel) and is skipped.
-REQUIREMENT_OPTIONS = {"--with", "--from", "--spec"}
+# Options whose value is a requirement; --from and --spec also name the tool.
+REQUIREMENT_OPTIONS = {"--with", "-w", "--from", "--spec"}
+TOOL_OPTIONS = {"--from", "--spec"}
+# Options whose value is anything else (a path, an index, a Python version, a
+# channel, a format control), which is skipped.
 VALUE_OPTIONS = {
+    "-C",
+    "-P",
     "-c",
     "-e",
     "-f",
     "-i",
+    "-n",
     "-p",
     "-r",
     "-t",
     "--channel",
+    "--config-settings",
     "--constraint",
+    "--default-index",
+    "--directory",
     "--editable",
+    "--env-file",
+    "--exclude-newer",
     "--extra",
     "--extra-index-url",
     "--find-links",
     "--group",
     "--index",
+    "--index-strategy",
     "--index-url",
+    "--name",
+    "--no-binary",
+    "--only-binary",
+    "--optional",
+    "--pip-args",
+    "--platform",
     "--prefix",
+    "--project",
     "--python",
+    "--python-platform",
+    "--python-version",
     "--requirement",
     "--root",
     "--target",
+    "--upgrade-package",
     "--with-editable",
     "--with-requirements",
 }
-# What ends a command on its line: shell operators, an inline-code backtick,
-# or the markup that follows a command embedded in a link, tag or string.
-COMMAND_END = re.compile(r"[`;|&<>)]")
-SENTENCE_END = ",.;:!?"
+# Flags that keep the installer off every index, as in an offline install
+# from a directory of built wheels.
+OFFLINE_FLAGS = {"--no-index", "--offline"}
+SHELL_OPERATORS = {";", "&", "&&", "|", "||", "<", ">", ">>", "(", ")"}
+FENCE = re.compile(r"^(```|~~~)[^\n]*\n(.*?)^\1", re.DOTALL | re.MULTILINE)
+INLINE_CODE = re.compile(r"`([^`]+)`")
+HTML_CODE = re.compile(r"<(code|pre)\b[^>]*>(.*?)", re.DOTALL)
+TAG = re.compile(r"<[^>]+>")
 
 
 def _normalize(name: str) -> str:
@@ -105,71 +133,106 @@ def _index_name(token: str) -> str | None:
         return None
     if token.endswith((".whl", ".tar.gz", ".zip")):
         return None
-    match = re.match(r"([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(.*)$", token)
-    if match is None:
-        return None
-    name, rest = match.groups()
-    if re.match(r"(\[[^\]]*\])?\s*@\s*(git\+|file:|\w+://)", rest):
-        return None
-    return _normalize(name)
+    match = re.match(r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?", token)
+    return _normalize(match.group()) if match else None
+
 
+def _tokens(arguments: str) -> list[str]:
+    """Shell tokens, with quotes, ``#`` comments and operators handled."""
+    for text in (arguments, re.sub(r"[\"']", "", arguments)):
+        lexer = shlex.shlex(text, posix=True, punctuation_chars=True)
+        lexer.whitespace_split = True
+        try:
+            return list(lexer)
+        except ValueError:  # an unbalanced quote: retry without quotes
+            continue
+    return []
 
-def _tokens(segment: str) -> list[str]:
-    try:
-        tokens = shlex.split(segment)
-    except ValueError:  # an unbalanced quote, e.g. a command inside a JSON string
-        tokens = segment.split()
-    return [token.strip("'\"") for token in tokens]
 
+def _scan(tokens: list[str], role: str, every_positional: bool) -> list[str]:
+    """Distributions an installer's arguments fetch from an index by name.
 
-def _scan(tokens: list[str], role: str) -> list[str]:
-    """Distributions the installer's arguments fetch from an index by name."""
+    In code every positional of a ``requirements`` installer counts. In prose
+    only the one right after the installer and its options does, so "uv tool
+    install puts policybench on your PATH" names no requirement.
+    """
     names = []
-    named_by_option = False
+    named_tool = False
     i = 0
-    while i < len(tokens):
-        raw = tokens[i]
-        token = raw if raw.startswith(".") else raw.rstrip(SENTENCE_END)
-        # Prose after an inline command: "pip install uv, then clone the repo".
-        ends_sentence = token != raw
+    while i < len(tokens) and tokens[i] not in SHELL_OPERATORS:
+        token = tokens[i].strip("\u201c\u201d\u2018\u2019")  # curly quotes in prose
+        if token in OFFLINE_FLAGS:
+            return []
         if token.startswith("-"):
             flag, has_value, value = token.partition("=")
             if flag in REQUIREMENT_OPTIONS:
                 if not has_value and i + 1 < len(tokens):
                     i += 1
                     value = tokens[i]
-                named_by_option = True
                 names += [n for n in map(_index_name, value.split(",")) if n]
+                named_tool = named_tool or flag in TOOL_OPTIONS
             elif flag in VALUE_OPTIONS and not has_value:
                 i += 1
-        elif role == "command" or (role == "tool" and named_by_option):
+        elif role == "command" or (role == "tool" and named_tool):
             break
-        elif i + 1 < len(tokens) and tokens[i + 1].startswith("@"):
-            # A PEP 508 direct reference split by spaces: "name @ url".
-            i += 2 if tokens[i + 1] == "@" else 1
         else:
-            name = _index_name(token)
+            name = _index_name(token.rstrip(".,;:!?)}\"'"))
             if name:
                 names.append(name)
-            if role == "tool":
+            if role == "tool" or not every_positional:
                 break
-        if ends_sentence:
-            break
         i += 1
     return names
 
 
-def index_installs(text: str) -> list[str]:
-    """Every distribution an install command in ``text`` fetches by name."""
+def _installs(snippet: str, every_positional: bool) -> list[str]:
     names = []
-    for line in text.replace("\\\n", " ").splitlines():
-        for pattern, role in INSTALLERS:
-            for match in pattern.finditer(line):
-                segment = COMMAND_END.split(line[match.end() :], maxsplit=1)[0]
-                names += _scan(_tokens(segment), role)
+    for pattern, role in INSTALLERS:
+        for match in pattern.finditer(snippet):
+            tokens = _tokens(snippet[match.end() :])
+            names += _scan(tokens, role, every_positional)
     return names
 
 
+def _code_and_prose(text: str, markup: bool) -> tuple[list[str], str]:
+    """Split text into code snippets and the prose around them.
+
+    Code is fenced blocks and, in markup, ``
`` elements (one command per
+    line), plus inline code spans and ```` elements, joined across a
+    soft wrap.
+    Prose is everything else with line breaks collapsed, so a wrapped sentence
+    reads as one line.
+    """
+    code = []
+    if markup:
+        for tag, inner in HTML_CODE.findall(text):
+            inner = html.unescape(TAG.sub("", inner))
+            code += inner.splitlines() if tag == "pre" else [" ".join(inner.split())]
+        text = html.unescape(TAG.sub(" ", text))
+    for _, block in FENCE.findall(text.replace("\\\n", " ")):
+        code += block.splitlines()
+    text = FENCE.sub("\n", text)
+    code += [" ".join(span.split()) for span in INLINE_CODE.findall(text)]
+    return code, " ".join(INLINE_CODE.sub(r"\1", text).split())
+
+
+def index_installs(text: str, markup: bool = False) -> list[str]:
+    """Every distribution an install command in ``text`` fetches by name."""
+    code, prose = _code_and_prose(text, markup)
+    names = [n for snippet in code for n in _installs(snippet, True)]
+    return names + _installs(prose, False)
+
+
+def _json_strings(value) -> list[str]:
+    if isinstance(value, str):
+        return [value]
+    if isinstance(value, dict):
+        value = list(value.values())
+    if isinstance(value, list):
+        return [s for item in value for s in _json_strings(item)]
+    return []
+
+
 def surface_installs(path: Path) -> list[str]:
     """Every distribution ``path`` fetches from an index by name."""
     text = path.read_text(encoding="utf-8")
@@ -177,78 +240,112 @@ def surface_installs(path: Path) -> list[str]:
         # Each line of a requirements file is itself a requirement.
         lines = (line.split("#", 1)[0].strip() for line in text.splitlines())
         return [n for n in map(_index_name, lines) if n]
-    if path.suffix == ".html":
-        # Highlighted code splits a command across spans; read it as shown.
-        text = html.unescape(re.sub(r"<[^>]+>", "", text))
-    return index_installs(text)
+    if path.suffix == ".json":
+        # Decode escapes so a quoted command in a note reads as written.
+        strings = _json_strings(json.loads(text))
+        return [n for s in strings for n in index_installs(s)]
+    if path.suffix in {".yml", ".yaml", ".sh"}:
+        return [n for line in text.splitlines() for n in _installs(line, True)]
+    return index_installs(text, markup=path.suffix in {".html", ".ts", ".tsx"})
 
 
 @pytest.mark.parametrize(
-    "command",
+    "text",
     [
         "pip install policybench",
-        "pip3 install -U policybench",
+        "$ pip3 install -U policybench",
         "python -m pip install 'policybench[dev]>=2'",
-        "uv pip install PolicyBench==2.0.0",
+        "py -3.12 -m pip install policybench",
+        "uv pip install --system PolicyBench==2.0.0",
         "uv tool install --python 3.12 policybench",
         "uv add policybench",
         "pipx install policybench",
         "conda install -c conda-forge policybench",
-        "pip install numpy policybench",
-        "pip install policybench.",
+        "!pip install policybench",
+        "`pip install numpy policybench`",
+        '`pip install "policyengine-us>=2" policybench`',
+        "`pip install --no-binary :all: policybench`",
+        "Run pip install policybench to get started.",
+        "Install it with `pip install\npolicybench` and run it.",
+        "Install it with `pip\ninstall policybench`.",
         "uvx policybench --help",
         "uvx policybench@latest --help",
         "uv tool run policybench --help",
         "pipx run policybench",
-        "uvx --with policybench python",
-        "uv run --with numpy,policybench python",
-        "uvx --from policybench policybench --help",
-        "Run `pip install policybench` first.",
-        '"body": "Run pip install policybench to start",',
-        "pip install \\\n  policybench",
+        "`uvx --with rich policybench --help`",
+        "`uvx -w policybench python`",
+        "`uv run --with numpy,policybench python`",
+        "`uvx --from policybench policybench --help`",
+        "uv add --optional dev policybench",
+        "uvx --exclude-newer 2026-10-01 policybench --help",
+        "Install it with pip install \u201cpolicybench\u201d.",
+        "PolicyBench is not on PyPI, so `pip install policybench` fails.",
+        "```bash\npip install \\\n  policybench\n```",
     ],
 )
-def test_detector_flags_index_installs_of_policybench(command):
-    assert DISTRIBUTION in index_installs(command)
+def test_detector_flags_index_installs_of_policybench(text):
+    assert DISTRIBUTION in index_installs(text)
 
 
 @pytest.mark.parametrize(
-    "command",
+    "text",
     [
         f"pip install git+{REPO_URL}",
         f"pip install git+{REPO_URL}.git@main",
-        f"uv tool install --python 3.12 git+{REPO_URL}",
+        f"pip install git+{REPO_URL}#egg=policybench",
+        f"uv tool install --python 3.12 git+{REPO_URL}  # puts policybench on PATH",
         f"uvx --from git+{REPO_URL} policybench --help",
         f"pip install 'policybench @ git+{REPO_URL}'",
         f"pip install policybench@git+{REPO_URL}",
-        'pip install -e ".[dev]"',
+        'pip install -e ".[dev]"   # editable install of policybench',
         "pip install -e policybench",
         "pip install ./policybench",
         "pip install dist/policybench-2.0.0-py3-none-any.whl",
         "pip install policybench-tools",
-        "pip install uv",
-        "Run pip install uv, then clone policybench.",
+        "pip install uv, then clone policybench.",
+        "uv tool install puts policybench on your PATH.",
+        "A plain pip install of policybench fails because it is not on PyPI.",
+        "uvx and uv tool install both fetch policybench from GitHub.",
+        "`pip install --no-index --find-links dist policybench`",
+        "conda install -n policybench uv",
         "uv sync --locked --extra dev",
         "uv run policybench onboard",
         "uv run python -m policybench.cli reference-outputs",
         "policybench --help",
     ],
 )
-def test_detector_passes_installs_from_source(command):
-    assert DISTRIBUTION not in index_installs(command)
+def test_detector_passes_installs_from_source(text):
+    assert DISTRIBUTION not in index_installs(text)
 
 
 @pytest.mark.parametrize(
-    ("lines", "flagged"),
+    ("name", "content", "flagged"),
     [
-        ("policybench>=2\n", True),
-        ("jupyter-book>=2.0\nPolicyBench  # the benchmark\n", True),
-        (f"git+{REPO_URL}\n-e .\n# policybench\n", False),
+        ("requirements.txt", "policybench>=2\n", True),
+        ("requirements.txt", "jupyter-book\nPolicyBench  # the benchmark\n", True),
+        ("requirements.txt", f"git+{REPO_URL}\n-e .\n# policybench\n", False),
+        ("note.json", json.dumps({"body": 'Run "pip install policybench".'}), True),
+        ("note.json", json.dumps({"body": "```bash\nuvx policybench\n```"}), True),
+        ("note.json", json.dumps({"body": f"pip install git+{REPO_URL}"}), False),
+        (
+            "Page.tsx",
+            "\n  uv tool install --python 3.12\n  policybench\n",
+            True,
+        ),
+        (
+            "Page.tsx",
+            "

\n Install with pip install\n policybench today.\n

", + True, + ), + ("page.html", 'pip install policybench', True), + ("page.html", "
pip install uv\npolicybench --help
", False), + ("ci.yml", " - run: pip install policybench\n", True), + ("ci.yml", " - run: pip install uv\n", False), ], ) -def test_detector_reads_requirements_files(tmp_path, lines, flagged): - path = tmp_path / "requirements.txt" - path.write_text(lines) +def test_detector_reads_each_surface_format(tmp_path, name, content, flagged): + path = tmp_path / name + path.write_text(content) assert (DISTRIBUTION in surface_installs(path)) is flagged @@ -259,20 +356,28 @@ def test_no_surface_installs_policybench_from_an_index(pattern): for path in paths: assert DISTRIBUTION not in surface_installs(path), ( f"{path.relative_to(ROOT)} installs {DISTRIBUTION} from a package " - f"index by name, but it is not published there; install from " - f"git+{REPO_URL} or a clone instead" + f"index by name, or quotes a command that does, but it is not " + f"published there; install from git+{REPO_URL} or a clone instead" ) +def _github_source(token: str) -> bool: + source = re.sub(r"^[\w.-]+(\[[^\]]*\])?\s*@\s*", "", token) + return re.match(rf"git\+{re.escape(REPO_URL)}(\.git)?([@#/]|$)", source) is not None + + def test_readme_installs_policybench_from_github(): - """The quick start installs the package from this repository, so a reader - can run ``policybench --help`` without a package index.""" + """The quick start installs PolicyBench itself from this repository (its + dependencies still come from PyPI) and shows ``policybench --help``.""" readme = (ROOT / "README.md").read_text(encoding="utf-8") quick_start = readme.split("## Quick start", 1)[1].split("\n## ", 1)[0] - installs_from_github = re.search( - r"(?:pip3?\s+install|uv\s+tool\s+install|uv\s+add|pipx\s+install|--from)" - rf"\s+(?:\S+\s+)*?git\+{re.escape(REPO_URL)}(?:\.git)?(?:@\S+)?(?=\s|$)", - quick_start, - ) - assert installs_from_github, quick_start - assert "policybench --help" in quick_start + code, _ = _code_and_prose(quick_start, markup=False) + sources = [ + token + for snippet in code + for pattern, _ in INSTALLERS[:3] + for match in pattern.finditer(snippet) + for token in _tokens(snippet[match.end() :]) + ] + assert any(map(_github_source, sources)), code + assert "policybench --help" in code