Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,12 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
- uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
30 changes: 12 additions & 18 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ ci:
autoupdate_commit_msg: "chore: update pre-commit hooks"
autoupdate_schedule: "monthly"
autofix_prs: false
skip: [] # pre-commit.ci only checks for updates, prek runs hooks locally
# mypy runs as a `language: system` hook via `uv run mypy`, which needs `uv`
# and the repo checkout to resolve the dev environment from `uv.lock` —
# unavailable on pre-commit.ci's runners. It is covered instead by the Lint
# GitHub Actions workflow and by local prek runs.
skip: [mypy]

default_stages: [pre-commit, pre-push]

Expand All @@ -27,25 +31,15 @@ repos:
- id: check-yaml
exclude: mkdocs.yml
- id: trailing-whitespace
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.20.2
- repo: local
hooks:
- id: mypy
files: ^(src|tests)/
additional_dependencies:
# Package dependencies
- packaging
- donfig
- numcodecs
- google-crc32c>=1.5
- numpy==2.1 # https://github.com/zarr-developers/zarr-python/issues/3780 + https://github.com/zarr-developers/zarr-python/issues/3688
- typing_extensions
- universal-pathlib
- obstore>=0.5.1
# Tests
- pytest
- hypothesis
- s3fs
name: mypy
language: system
entry: uv run --frozen mypy
pass_filenames: false
always_run: true
types_or: [python, pyi]
- repo: https://github.com/scientific-python/cookie
rev: 2026.04.04
hooks:
Expand Down
1 change: 1 addition & 0 deletions changes/3972.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +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.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ maintainers = [
{ name = "Deepak Cherian" }
]
requires-python = ">=3.12"
# If you add a new dependency here, please also add it to .pre-commit-config.yaml
dependencies = [
'packaging>=22.0',
'numpy>=2',
Expand Down Expand Up @@ -154,6 +153,7 @@ version.source = "vcs"
hooks.vcs.version-file = "src/zarr/_version.py"

[tool.hatch.envs.dev]
python = "3.12"
dependency-groups = ["dev"]

[tool.hatch.envs.test]
Expand Down Expand Up @@ -444,6 +444,7 @@ markers = [
[tool.repo-review]
ignore = [
"PC111", # fix Python code in documentation - enable later
"PC140", # we run mypy via `uv run mypy`, not via mirrors-mypy
"PC170", # use PyGrep hooks - no *.rst files to check
"PC180", # for JavaScript - not interested
"PC902", # pre-commit.ci custom autofix message - not using autofix
Expand Down
2 changes: 1 addition & 1 deletion src/zarr/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def _reshape_view(arr: "NDArray[Any]", shape: tuple[int, ...]) -> "NDArray[Any]"
If a view cannot be created (the array is not contiguous) on NumPy >= 2.1.
"""
if Version(np.__version__) >= Version("2.1"):
return arr.reshape(shape, copy=False) # type: ignore[call-overload, no-any-return]
return arr.reshape(shape, copy=False)
else:
arr.shape = shape
return arr
3 changes: 2 additions & 1 deletion src/zarr/api/asynchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,8 @@ async def create(
store_path,
shape=shape,
chunks=chunks,
dtype=dtype,
# Legacy v2 behavior: an unspecified dtype defaults to float64.
dtype=dtype or "float64",
compressor=compressor,
fill_value=fill_value,
overwrite=overwrite,
Expand Down
6 changes: 3 additions & 3 deletions src/zarr/codecs/cast_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,11 +300,11 @@ def _validate_scalar_map(

def _do_cast(
self,
arr: np.ndarray, # type: ignore[type-arg]
arr: np.ndarray,
*,
target_dtype: np.dtype, # type: ignore[type-arg]
target_dtype: np.dtype,
scalar_map: Mapping[str | float | int, str | float | int] | None,
) -> np.ndarray: # type: ignore[type-arg]
) -> np.ndarray:
if not _HAS_RUST_BACKEND:
raise ImportError(
"The cast_value codec requires the 'cast-value-rs' package. "
Expand Down
16 changes: 12 additions & 4 deletions src/zarr/core/buffer/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,13 @@ def __setitem__(self, key: slice, value: Any) -> None: ...
def __array__(self) -> npt.NDArray[Any]: ...

def reshape(
self, shape: tuple[int, ...] | Literal[-1], *, order: Literal["A", "C", "F"] = ...
) -> Self: ...
self,
shape: tuple[int, ...],
/,
*,
order: Literal["A", "C", "F"] | None = ...,
copy: bool | None = ...,
) -> NDArrayLike: ...

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

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

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

def all(self) -> bool: ...
def all(self) -> np.bool_: ...

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

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

def squeeze(self, axis: tuple[int, ...]) -> Self:
newshape = tuple(a for i, a in enumerate(self.shape) if i not in axis)
Expand Down
2 changes: 1 addition & 1 deletion src/zarr/core/dtype/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def parse_dtype(
# First attempt to interpret the input as JSON
if isinstance(dtype_spec, Mapping | str | Sequence):
try:
return get_data_type_from_json(dtype_spec, zarr_format=zarr_format) # type: ignore[arg-type]
return get_data_type_from_json(dtype_spec, zarr_format=zarr_format)
except ValueError:
# no data type matched this JSON-like input
pass
Expand Down
18 changes: 12 additions & 6 deletions src/zarr/core/dtype/npy/int.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,8 @@ def to_native_dtype(self) -> np.dtypes.Int16DType:
The np.dtype('int16') instance.
"""
byte_order = endianness_to_numpy_str(self.endianness)
return self.dtype_cls().newbyteorder(byte_order)
# numpy 2.x stub: newbyteorder widens to base dtype, runtime preserves the concrete subclass
return self.dtype_cls().newbyteorder(byte_order) # type: ignore[return-value]

@classmethod
def _from_json_v2(cls, data: DTypeJSON) -> Self:
Expand Down Expand Up @@ -762,7 +763,8 @@ def to_native_dtype(self) -> np.dtypes.UInt16DType:
The np.dtype('uint16') instance.
"""
byte_order = endianness_to_numpy_str(self.endianness)
return self.dtype_cls().newbyteorder(byte_order)
# numpy 2.x stub: newbyteorder widens to base dtype, runtime preserves the concrete subclass
return self.dtype_cls().newbyteorder(byte_order) # type: ignore[return-value]

@classmethod
def _from_json_v2(cls, data: DTypeJSON) -> Self:
Expand Down Expand Up @@ -945,7 +947,8 @@ def to_native_dtype(self: Self) -> np.dtypes.Int32DType:
The np.dtype('int32') instance.
"""
byte_order = endianness_to_numpy_str(self.endianness)
return self.dtype_cls().newbyteorder(byte_order)
# numpy 2.x stub: newbyteorder widens to base dtype, runtime preserves the concrete subclass
return self.dtype_cls().newbyteorder(byte_order) # type: ignore[return-value]

@classmethod
def _from_json_v2(cls, data: DTypeJSON) -> Self:
Expand Down Expand Up @@ -1130,7 +1133,8 @@ def to_native_dtype(self) -> np.dtypes.UInt32DType:
The NumPy unsigned 32-bit integer dtype.
"""
byte_order = endianness_to_numpy_str(self.endianness)
return self.dtype_cls().newbyteorder(byte_order)
# numpy 2.x stub: newbyteorder widens to base dtype, runtime preserves the concrete subclass
return self.dtype_cls().newbyteorder(byte_order) # type: ignore[return-value]

@classmethod
def _from_json_v2(cls, data: DTypeJSON) -> Self:
Expand Down Expand Up @@ -1288,7 +1292,8 @@ def to_native_dtype(self) -> np.dtypes.Int64DType:
The NumPy signed 64-bit integer dtype.
"""
byte_order = endianness_to_numpy_str(self.endianness)
return self.dtype_cls().newbyteorder(byte_order)
# numpy 2.x stub: newbyteorder widens to base dtype, runtime preserves the concrete subclass
return self.dtype_cls().newbyteorder(byte_order) # type: ignore[return-value]

@classmethod
def _from_json_v2(cls, data: DTypeJSON) -> Self:
Expand Down Expand Up @@ -1419,7 +1424,8 @@ def to_native_dtype(self) -> np.dtypes.UInt64DType:
The native NumPy dtype.eeeeeeeeeeeeeeeee
"""
byte_order = endianness_to_numpy_str(self.endianness)
return self.dtype_cls().newbyteorder(byte_order)
# numpy 2.x stub: newbyteorder widens to base dtype, runtime preserves the concrete subclass
return self.dtype_cls().newbyteorder(byte_order) # type: ignore[return-value]

@classmethod
def _from_json_v2(cls, data: DTypeJSON) -> Self:
Expand Down
3 changes: 2 additions & 1 deletion src/zarr/core/dtype/npy/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ def to_native_dtype(self) -> np.dtypes.StrDType[int]:
The NumPy data type.
"""
byte_order = endianness_to_numpy_str(self.endianness)
return self.dtype_cls(self.length).newbyteorder(byte_order)
# numpy 2.x stub: newbyteorder widens to base dtype, runtime preserves the concrete subclass
return self.dtype_cls(self.length).newbyteorder(byte_order) # type: ignore[return-value]

@classmethod
def _check_json_v2(cls, data: DTypeJSON) -> TypeGuard[FixedLengthUTF32JSON_V2]:
Expand Down
21 changes: 15 additions & 6 deletions src/zarr/core/dtype/npy/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,9 @@ def _cast_scalar_unchecked(self, data: TimeDeltaLike) -> np.timedelta64:
numpy.timedelta64
The input data cast as a numpy timedelta64 scalar.
"""
return self.to_native_dtype().type(data, f"{self.scale_factor}{self.unit}")
# numpy 2.x stub: timedelta64(scalar, formatted_unit_str) is runtime-valid
# but no overload matches the dynamic f-string unit argument.
return self.to_native_dtype().type(data, f"{self.scale_factor}{self.unit}") # type: ignore[call-overload, no-any-return]

def cast_scalar(self, data: object) -> np.timedelta64:
"""
Expand All @@ -546,7 +548,8 @@ def cast_scalar(self, data: object) -> np.timedelta64:
"""
if self._check_scalar(data):
if isinstance(data, np.timedelta64) and np.isnat(data):
return np.timedelta64("NaT", self.unit)
# numpy 2.x stub: 'generic' is a runtime-valid unit but not in the Literal overload.
return np.timedelta64("NaT", self.unit) # type: ignore[arg-type]
return self._cast_scalar_unchecked(data)
msg = (
f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the "
Expand All @@ -561,7 +564,8 @@ def default_scalar(self) -> np.timedelta64:
This method provides a default value for the timedelta64 scalar, which is
a 'Not-a-Time' (NaT) value.
"""
return np.timedelta64("NaT", self.unit)
# numpy 2.x stub: 'generic' is a runtime-valid unit but not in the Literal overload.
return np.timedelta64("NaT", self.unit) # type: ignore[arg-type]

def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> np.timedelta64:
"""
Expand All @@ -585,7 +589,9 @@ def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> np.timedel
If the input JSON is not a valid representation of a scalar for this data type.
"""
if check_json_time(data):
return self.to_native_dtype().type(data, f"{self.scale_factor}{self.unit}")
# numpy 2.x stub: timedelta64(scalar, formatted_unit_str) is runtime-valid
# but no overload matches the dynamic f-string unit argument.
return self.to_native_dtype().type(data, f"{self.scale_factor}{self.unit}") # type: ignore[call-overload, no-any-return]
raise TypeError(f"Invalid type: {data}. Expected an integer.") # pragma: no cover


Expand Down Expand Up @@ -812,7 +818,9 @@ def _cast_scalar_unchecked(self, data: DateTimeLike) -> np.datetime64:
numpy.datetime64
The input cast to a NumPy datetime scalar.
"""
return self.to_native_dtype().type(data, f"{self.scale_factor}{self.unit}")
# numpy 2.x stub: datetime64(scalar, formatted_unit_str) is runtime-valid
# but no overload matches the dynamic f-string unit argument.
return self.to_native_dtype().type(data, f"{self.scale_factor}{self.unit}") # type: ignore[call-overload, no-any-return]

def cast_scalar(self, data: object) -> np.datetime64:
"""
Expand Down Expand Up @@ -851,7 +859,8 @@ def default_scalar(self) -> np.datetime64:
The default scalar value, which is a 'Not-a-Time' (NaT) value
"""

return np.datetime64("NaT", self.unit)
# numpy 2.x stub: 'generic' is a runtime-valid unit but not in the Literal overload.
return np.datetime64("NaT", self.unit) # type: ignore[arg-type]

def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> np.datetime64:
"""
Expand Down
2 changes: 1 addition & 1 deletion src/zarr/core/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,7 @@ async def require_array(
name: str,
*,
shape: ShapeLike,
dtype: npt.DTypeLike = None,
dtype: npt.DTypeLike | None = None,
exact: bool = False,
**kwargs: Any,
) -> AnyAsyncArray:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -1792,7 +1792,7 @@ async def test_from_array(
assert result.fill_value == new_fill_value
assert result.dtype == src_dtype
assert result.attrs == new_attributes
assert result.chunks == new_chunks # type: ignore[unreachable]
assert result.chunks == new_chunks


@pytest.mark.parametrize("store", ["local"], indirect=True)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_dtype_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def test_match_dtype_string_na_object_error(
data_type_registry_fixture: DataTypeRegistry,
) -> None:
data_type_registry_fixture.register(VariableLengthUTF8._zarr_v3_name, VariableLengthUTF8) # type: ignore[arg-type]
dtype: np.dtype[Any] = np.dtypes.StringDType(na_object=None) # type: ignore[call-arg]
dtype: np.dtype[Any] = np.dtypes.StringDType(na_object=None)
with pytest.raises(ValueError, match=r"Zarr data type resolution from StringDType.*failed"):
data_type_registry_fixture.match_dtype(dtype)

Expand Down
Loading
Loading