Skip to content

Commit 7e58df0

Browse files
d-v-bclaudemaxrjones
authored
chore: run mypy from developer environment (zarr-developers#3972)
* chore: ignore docs/superpowers/ scratch directory * chore: pin python 3.12 on hatch dev env * chore: run mypy from hatch dev env, drop mirrors-mypy hook Replace the pre-commit/mirrors-mypy hook (which maintained its own duplicate dep list) with a `repo: local` hook that runs `hatch run dev:mypy`. The dev hatch env's `dev` group (resolved via uv.lock) becomes the single source of truth for mypy's dependency set. This also unpins numpy from the type-check environment (it was hard-pinned to `numpy==2.1` in the old hook); type fixes that follow keep mypy clean against current numpy stubs: - relax NDArrayLike.reshape/all signatures so np.ndarray structurally satisfies the protocol - widen AsyncGroup.require_array's `dtype` to include None - add narrowly-scoped `# type: ignore` comments with explanatory notes where numpy 2.x stubs are too strict against runtime-valid calls (datetime64 unit f-strings, 'generic' unit sentinel, newbyteorder subclass identity, ZDTypeLike None handling) - drop stale `# type: ignore` comments that are no longer needed * ci: install hatch in lint workflow so mypy hook can run * docs: changelog for mypy-in-dev-env change * refactor: resolve None dtype at create() boundary `create()` accepts `dtype=None` (legacy v2 behavior: an unspecified dtype defaults to float64). Previously this `None` was forwarded untyped into `_create`, which doesn't accept `None` — it only worked because `parse_dtype(None)` -> `np.dtype(None)` happens to resolve to float64. That required a `cast()` to silence mypy. Resolve `None` to `"float64"` explicitly in `create()` before forwarding, so the value passed to `_create` is a real dtype and the cast is no longer needed. No behavior change. * refactor: give NDArrayLike.reshape/all precise signatures The initial fix for numpy-stub conformance widened the NDArrayLike protocol's `reshape` and `all` to `(*args: Any, **kwargs: Any) -> Any`, which erased type information for every consumer of the protocol. Replace with precise signatures that np.ndarray still satisfies structurally: - `reshape(shape: tuple[int, ...], /, *, order=..., copy=...) -> NDArrayLike` — the `Literal[-1]` form was the only thing blocking a precise signature (it straddles numpy's arity-split overloads); it is unused on protocol-typed values, so drop it. `NDBuffer.reshape` keeps its public `-1` support by normalizing `-1` to `(-1,)` before forwarding. - `all(self) -> np.bool_` — the sole caller wraps the result in `bool(...)`, and no-arg is all we use. * chore: remove gitignore for claude docs * chore: restore uv.lock uv.lock was removed in zarr-developers#3962 as unused. The mypy-via-hatch change in this branch makes it load-bearing again: it is the single source of truth that keeps the `dev` hatch environment (and therefore mypy's results) consistent across developer machines and CI. Restore it, regenerated against the current pyproject.toml. * docs: rename changelog entry to PR zarr-developers#3972 * ci: skip mypy hook on pre-commit.ci The mypy hook is now `language: system` and shells out to `hatch run dev:mypy`, which needs the project's hatch dev environment. pre-commit.ci's hosted runners don't have it, so the hook can only fail there. Add it to `ci.skip`; mypy is still covered by the Lint GitHub Actions workflow (which installs hatch) and by local prek runs. * refactor: apply review nitpicks from PR zarr-developers#3972 - Inline the float64 dtype default into the `_create` call instead of reassigning the `dtype` variable. - Move the numpy 2.x stub explanation onto its own line above the code so `# type: ignore` comment lines stay short. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: run mypy via `uv run` so the lockfile is actually honored `hatch run dev:mypy` does not consume `uv.lock` — hatch has no lockfile support and re-resolves the `dev` dependency group from scratch each time it builds the environment. This defeated the PR's goal of a reproducible type-checking environment: contributors with stale or differently-resolved hatch `dev` envs saw different mypy results (e.g. errors from an older `tomlkit` whose `TOMLDocument.__getitem__` was typed `Item | Container` rather than `Any`). Switch the mypy pre-commit hook and the Lint workflow to `uv run --frozen mypy`. `uv` does sync from `uv.lock`, so the committed lockfile becomes the real single source of truth for mypy's dependency set, identical for every contributor and for CI. - .pre-commit-config.yaml: hook entry `hatch run dev:mypy` -> `uv run --frozen mypy` - .github/workflows/lint.yml: install `uv` instead of `hatch` - pyproject.toml / changes: update wording to match Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Apply suggestion from @maxrjones Co-authored-by: Max Jones <14077947+maxrjones@users.noreply.github.com> * fix: test dtype is None exactly, not via falsy collapse `dtype or "float64"` substitutes the default for any falsy input — empty string, 0, empty Mapping — not just None. Those wouldn't pass ZDTypeLike validation anyway, but the failure mode was "silent substitution to float64" instead of "raise on invalid input". Use an exact `is None` check expressed as a conditional expression. * chore: add .python-version pinning default to 3.12 uv reads `.python-version` to decide which interpreter to use for `uv venv` / `uv sync` / `uv run`. With the mypy hook now running as `uv run --frozen mypy`, pinning the interpreter here keeps the dev env consistent across developer machines — matching the existing `[tool.mypy].python_version = "3.12"` and `requires-python = ">=3.12"` declarations. `.python-version` is not consumed by hatch (its envs declare their own Python via `[tool.hatch.envs.*].python`), so the test matrix (py3.12/3.13/3.14) is unaffected. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Max Jones <14077947+maxrjones@users.noreply.github.com>
1 parent e815b51 commit 7e58df0

17 files changed

Lines changed: 4223 additions & 45 deletions

File tree

.github/workflows/lint.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,12 @@ jobs:
2222
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2323
with:
2424
persist-credentials: false
25+
- name: Set up Python
26+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
27+
with:
28+
python-version: "3.12"
29+
- name: Install uv
30+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
31+
with:
32+
enable-cache: true
2533
- uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3

.pre-commit-config.yaml

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ ci:
22
autoupdate_commit_msg: "chore: update pre-commit hooks"
33
autoupdate_schedule: "monthly"
44
autofix_prs: false
5-
skip: [] # pre-commit.ci only checks for updates, prek runs hooks locally
5+
# mypy runs as a `language: system` hook via `uv run mypy`, which needs `uv`
6+
# and the repo checkout to resolve the dev environment from `uv.lock` —
7+
# unavailable on pre-commit.ci's runners. It is covered instead by the Lint
8+
# GitHub Actions workflow and by local prek runs.
9+
skip: [mypy]
610

711
default_stages: [pre-commit, pre-push]
812

@@ -27,25 +31,15 @@ repos:
2731
- id: check-yaml
2832
exclude: mkdocs.yml
2933
- id: trailing-whitespace
30-
- repo: https://github.com/pre-commit/mirrors-mypy
31-
rev: v1.20.2
34+
- repo: local
3235
hooks:
3336
- id: mypy
34-
files: ^(src|tests)/
35-
additional_dependencies:
36-
# Package dependencies
37-
- packaging
38-
- donfig
39-
- numcodecs
40-
- google-crc32c>=1.5
41-
- numpy==2.1 # https://github.com/zarr-developers/zarr-python/issues/3780 + https://github.com/zarr-developers/zarr-python/issues/3688
42-
- typing_extensions
43-
- universal-pathlib
44-
- obstore>=0.5.1
45-
# Tests
46-
- pytest
47-
- hypothesis
48-
- s3fs
37+
name: mypy
38+
language: system
39+
entry: uv run --frozen mypy
40+
pass_filenames: false
41+
always_run: true
42+
types_or: [python, pyi]
4943
- repo: https://github.com/scientific-python/cookie
5044
rev: 2026.04.04
5145
hooks:

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

changes/3972.misc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Run `mypy` via `uv run mypy` instead of `pre-commit`'s isolated venv. The `dev` dependency group in `pyproject.toml`, locked by `uv.lock`, is now the single source of truth for `mypy`'s dependency set, eliminating the duplicate dependency list previously maintained in `.pre-commit-config.yaml` and giving every contributor and CI an identical, reproducible type-checking environment.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ maintainers = [
3030
{ name = "Deepak Cherian" }
3131
]
3232
requires-python = ">=3.12"
33-
# If you add a new dependency here, please also add it to .pre-commit-config.yaml
3433
dependencies = [
3534
'packaging>=22.0',
3635
'numpy>=2',
@@ -444,6 +443,7 @@ markers = [
444443
[tool.repo-review]
445444
ignore = [
446445
"PC111", # fix Python code in documentation - enable later
446+
"PC140", # we run mypy via `uv run mypy`, not via mirrors-mypy
447447
"PC170", # use PyGrep hooks - no *.rst files to check
448448
"PC180", # for JavaScript - not interested
449449
"PC902", # pre-commit.ci custom autofix message - not using autofix

src/zarr/_compat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def _reshape_view(arr: "NDArray[Any]", shape: tuple[int, ...]) -> "NDArray[Any]"
102102
If a view cannot be created (the array is not contiguous) on NumPy >= 2.1.
103103
"""
104104
if Version(np.__version__) >= Version("2.1"):
105-
return arr.reshape(shape, copy=False) # type: ignore[call-overload, no-any-return]
105+
return arr.reshape(shape, copy=False)
106106
else:
107107
arr.shape = shape
108108
return arr

src/zarr/api/asynchronous.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1049,7 +1049,8 @@ async def create(
10491049
store_path,
10501050
shape=shape,
10511051
chunks=chunks,
1052-
dtype=dtype,
1052+
# Legacy v2 behavior: an unspecified dtype defaults to float64.
1053+
dtype="float64" if dtype is None else dtype,
10531054
compressor=compressor,
10541055
fill_value=fill_value,
10551056
overwrite=overwrite,

src/zarr/codecs/cast_value.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,11 +300,11 @@ def _validate_scalar_map(
300300

301301
def _do_cast(
302302
self,
303-
arr: np.ndarray, # type: ignore[type-arg]
303+
arr: np.ndarray,
304304
*,
305-
target_dtype: np.dtype, # type: ignore[type-arg]
305+
target_dtype: np.dtype,
306306
scalar_map: Mapping[str | float | int, str | float | int] | None,
307-
) -> np.ndarray: # type: ignore[type-arg]
307+
) -> np.ndarray:
308308
if not _HAS_RUST_BACKEND:
309309
raise ImportError(
310310
"The cast_value codec requires the 'cast-value-rs' package. "

src/zarr/core/buffer/core.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,13 @@ def __setitem__(self, key: slice, value: Any) -> None: ...
7171
def __array__(self) -> npt.NDArray[Any]: ...
7272

7373
def reshape(
74-
self, shape: tuple[int, ...] | Literal[-1], *, order: Literal["A", "C", "F"] = ...
75-
) -> Self: ...
74+
self,
75+
shape: tuple[int, ...],
76+
/,
77+
*,
78+
order: Literal["A", "C", "F"] | None = ...,
79+
copy: bool | None = ...,
80+
) -> NDArrayLike: ...
7681

7782
def view(self, dtype: npt.DTypeLike) -> Self: ...
7883

@@ -92,7 +97,7 @@ def transpose(self, axes: SupportsIndex | Sequence[SupportsIndex] | None) -> Sel
9297

9398
def ravel(self, order: Literal["K", "A", "C", "F"] = ...) -> Self: ...
9499

95-
def all(self) -> bool: ...
100+
def all(self) -> np.bool_: ...
96101

97102
def __eq__(self, other: object) -> Self: # type: ignore[override]
98103
"""Element-wise equal
@@ -502,7 +507,10 @@ def byteorder(self) -> Endian:
502507
return Endian(sys.byteorder)
503508

504509
def reshape(self, newshape: tuple[int, ...] | Literal[-1]) -> Self:
505-
return self.__class__(self._data.reshape(newshape))
510+
# numpy accepts a bare -1, but the NDArrayLike protocol only types the
511+
# tuple form; normalize so the forwarded value matches the protocol.
512+
shape = (newshape,) if newshape == -1 else newshape
513+
return self.__class__(self._data.reshape(shape))
506514

507515
def squeeze(self, axis: tuple[int, ...]) -> Self:
508516
newshape = tuple(a for i, a in enumerate(self.shape) if i not in axis)

src/zarr/core/dtype/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def parse_dtype(
276276
# First attempt to interpret the input as JSON
277277
if isinstance(dtype_spec, Mapping | str | Sequence):
278278
try:
279-
return get_data_type_from_json(dtype_spec, zarr_format=zarr_format) # type: ignore[arg-type]
279+
return get_data_type_from_json(dtype_spec, zarr_format=zarr_format)
280280
except ValueError:
281281
# no data type matched this JSON-like input
282282
pass

0 commit comments

Comments
 (0)