Skip to content

Fix CI and CD: twine metadata, mypy/numpy/python version clash - #15

Merged
AlecThomson merged 7 commits into
mainfrom
claude/prs-cicd-failures-bfomu2
Sep 22, 2026
Merged

AlecThomson merged 7 commits into
mainfrom
claude/prs-cicd-failures-bfomu2

Conversation

@AlecThomson

@AlecThomson AlecThomson commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes the red CI and CD on main, which was also blocking #8, #13 and #14.

main's last CI run was 2026-05-22, so it looked green but was stale. Fresh
workflow_dispatch runs on main today confirmed both workflows fail
(CI,
CD).

CD — Distribution build

twine check --strict failed with:

InvalidDistribution: Invalid distribution metadata: '2.5' is not a valid metadata version

hatchling emits Metadata-Version: 2.5, which only Twine 7 understands.
build-and-inspect-python-package picked up Twine 7 in v3.0.0 — but v3 also
stopped force-tagging minor releases, so the floating @v2 tag we were on could
never receive the fix. This one was not going to heal on its own.

Pinned to @v3.0.1.

CI — Format

mypy failed while parsing numpy's own stubs:

numpy/__init__.pyi:737: error: Type statement is only supported in Python 3.12 and greater  [syntax]

The Format job runs on 3.x (3.14), where numpy resolves to 2.5.x, whose stubs
use PEP 695 type statements. mypy was targeting 3.10 and refused to parse them.

This is the numpy / mypy / python version clash from #14. The root of it is that
requires-python was raised to >=3.11 in e9fba2e ("Add the codes") when the
real code landed, but three leftovers from the scientific-python/cookie template
were never updated with it:

  • Programming Language :: Python :: 3.10 classifier
  • [tool.mypy] python_version = "3.10"
  • CI matrix "3.10" — and 3.11 was never in the matrix at all

3.10 is not installable anyway: numpy, astropy, scipy, reproject and capn-crunch
all require >=3.11.

So rather than raising the mypy target to 3.12, this keeps the type check honest
at the project's real minimum:

  • python_version = "3.11"
  • mypy hook pinned via language_version: python3.11, which resolves numpy to
    the 2.4.x line whose stubs parse at that target
  • the Format job installs 3.11 alongside 3.x so the hook can find it (3.x
    stays last and remains the interpreter pre-commit itself runs on)
  • matrix is now ["3.11", "3.12", "3.13", "3.14"], so the minimum supported
    version is actually exercised for the first time
  • 3.10 classifier dropped

A real bug this uncovered

Type-checking against a supported Python surfaced a genuine bug that the 3.10
target had been masking:

masking.py:910: error: Argument "mask" to "beam_shape_erode" has incompatible type
ndarray[..., dtype[numpy.bool]]; expected ndarray[..., dtype[floating]]

reverse_negative_flood_fill returns a bool array. It was passed to
beam_shape_erode (which expects floats) and then handed to fits.writeto,
where astropy rejects it. That is exactly the failure Beth reported via #14.

Fixed by casting the flood-fill mask to float32 where it is created, so both
the erosion step and the FITS write get a float array, and dropping the
now-redundant type: ignore[assignment].

Because beam_shape_erode derives its return dtype from its input, fixing the
dtype at the source also removes the bool return it could previously produce —
so its return-dtype docstring, which was wrong in both branches it described,
is corrected here too.

This overlaps #14, which applied .astype(np.float32) at the writeto call.
That cast is kept on #14: it is no longer load-bearing for the bool bug, but the
multi-scale path of beam_shape_erode still returns float64, so it is what keeps
the mask on disk at float32.

Test coverage

codecov flagged 0% patch coverage, and it was right twice over — both gaps sat
directly on the path of this bug:

  • every existing mask test ran with flood_fill=False, so nothing exercised the
    branch that built the bool array
  • nothing called create_snr_mask_from_fits with beam_shape_erode=True, so the
    one place the bool mask was passed into a float-typed parameter was never run

Two tests added for those paths. The first was verified to fail without the fix,
raising the same KeyError from astropy's image HDU on write that Beth hit.

Verification

  • pre-commit run --hook-stage manual --all-files — all hooks pass locally
  • pytest — 22 passed
  • mypy reproduced against a Python 3.14 / numpy 2.5.3 environment to confirm both
    the CI failure and the fix, not just the local 3.11 / numpy 2.4.6 one
  • all 8 checks green on the current head, codecov included

Once this lands, #13 and #8 need only a branch update to go green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GTPJweMZavwh86CQ7qH7rb

Three related failures were making CI and CD red on main and on every
open PR.

CD: the "Distribution build" job failed on `twine check --strict` with
"Invalid distribution metadata: '2.5' is not a valid metadata version".
hatchling emits Metadata-Version 2.5, which only Twine 7 understands.
build-and-inspect-python-package gained Twine 7 in v3.0.0, but v3 also
stopped force-tagging minor releases, so the floating `@v2` tag could
never pick the fix up. Pin to v3.0.1.

CI: the "Format" job failed in mypy while parsing numpy's own stubs:
"Type statement is only supported in Python 3.12 and greater". The
Format job runs on Python 3.14, where numpy resolves to 2.5.x, whose
stubs use PEP 695 `type` statements. mypy was targeting 3.10, so it
refused to parse them. numpy 2.5 itself requires Python >=3.12, so the
target has to be at least 3.12 for its stubs to parse.

Align the stale 3.10 references, which sat below the project's own
`requires-python = ">=3.11"`:
  - mypy python_version 3.10 -> 3.12
  - test matrix 3.10 -> 3.11, so the minimum supported version is
    actually exercised (it previously was not tested at all)
  - drop the 3.10 classifier

Raising the mypy target uncovered a real bug that the 3.10 target had
been masking: `reverse_negative_flood_fill` returns a bool array, which
was passed to `beam_shape_erode` (which expects floats) and then handed
to `fits.writeto`, where astropy rejects it. This is the same failure
Beth reported via #14. Cast the flood-fill mask to float32 at the point
it is created, so both the erosion step and the FITS write get a float
array, and drop the now-redundant `type: ignore[assignment]`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTPJweMZavwh86CQ7qH7rb
Target mypy at the project's actual minimum (3.11) rather than 3.12.

numpy 2.5 requires Python >=3.12 and its stubs use PEP 695 `type`
statements, which mypy refuses to parse while targeting anything below
3.12. Pinning the hook's environment to 3.11 resolves numpy to the
2.4.x line, whose stubs parse cleanly at that target, so the type
check now matches `requires-python = ">=3.11"` instead of silently
skipping over it.

The Format job installs 3.11 alongside 3.x so the hook can find it;
3.x stays last and remains the interpreter pre-commit itself runs on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTPJweMZavwh86CQ7qH7rb
@codecov

codecov Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

AlecThomson pushed a commit that referenced this pull request Sep 21, 2026
Per review on #14:

Revert .pre-commit-config.yaml to the version on main. The copied flint
config regressed several things here: it pointed mypy at `files: flint|tests`
(the wrong package for this repo, so mypy checked nothing), dropped the
`exclude` for the copier answers file, downgraded blacken-docs to black 24,
commented out the prettier and pygrep hooks, renamed `ruff-check` to the
deprecated `ruff` alias, and dropped the numpy/capn-crunch/pytest-stub
dependencies that mypy needs to resolve this package's own types.

The underlying numpy / mypy / python version clash is addressed explicitly
instead, via the CI fixes merged in from #15: mypy targets 3.11 (the project's
real minimum) with the hook pinned to a 3.11 environment, so numpy resolves to
the 2.4.x stubs that parse at that target.

Annotate CustomFormatter.format, which mypy requires now that it actually
checks this package (`disallow_untyped_defs` is on for eye_patch.*).

The bool mask fix is now applied at the point the flood-fill mask is created
rather than only at `fits.writeto`, which also fixes the type error passing it
into `beam_shape_erode`. The `.astype(np.float32)` at the write is kept: it is
no longer load-bearing for the bool bug, but `beam_shape_erode` still returns
float64 on the multi-scale path, so the cast keeps the mask on disk at float32.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTPJweMZavwh86CQ7qH7rb
The docstring claimed a bool return for no/single scale and int32 otherwise.
Neither held: the single-scale path casts to the input dtype, and the
multi-scale path returns the float64 array it accumulates into. Now that the
mask is float from the point it is created, the bool return is gone entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTPJweMZavwh86CQ7qH7rb
codecov flagged 0% patch coverage, which turned out to be pointing at a
real gap rather than a threshold quirk: every existing mask test runs with
`flood_fill=False`, so nothing exercised the branch that produced the bool
array. That is why this shipped.

Covers the path end to end and asserts the written mask is floating point.
Verified it fails without the fix, raising the same KeyError from
astropy's image HDU while writing the mask that Beth hit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTPJweMZavwh86CQ7qH7rb
codecov was still reporting 0% patch coverage. The remaining uncovered
changed line was the `beam_shape_erode` call itself: no test ran
`create_snr_mask_from_fits` with `beam_shape_erode=True`, so the one place
the bool mask was actually passed into a float-typed parameter, which is
what mypy flagged, went unexercised.

Adds a fixture whose image carries the beam keywords, and a test running
flood fill and beam erosion together through the public entry point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTPJweMZavwh86CQ7qH7rb
The two tests added here were unannotated. mypy did not catch it because
disallow_untyped_defs is only switched on for eye_patch.*, not tests.

The new fixture takes `tmp_path` rather than `tmpdir`, since the latter is
`py.path.local` and the surrounding fixture was converting it to a Path
anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTPJweMZavwh86CQ7qH7rb
@AlecThomson
AlecThomson merged commit 6fdf055 into main Sep 22, 2026
8 checks passed
@AlecThomson
AlecThomson deleted the claude/prs-cicd-failures-bfomu2 branch September 22, 2026 02:34
AlecThomson pushed a commit that referenced this pull request Sep 22, 2026
main now carries the CI/CD fix (#15), the actions bump (#13) and the
pre-commit hook bumps (#8), so this branch needed bringing up to date.

The only conflict was in tests/test_masking.py, where this branch held an
earlier iteration of the flood-fill tests that landed on main via #15.
Resolved in favour of main's version, which is a strict superset: the same
test, now annotated, plus the fixture and beam-erosion test added there.
Nothing from this branch was lost; the PR never touched tests itself.

What remains here is the original contribution: the logging sub-module,
the float32 cast at fits.writeto, and the kernel-too-small ValueError.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTPJweMZavwh86CQ7qH7rb
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.

2 participants