chore: lower requires-python to >=3.11 — the platform floor outlier (#443) - #444
Conversation
…443) views-datafactory was the only repo on the platform above 3.11, and the views-models conda envs run 3.11.14/3.11.15, so 28 requirements files could not install it at all. ADR-030 set >=3.12 because "tifffile's current releases require it" — a fact about a vendor at a moment, not an architectural need. 3.11 was never considered there; the string does not appear in the ADR. Verified at both ends, not one: pytest exit 0 on 3.11.13 and 3.13.7, ruff and mypy clean on both, same six pre-existing xfails. THE COST IS REAL. uv.lock now forks: 3.11 resolves tifffile 2026.3.3 / imagecodecs 2026.3.6, >=3.12 keeps 2026.5.15 / 2026.5.10. Both upstreams dropped 3.11 permanently. The required `test` job pins the floor, so it decodes rasters with a different codec build than the server — registered as C-347, with `test-py313` covering the production line until it can be made a required check. No strategy.matrix: it renames the required `test` context and deadlocks every PR under enforce_admins. Three things this turned up that were not predicted: 1. test_dependency_floors._locked_versions collapsed duplicate package names via a dict comprehension. That path was flagged in review on #430, scored 25, and CORRECTLY refuted — on the premise that this project had a single requires-python and no markers. This change deletes that premise. A refutation is only as durable as its premise, and nothing was watching the premise. Drilled: on the identical forked lock the new parser fails naming both resolutions and the old one passes. 2. No test in this repository had ever written a compressed TIFF. imagecodecs decodes 100% of production GHS-POP/GHS-BUILT-S rasters, is imported by nothing, and blocking it raises KeyError "<COMPRESSION .LZW: 5> requires the 'imagecodecs' package" while every existing test keeps passing. An import-graph audit would have called it removable. Now covered. 3. Pinning mypy to the floor made it parse tifffile at 3.11 too, so it died on tifffile's own PEP 695 syntax under 3.13 while passing under 3.11. Then the fix for that was written inside [tool.mypy], where a [[overrides]] header silently captured disallow_any_generics and took mypy from clean to 98 errors. The TOML parsed and meant something else. ADR-030 amended, not superseded — the tooling decision is untouched. The Hetzner guide's "Install Python 3.10+" is rewritten: it produced an uninstallable environment for three months, and that number now selects which raster decoder the server installs. C-348 records that nothing asserts the server's interpreter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/code-review found seven, /review-diff one more. Three were defects in the change's own reasoning, not in the code it touched. REVERTED python_version = "3.11" from mypy. It was added so the declared floor would be checked at the keyboard, but mypy then parses third-party source at 3.11 too and dies on tifffile's PEP 695 under 3.13. The escape — follow_imports = "skip" for tifffile — silently deleted type checking of read_geotiff, the ONLY tifffile call site in this repo. Drilled both ways: with the skip, page.asarray(maxworkerz=1) type-checks clean; without it, mypy names the typo. A convenience CI already provides is not worth a real check. Reverting also removed the [[overrides]] table that, written mid-section, had captured disallow_any_generics and taken mypy from clean to 98 errors. The CI-pin guard overclaimed. Its docstring and the PR body said it would have caught the guide-vs-pyproject drift; checked against 12d5afa, it would have been GREEN the whole window, because CI and pyproject agreed and only the prose disagreed. Docstring rewritten to say what it does not cover; the residual gap is registered as C-349. The guard was also too narrow and then too broad. Widened from ci.yml to every workflow plus a --python flag check, since publish_package.yml builds the artefact consumers actually get. That version then reddened on a legitimate --python 3.13 inside test-py313 and on a version named in a comment — C-320, introduced into a change citing C-320 three times. Now parses YAML, exempts the off-floor job, strips comments; re-drilled all three ways. ADR-030's Implementation Notes still said "Bump requires-python to >=3.12" and Open Question 4 was still open, both after the amendment. An implementer reading only that section would have reversed this change. Inline superseded flags added rather than editing the original. Also: uv sync --frozen was a false guarantee in test-py313, since bare uv run re-locks by default — now uv run --frozen. The deployment guide stopped installing python3 while three later steps still invoked it — now uv run python. And the non-floor coverage guard accepted import-enforcement's single-file pytest — now requires a bare full suite. pytest exit 0, ruff clean, mypy clean at 3.11 and 3.13, validate_docs PASSED, register 349 IDs / 42 open / tier sum 42. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ritual complete — 8 findings, all fixed and re-drilled
Three were defects in this change's own reasoning: Reverted The pin guard overclaimed, then was too narrow, then too broad. Its docstring and this PR's body said it would have caught the guide-vs-pyproject drift; checked against ADR-030 still told you to undo this. Implementation Notes step 1 read "Bump Also: Verification
Required check names confirmed reporting under their exact bare names — Correction to this PR's own bodyIt says the views-models unblocking in the present tense. It is not live: |
MINOR, not patch: the supported environment set GREW. requires-python moved >=3.12 -> >=3.11 in #444, and nothing reaches anyone until a release, because PyPI metadata is built from pyproject at release time. Until this tag exists the 28 views-models requirements files stay blocked on a floor this repository no longer declares. src/ is untouched — the wheel is byte-identical to v1.11.0 apart from metadata, same as v1.11.0 was to v1.10.0. Also carried by this release, both sitting in development since #431/#432 and in no tag, therefore not on the server: C-331 (the heartbeat URL off the curl command line) and C-343 (the refresh_pipeline.sh header that used to say deploying was one step). The product plan gained a v1.12 addendum rather than a retitled header. TestF7ProductPlanCurrency fired on the bump because the title read "current through v1.11", and editing just the title would have been the cheapest green path rather than the honest one — the plan's own convention is an addendum per release. It records why the floor was never chosen, the permanently forked raster stack, the LZW hole the change exposed rather than caused, and C-347/348/349. pytest exit 0, ruff clean, mypy clean, uv lock --check clean, validate_docs PASSED. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #443.
views-datafactorywas the only repo on the platform above 3.11. The views-models conda envs run 3.11.14 / 3.11.15, sopip install views-datafactoryfailed in all four and 28 requirements files could not use it. ADR-030 set>=3.12because "tifffile's current releases require it" — a fact about a vendor at a moment, not an architectural need. 3.11 was never considered there; the string does not appear in the ADR, and its supporting evidence ("no 3.10-specific patterns") assesses a version nobody asked about.Zero changes under
src/.Verified at both ends, not one
pytestruff check .mypy src/tifffile 2026.3.3/imagecodecs 2026.3.6tifffile 2026.5.15/imagecodecs 2026.5.10Same six pre-existing xfails on both.
uv lock --checkclean,validate_docs.shPASSED.The cost, stated rather than buried
uv.lockis now multi-version — 55 → 57 packages, exactly two forks, no maximum regressed. Both upstream drops are permanent: tifffile adopted PEP 695 at 2026.4.11 (newer releasesSyntaxErroron 3.11), imagecodecs shipscp312-abi3only from 2026.5.10.So the required
testjob pins the floor and therefore decodes rasters with a different codec build than the server.test-py313covers the production line but is not required — a required check that has never reported blocks every merge. That residual is C-347, with an explicit closure condition: make it required on both branches once it has reported.No
strategy.matrix. It renames the reported context totest (3.11); branch protection requires the bare nametestwithenforce_admins: true, so the required check would never report and every PR would wait forever.Three things this turned up that the plan did not predict
1. A refutation expired.
_locked_versions()collapsed duplicate package names via a dict comprehension. That exact path was flagged in/code-reviewon #430, scored 25, and correctly refuted — on the premise that this project had a singlerequires-pythonand no markers. This change deletes that premise.Drilled on the identical forked lock:
That pass is the bug —
uvwrites ascending, the dict kept2026.5.15, and that did not equal the floor. A refutation is only as durable as its premise, and nothing was watching the premise.2. No test in this repo had ever written a compressed TIFF.
imagecodecsdecodes 100% of production GHS-POP and GHS-BUILT-S rasters, is imported by nothing undersrc/, and is reached only implicitly throughread_geotiff'spage.asarray(). Blocking the import:Every existing test uses the uncompressed path, so an import-graph audit would have called the dependency removable and removing it would have left the suite green. Now covered.
3. The fork bit within the hour — twice. Pinning mypy to the floor made it parse third-party source at 3.11, so it died on tifffile's own PEP 695 under 3.13 (
tifffile.py:929: Type statement is only supported in Python 3.12 and greater) while passing under 3.11. A single-ended check would have shipped that. Then the fix for it was written inside[tool.mypy], where a[[overrides]]header silently captureddisallow_any_genericsand took mypy from clean to 98 errors — TOML that parsed perfectly and meant something else. Same shape as the duplicaterun:key in #424: a parse is not a verification.Drills — six, all fired
ALLOWED_AT_FLOORentryimagecodecsblockedtest-py313deletedPlus: a deliberate type error in
raster_io.pystill surfaces through the tifffile override, so the skip does not mask our own code.Also in here
tests/test_ci_gates.py— CI's interpreter pins must equalrequires-python, and something must test a non-floor interpreter. The first would have caught the drift where the deploy guide said 3.10 and pyproject said 3.12 for three months.hetzner_deployment_guide.mdsaidInstall Python 3.10+under a>=3.12floor — an instruction producing an uninstallable environment. Rewritten to name an interpreter, because that number now selects which raster decoder the server installs. C-348 records that nothing asserts it.release-topology.ymlhad no interpreter pin at all; added.Before merging
Check the PR's check list against the required list:
lint,typecheck,test,docs(+import-enforcementonmain) must all still report under those exact names, andtest-py313should appear as a new, non-blocking one.🤖 Generated with Claude Code