Skip to content

Test across Python 3.9-3.14 in CI - #7

Merged
wrignj08 merged 1 commit into
mainfrom
ci/python-matrix
Aug 14, 2026
Merged

Test across Python 3.9-3.14 in CI#7
wrignj08 merged 1 commit into
mainfrom
ci/python-matrix

Conversation

@wrignj08

Copy link
Copy Markdown
Contributor

Short answer to "should we test more Python versions": yes, and doing it found a bug.

CI ran a single Python 3.11 job while pyproject.toml declares requires-python = ">=3.9" — five of the six supported versions were never exercised.

What the matrix caught immediately

Nothing is pinned (no lockfile is committed), so each Python resolves its own dependency set. That is the point of the matrix here, and it is where the failure was hiding:

Python numpy resolved Lint mypy Tests
3.9 2.0.2 ok ok 573 passed
3.10 2.2.6 ok ok 573 passed
3.11 2.4.6 ok ok 573 passed
3.12 2.5.2 ok FAIL 573 passed
3.13 2.5.2 ok FAIL 573 passed
3.14 2.5.2 ok FAIL 573 passed
multiclean/multiclean.py:79: error: Incompatible types in assignment
(expression has type "None", variable has type "ndarray[tuple[Any, ...], dtype[Any]]")

numpy 2.5's stubs give np.isnan a concrete ndarray return type, so assigning None to that variable afterwards is an error. Python 3.9–3.11 resolve to numpy ≤ 2.4 and never see it — which is exactly why a single-version job missed it.

Fixed by annotating nan_mask: Optional[np.ndarray]. Typing-only, no runtime effect — the test suite passes on all six versions with or without it. No changelog entry for that reason.

Also included, matching OmniWatermask

  • fetch-depth: 0 so setuptools-scm can read tags. Without it CI was installing 0.1.dev1+g<hash> instead of the real version — I verified this against a shallow clone. It is the same silent-version hazard the conda-forge recipe explicitly guards against with its version assertion.
  • Weekly schedule (Sundays 00:00 UTC). With nothing pinned, upstream releases reach users through a fresh resolve whether we watch for them or not. Worth noting: OpenCV 5.0.0 already satisfies the >=4.10 floor and is what every version above resolves to today. The suite passes on it, but that happened without anyone deciding it.
  • workflow_dispatch and a per-version uv cache suffix.
  • fail-fast: false so one version failing does not mask the rest.

Verification

Ran the complete CI sequence locally against all six interpreters — uv sync --all-extras --dev, ruff, mypy, pytest. All green:

3.9   lint ok | mypy ok | 573 passed
3.10  lint ok | mypy ok | 573 passed
3.11  lint ok | mypy ok | 573 passed
3.12  lint ok | mypy ok | 573 passed
3.13  lint ok | mypy ok | 573 passed
3.14  lint ok | mypy ok | 573 passed

Two things left as your call

  • Python 3.9 reached end of life in October 2025. I have tested the range as declared rather than narrowing it, because dropping it is a support decision, not a CI one. If you want to follow OmniWatermask to >=3.10, that is a requires-python bump plus removing one matrix entry.
  • pyproject.toml has no classifiers. OmniWatermask lists Programming Language :: Python :: 3.103.14 and topic/audience classifiers; MultiClean lists none, so PyPI shows no version support at all. Adjacent to this change but not part of it — happy to add them separately.

🤖 Generated with Claude Code

CI ran a single Python 3.11 job while pyproject declares
`requires-python = ">=3.9"`, so five of the six supported versions were
never exercised. Add a matrix over the full declared range, with
fail-fast disabled so one version failing does not hide the others.

The matrix immediately caught a real failure. Nothing is pinned -- no
lockfile is committed -- so each Python resolves its own dependency set,
and 3.12+ picks up numpy 2.5, whose stubs give np.isnan a concrete
ndarray return type. Assigning None to that variable afterwards is an
error mypy only sees on those versions; 3.9 through 3.11 resolve to
numpy <= 2.4 and pass. Annotate nan_mask as Optional[np.ndarray]. This
is a typing-only change with no runtime effect, and the suite passes on
all six versions either way.

Also add, matching OmniWatermask:

- `fetch-depth: 0`, so setuptools-scm can read tags. Without it CI was
  installing version 0.1.dev1+g<hash> rather than the real version --
  the same silent-version hazard the conda-forge recipe guards against.
- A weekly schedule. With nothing pinned, upstream releases reach users
  through a fresh resolve; this surfaces them on our schedule instead of
  theirs. OpenCV 5.0.0 already satisfies our `>=4.10` floor and is what
  every version above resolves to today.
- `workflow_dispatch`, and a per-version uv cache suffix.

Verified locally across all six: lint, mypy and 573 tests pass on each.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wrignj08
wrignj08 merged commit adee68d into main Aug 14, 2026
12 checks passed
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