llm-ingestion-okf/tests/test_pandoc_binary.py
Kjell Tore Guttormsen 36c201cc8a chore(ruff): the acceptance was whatever the default happened to be [skip-docs]
`uv sync --frozen` resolved ruff 0.15.22 and the tree read clean. A loose
install resolves 0.16.6, under which the SAME untouched code reports 148
findings -- 4 more than round 9 counted, because this round added four files.
All of them are new rules rather than new defects: 0.16 widened the default
rule set to whole families (YTT, ASYNC, PL, ISC, C4, UP, B, SIM, FURB, ...).

(`[skip-docs]` is for CLAUDE.md, which a lint-configuration change does not
reach. README's developer section IS updated in this commit.)

THE DEFECT IS NOT THE 148, IT IS THAT NOBODY CHOSE THEM. `[tool.ruff]` set only
`line-length` and `target-version`, so the acceptance was ruff's default, and
the tree stayed green only as long as the lockfile froze an old ruff. `select`
is now written down: `E4`, `E7`, `E9`, `F` (the historical default), `I`
because this tree already keeps imports sorted, and `RUF100` so a `noqa` that
has stopped meaning anything is caught rather than left as decoration. Pin
`ruff>=0.9` -> `ruff>=0.16.6,<0.17`.

Per rule, before -> after: RUF100 50 -> 0, I001 20 -> 0, ISC004 19, PLW1510 8,
C408 8, EXE001 6, RUF007 5, PLE2515 4, UP031 3, B017 3, and fourteen more with
2 or fewer -- the families out of the declared set are 0 by selection, and 148
is the number to start from if they are adopted, which is a separate decision
and not one to take inside a version-pin commit. 57 were auto-fixed; one E402
was reintroduced by the import-sorting fix merging a block away from its
`noqa`, and got the directive back rather than a bare one.

`S` IS MEASURED OUT, NOT ASSUMED OUT: it reports 2657 `S101` on a suite whose
every assertion is an `assert`, and `S603` flags 19 subprocess calls of which
one was ever marked -- selecting it buys 18 suppressions and no defect. Two
`noqa` directives naming non-selected rules were dropped with that reason
recorded in the configuration instead.

THE TWO FILES 0.16 WOULD REFORMAT ARE MARKDOWN, NOT PYTHON: `README.md` and
`docs/2026-09-08-blindsone-below-k-k2.md`. 0.16 formats fenced Python inside
markdown, and both blocks are RECORDS -- the second is a quotation of
`COST_VOCABULARY` as it stood when that measurement was taken. Reformatting a
quotation makes it stop being one, so markdown is excluded from the formatter
and `ruff format --check .` stays in the acceptance over `.py`.

`tools/okf_consume_measure.py` is fenced by the order as run-not-edited, so its
three findings are exempted by path with the reason and the debt named, and its
bytes are untouched.

THE LOCKFILE TRAP IS CLOSED, NOT AVOIDED. `uv.lock` predated the `[ocr]` extra,
so any unlocked resolve wrote that extra's transitive tree back into it -- 681
insertions over 4 deletions, twice now, and round 9 recorded the cause as
`uv run` OUTSIDE the project when it is `uv run` without `--frozen` INSIDE it.
The relock is complete for every declared extra (703 insertions, 26 deletions),
and measured after it, an unfrozen `uv run` leaves the file alone.

`ruff check src tests tools`, `ruff format --check .` (0.16.6), `mypy src` over
21 files and 1535 tests, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 23:15:17 +02:00

147 lines
5.6 KiB
Python

"""The vendored converter is resolved explicitly, or not used at all.
`pypandoc` searches `PATH` before its own bundled binary and takes the HIGHEST
version it finds. Measured three times independently on this host: the wheel
carries pandoc 3.9, the host carries 3.10.2, and `pypandoc.get_pandoc_version()`
returns 3.10.2. So "we vendored the binary" buys nothing on its own -- the
bundle would be built by a converter nobody chose, and nothing would say so.
These tests pin the resolution, not the conversion. The seam that converts is
tested separately; what is falsifiable here is which binary a conversion would
have used, and that the answer is refused rather than guessed when it is wrong.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import pytest
from llm_ingestion_okf._pandoc import (
PANDOC_VERSION,
converter_path,
resolve_pandoc,
)
from llm_ingestion_okf.errors import ExtractionError
pytest.importorskip("pypandoc", reason="the [extract] extra is not installed")
def test_the_resolved_binary_is_the_bundled_one_not_a_path_binary() -> None:
"""The whole point of the module, stated as an assertion.
The bundled binary lives inside the installed package; a PATH binary does
not. Comparing the resolved path against the package directory is what
distinguishes them -- comparing versions would not, because a host could
coincidentally carry the pinned version today and a different one tomorrow.
"""
import pypandoc
resolved = resolve_pandoc()
bundled = Path(pypandoc.__file__).parent / "files" / "pandoc"
assert resolved == bundled
assert resolved.is_file()
def test_the_resolved_binary_reports_the_pinned_version() -> None:
import subprocess
reported = subprocess.run(
[str(resolve_pandoc()), "--version"], capture_output=True, text=True
).stdout.split("\n")[0]
assert reported == f"pandoc {PANDOC_VERSION}"
def test_resolution_is_not_poisoned_by_an_earlier_probe_in_the_process() -> None:
"""The regression this file did not catch on its first pass.
`pypandoc.get_pandoc_version()` answers from a module global that
`clean_pandocpath_cache()` does not reset. Using it meant the version check
described whichever binary was probed FIRST in the process -- so the first
implementation reported the host's 3.10.2 for the bundled 3.9 binary, and
the suite stayed green because nothing in it probed the host binary first.
A test that passes only in a fresh process is not a test of the resolver.
This one poisons the caches the way real use does, then resolves.
"""
import pypandoc
pypandoc.get_pandoc_version() # caches whatever the search finds
assert resolve_pandoc().is_file() # must not raise extractor_binary_version
def test_a_version_mismatch_is_refused_and_names_both_versions(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Refused, not used with a warning.
Extraction is deterministic within a converter version and not across one,
and the byte-pinned fixtures cannot tell "different converter" from
"defect". A mismatch that proceeded would make every later measurement
unattributable.
"""
monkeypatch.setattr("llm_ingestion_okf._pandoc.PANDOC_VERSION", "0.0.0")
with pytest.raises(ExtractionError) as excinfo:
resolve_pandoc()
assert excinfo.value.code == "extractor_binary_version"
message = str(excinfo.value)
assert "0.0.0" in message, "the expected version is not named"
assert PANDOC_VERSION in message, "the found version is not named"
def test_an_absent_binary_raises_binary_missing(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Distinct from the extra being absent: the wheel can be there without it."""
import pypandoc
monkeypatch.setattr(pypandoc, "__file__", str(tmp_path / "pypandoc" / "__init__.py"))
with pytest.raises(ExtractionError) as excinfo:
resolve_pandoc()
assert excinfo.value.code == "extractor_binary_missing"
def test_the_extra_being_absent_keeps_the_same_rejection(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setitem(sys.modules, "pypandoc", None)
with pytest.raises(ExtractionError) as excinfo:
resolve_pandoc()
assert excinfo.value.code == "extractor_extra_missing"
assert "extract" in str(excinfo.value)
def test_the_scoped_path_leaves_os_environ_exactly_as_it_found_it() -> None:
"""A library must not set a process-global that outlives its own call.
`pypandoc` offers no per-call path parameter -- the only override is the
`PYPANDOC_PANDOC` environment variable plus a cached module global. Both
are process-wide, so the scope is where the discipline has to live: enter,
convert, restore, whether or not the body raised.
"""
before = dict(os.environ)
with converter_path() as path:
assert os.environ["PYPANDOC_PANDOC"] == str(path)
assert dict(os.environ) == before
with pytest.raises(RuntimeError):
with converter_path():
raise RuntimeError("the body failed")
assert dict(os.environ) == before, "an exception must not leak the override"
def test_a_pre_existing_override_is_restored_not_dropped(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Restoring means putting back what was there, including a wrong value.
Deleting the key on exit would look correct in an environment that had
none, and would silently erase an operator's deliberate override in one
that did.
"""
monkeypatch.setenv("PYPANDOC_PANDOC", "/somewhere/else/pandoc")
with converter_path():
pass
assert os.environ["PYPANDOC_PANDOC"] == "/somewhere/else/pandoc"