diff --git a/CHANGELOG.md b/CHANGELOG.md index ccef9f2a..5019bba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -132,6 +132,17 @@ per-file diff commands. The standard `date`-first header already worked; regression coverage checks all three column orders with and without a BOM. +- **`verify_layout.py` no longer emits a `SyntaxWarning` on every run** (`tools/verify_layout.py`, + `tests/test_verify_layout.py`) - the module docstring names the macro whose absence triggers the + Poppler `-bbox` crash, `\hypersetup{pdftitle=...}`, and a bare `\h` in a non-raw docstring is an + invalid escape sequence. Python 3.12+ prints `SyntaxWarning: "\h" is an invalid escape sequence` + the first time the module is compiled (CPython gh-98401) - it shows up in this repo's own CI log, + and in the middle of `/apply` Step 5b's layout report. The 3.15 language reference still + documents the sequence as a `SyntaxWarning`, with a `SyntaxError` only in a future Python + version. Escaped in the docstring; the new `ToolsCompileWithoutWarnings` case compiles the + source of every `tools/*.py` with warnings captured and fails on the unescaped version, so the + next docstring that quotes a LaTeX macro is caught too. + - **`/apply` Step 5b now actually runs the page-count check it claimed Step 5d ran** (`.claude/commands/apply.md`, `tests/test_apply_page_count.py`) - the 5b prose said "Page count is not checked here - that is `verify_pdf.py --pages`'s job, and Step 5d already diff --git a/tests/test_verify_layout.py b/tests/test_verify_layout.py index 31ce49d9..5483c134 100644 --- a/tests/test_verify_layout.py +++ b/tests/test_verify_layout.py @@ -16,6 +16,7 @@ from contextlib import redirect_stdout from pathlib import Path from unittest.mock import patch +from tools import verify_layout from tools.verify_layout import Line, Page, find_orphans, main, parse_pdf, report A4_HEIGHT = 842.0 @@ -164,5 +165,41 @@ class TestExtractorFailure(unittest.TestCase): self.assertIn("skipped:", err.getvalue()) +class ToolsCompileWithoutWarnings(unittest.TestCase): + """Every tool's source must compile clean. + + `verify_layout.py`'s docstring documents LaTeX macros, and a bare `\\h` in a + non-raw docstring is an invalid escape sequence: Python 3.12+ emits a + SyntaxWarning when the module is compiled (CPython gh-98401), 3.10 and 3.11 + a DeprecationWarning, so the test records both; the language + reference still documents it as a warning in 3.15, with a SyntaxError only + in a future Python version. The tool is run per-document from `/apply`, so + the warning lands in the middle of a verification report. Compiling is a + pure `compile()` over the source text - no cache file, no temp file - so the + check costs nothing and covers every `tools/*.py`, guarding the next + docstring that quotes a macro too. + """ + + def test_sources_have_no_invalid_escape_sequences(self): + import warnings + + tools_dir = Path(verify_layout.__file__).resolve().parent + sources = sorted(tools_dir.glob("*.py")) + self.assertIn(Path(verify_layout.__file__).resolve(), sources) + offenders: dict[str, list[str]] = {} + for source in sources: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + compile(source.read_text(encoding="utf-8"), str(source), "exec") + syntax = [ + str(w.message) + for w in caught + if issubclass(w.category, (SyntaxWarning, DeprecationWarning)) + ] + if syntax: + offenders[source.name] = syntax + self.assertEqual({}, offenders, "tools/*.py warn about invalid escapes when compiled") + + if __name__ == "__main__": unittest.main() diff --git a/tools/verify_layout.py b/tools/verify_layout.py index b8eb34aa..d43e097c 100644 --- a/tools/verify_layout.py +++ b/tools/verify_layout.py @@ -32,7 +32,7 @@ than inventing a layout failure. A broken extractor has two distinct causes: an all, and Poppler 26.0x before 26.05 aborts `-bbox`/`-bbox-layout`/`-htmlmeta` on a PDF whose Info dictionary carries an empty string in any field - which `hyperref` writes for every field it does not set, so any `lualatex`/`pdflatex` document built with `hyperref` -and no `\hypersetup{pdftitle=...}` triggers a real Poppler crashing on a legal PDF (#451). +and no `\\hypersetup{pdftitle=...}` triggers a real Poppler crashing on a legal PDF (#451). Line height serves as a font-size proxy to spot section headings; left edge (xMin) separates bullet lines from entry headers.