Skip to content

Lower requires-python to >=3.11 — the platform floor outlier (ADR-030 amendment) #443

Description

@Polichinel

Why

views-datafactory declares requires-python = ">=3.12" and is the only repo on the platform above 3.11:

Repo Declared
views-datafactory >=3.12 ← the outlier
views-frames, views-lab00, views-lstm-lab, views-metric-lab >=3.10
views-bayesian >=3.11
views-baseline >=3.11,<3.14
views-crafdapi, views-faoapi, views-hydranet, views-reporting >=3.11,<3.15
views-pipeline-core, views-postprocessing, views-evaluation, views-stepshifter, views-r2darts2 >=3.11,<3.15

The views-models conda envs run 3.11.14 / 3.11.15 (postprocessing, ensemble, baseline, hydranet), so pip install views-datafactory fails in all four — and 28 views-models requirements files reference this package. That is the named consumer justifying this change. Without one, the right action would be to leave the floor alone and simply record that it was inherited.

The floor is soft

docs/ADRs/030_raster_tooling.md (Accepted, 2026-05-18) is the sole document that decided it. Its reason is a fact about a vendor at a moment, not an architectural need:

Python ≥3.12 version bump (tifffile's current releases require it; assessed as safe for this repo)

3.11 is never mentioned in that ADR — it was not a rejected option, it was never on the table. And the ADR's own supporting evidence assesses the wrong version: "no 3.10-specific patterns in codebase" says nothing about 3.11 or 3.12.

Already verified (before opening this)

  • Suite green under 3.11.13pytest exit 0, same six pre-existing xfails, no new failures
  • ruff at py311 clean · mypy src/ clean · compileall clean
  • No 3.12-only syntax or runtime API anywhere: no PEP 695, itertools.batched, @override, Path.walk, sys.monitoring
  • Dependencies resolve at 3.11

The cost, stated plainly

uv.lock becomes multi-version. Under 3.11 it resolves tifffile 2026.3.3 / imagecodecs 2026.3.6; under ≥3.12 it keeps 2026.5.15 / 2026.5.10. Both upstream drops are real and permanent:

  • tifffile dropped 3.11 at 2026.4.11 and now uses PEP 695 syntax — it would SyntaxError on 3.11, not degrade
  • imagecodecs dropped it at 2026.5.10 and ships cp312-abi3 wheels only

3.11-compatible releases with real wheels do exist, so the floor is installable — just pinned to the March-2026 raster line for 3.11 consumers.

The finding that reorders the work

imagecodecs is imported by nothing in src/. It is tifffile's LZW decoder, reached implicitly through read_geotiff's page.asarray() call, and every GHS-POP and GHS-BUILT-S GeoTIFF from JRC is LZW-compressed. tifffile has no pure-Python LZW fallback — COMPRESSION 5 dispatches unconditionally to imagecodecs.lzw_decode.

No test in this repository has ever written a compressed TIFF. Every imwrite in the suite is uncompressed — verified. So the load-bearing production decoder has zero coverage on any interpreter, and has since ADR-030.

This change does not create that hole. It makes it visible, because for the first time there are two candidate decoders and a reason to ask which one we tested. The LZW test must land before the 3.13 CI job, or that job compares two uncompressed code paths and proves nothing.

Hard constraint — no matrix

Branch protection on both main and development requires status checks by bare job name (lint, typecheck, test, docs; main also import-enforcement) with enforce_admins: true. A strategy.matrix renames the reported check to test (3.11), so the required test never reports and every PR deadlocks forever. Adding new job names is safe — they are not required, hence non-blocking.

Scope

  1. pyproject.toml — floor, missing 3.11 classifier, ruff py311, add python_version = "3.11" to [tool.mypy] (it has none, so mypy checks against the running 3.13)
  2. uv lock — verify exactly two forks appear, no maximum regresses
  3. tests/test_dependency_floors.py_locked_versions() collapses duplicate names via dict comprehension. Fix to collect all resolutions; strict semantics (any(v == floor))
  4. tests/test_raster_io.py — the first LZW test
  5. .github/workflows/ci.yml — four pins to the floor; new non-required test-py313 job; interpreter assertions, not echoes
  6. tests/test_ci_gates.py — bind CI pins to requires-python; assert something tests a non-floor interpreter
  7. publish_package.yml (5 sites), publishing_to_pypi.md (2), hetzner_deployment_guide.md (1 — rewritten, not renumbered)
  8. ADR-030 amendment, following ADR-051's blockquote convention. Amend, do not supersede — the tooling decision is untouched
  9. Register C-347, C-348

The interesting sub-finding

The _locked_versions() collapse was raised during /code-review on #430, scored 25, and correctly refuted — the changelog records it as "a dict-collision path that this project's single requires-python and marker-free dependencies cannot reach." This change deletes that premise.

A refutation is only as durable as its premise, and nothing was watching the premise.

Follow-up that must not be dropped

test-py313 ships non-required (a required check that has never reported blocks all merges). Once it has reported on both development and main, add it to both required-check lists. That is a GitHub settings action and it is the only thing that closes C-347. A workaround whose enforcement lives in a settings page is weaker than the thing it replaces.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions