Skip to content

chore: lower requires-python to >=3.11 — the platform floor outlier (#443) - #444

Merged
Polichinel merged 2 commits into
developmentfrom
chore/python-floor-311
Aug 13, 2026
Merged

chore: lower requires-python to >=3.11 — the platform floor outlier (#443)#444
Polichinel merged 2 commits into
developmentfrom
chore/python-floor-311

Conversation

@Polichinel

Copy link
Copy Markdown
Contributor

Closes #443.

views-datafactory was the only repo on the platform above 3.11. The views-models conda envs run 3.11.14 / 3.11.15, so pip install views-datafactory failed in all four and 28 requirements files could not use it. 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, 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

3.11.13 3.13.7
pytest exit 0 exit 0
ruff check . pass pass
mypy src/ 89 files, clean 89 files, clean
resolves tifffile 2026.3.3 / imagecodecs 2026.3.6 tifffile 2026.5.15 / imagecodecs 2026.5.10

Same six pre-existing xfails on both. uv lock --check clean, validate_docs.sh PASSED.

The cost, stated rather than buried

uv.lock is 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 releases SyntaxError on 3.11), imagecodecs ships cp312-abi3 only from 2026.5.10.

So the required test job pins the floor and therefore decodes rasters with a different codec build than the server. test-py313 covers 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 to test (3.11); branch protection requires the bare name test with enforce_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-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.

Drilled on the identical forked lock:

NEW parser:  AssertionError: ... {'tifffile': 'floor 2026.3.3, resolved 2026.3.3, 2026.5.15'}
OLD parser:  ..                                                              [100%]

That pass is the bug — uv writes ascending, the dict kept 2026.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. imagecodecs decodes 100% of production GHS-POP and GHS-BUILT-S rasters, is imported by nothing under src/, and is reached only implicitly through read_geotiff's page.asarray(). Blocking the import:

LZW write RAISED KeyError: "<COMPRESSION.LZW: 5> requires the 'imagecodecs' package"
uncompressed write/read still fine: (4, 4)

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 captured disallow_any_generics and took mypy from clean to 98 errors — TOML that parsed perfectly and meant something else. Same shape as the duplicate run: key in #424: a parse is not a verification.

Drills — six, all fired

Drill Result
D1 Floor raised to the 3.11 fork's version new parser fails with both resolutions; old parser passes
D2 Parser reverted to the comprehension fixture test reddens
D3 Bogus ALLOWED_AT_FLOOR entry rot test reddens
D4 LZW write with imagecodecs blocked raises; uncompressed still fine
D5 One CI pin flipped to 3.12 floor-pin guard reddens
D6 test-py313 deleted non-floor-job guard reddens

Plus: a deliberate type error in raster_io.py still 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 equal requires-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.
  • ADR-030 amended, not superseded, following ADR-051's blockquote convention. The tooling decision is untouched.
  • hetzner_deployment_guide.md said Install Python 3.10+ under a >=3.12 floor — 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.yml had no interpreter pin at all; added.

Before merging

Check the PR's check list against the required list: lint, typecheck, test, docs (+ import-enforcement on main) must all still report under those exact names, and test-py313 should appear as a new, non-blocking one.

🤖 Generated with Claude Code

Polichinel and others added 2 commits August 13, 2026 05:44
…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>
@Polichinel

Copy link
Copy Markdown
Contributor Author

Ritual complete — 8 findings, all fixed and re-drilled

/code-review medium → 7 · /register-riskC-349 · /review-diff → 1 · /register-risk → 0, loop terminates.

Three were defects in this change's own reasoning:

Reverted python_version = "3.11". It was added so the floor would be checked at the keyboard. 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"silently deleted type checking of read_geotiff, the only tifffile call site in the 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. That revert also removed the [[overrides]] table which — written mid-section — had captured disallow_any_generics and taken mypy from clean to 98 errors.

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 12d5afa, it would have been green the whole window, because CI and pyproject agreed and only the prose disagreed. Docstring corrected; the residual gap is C-349. Widened to all workflows plus a --python flag check — and that version reddened on a legitimate --python 3.13 in 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 with real drift still failing and naming the job.

ADR-030 still told you to undo this. Implementation Notes step 1 read "Bump requires-python to \">=3.12\"" and Open Question 4 was still open — both after the amendment. Inline superseded flags added rather than editing the original.

Also: uv sync --frozen was a false guarantee (bare uv run re-locks) → uv run --frozen; the deployment guide stopped installing python3 while three later steps still invoked it → uv run python; the non-floor coverage guard accepted import-enforcement's single-file pytest → now requires a bare full suite.

Verification

pytest exit 0 · ruff clean · mypy src/ clean at both 3.11 and 3.13 · uv lock --check clean · validate_docs.sh PASSED · register 349 IDs / 42 open / tier sum 42 / header 1931 chars.

Required check names confirmed reporting under their exact bare names — lint, typecheck, test, docs — with test-py313 as a new non-blocking one. No rename, no deadlock.

Correction to this PR's own body

It says the views-models unblocking in the present tense. It is not live: version stays 1.11.0, so PyPI metadata still advertises >=3.12 until a tagged release. Merging here does not deliver #443's purpose — a release does.

@Polichinel
Polichinel enabled auto-merge (squash) August 13, 2026 04:44
@Polichinel
Polichinel merged commit 93dbcb2 into development Aug 13, 2026
6 checks passed
@Polichinel
Polichinel deleted the chore/python-floor-311 branch August 13, 2026 04:55
Polichinel added a commit that referenced this pull request Aug 13, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant