From 54624c5b3b98030995d2b16745a36f1952d8a9e0 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Wed, 29 Jul 2026 22:53:45 +0200 Subject: [PATCH] chore: bump ruff to 0.16.0 and fix new default-rule violations Ruff 0.16.0 enables a much larger default rule set (flake8-bugbear, blind-except, bandit subset, pylint subset, etc.) and formats Python code blocks inside Markdown files. This bumps the pin in pyproject.toml and pre-commit, applies the automatic fixes (RUF036 None-at-end-of-union, RUF100 unused noqa, PLR1716 chained comparison), and resolves the rest by hand: - StorePath.__eq__ narrows a blind 'except Exception: pass' to 'except AttributeError: return False' (BLE001/S110) - reset_resources_after_fork drops 'loop' and 'iothread' from the global statement; they are mutated in place, not rebound (PLW0602) - subprocess.run calls in tests pass check=False explicitly since they assert on returncode themselves (PLW1510) - intentional patterns (returning the caught exception in sync._runner, self-equality assertion in the store test suite) get targeted noqa comments (BLE001/PLR0124) Assisted-by: ClaudeCode:claude-fable-5 --- .pre-commit-config.yaml | 2 +- changes/4213.misc.md | 1 + design/chunk-grid.md | 67 +++++++++++-------- pyproject.toml | 2 +- src/zarr/api/synchronous.py | 2 +- src/zarr/codecs/sharding.py | 2 +- src/zarr/core/array.py | 6 +- src/zarr/core/dtype/common.py | 2 +- src/zarr/core/sync.py | 6 +- src/zarr/storage/_common.py | 5 +- src/zarr/testing/store.py | 2 +- src/zarr/testing/strategies.py | 6 +- tests/test_api.py | 2 +- tests/test_codecs/test_blosc.py | 4 +- tests/test_dtype_registry.py | 2 +- tests/test_examples.py | 2 +- .../test_v2_dtype_regression.py | 2 + tests/test_store/test_core.py | 2 +- tests/test_store/test_object.py | 1 - tests/test_unified_chunk_grid.py | 4 +- tests/test_v2.py | 2 +- uv.lock | 46 ++++++------- 22 files changed, 91 insertions(+), 79 deletions(-) create mode 100644 changes/4213.misc.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57a1d0d4f7..7f49f47187 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ default_language_version: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.20 + rev: v0.16.0 hooks: - id: ruff-check args: ["--fix", "--show-fixes"] diff --git a/changes/4213.misc.md b/changes/4213.misc.md new file mode 100644 index 0000000000..150e60b57b --- /dev/null +++ b/changes/4213.misc.md @@ -0,0 +1 @@ +Updated ruff to 0.16.0 and fixed the violations surfaced by its expanded default rule set: narrowed a blind `except Exception` in `StorePath.__eq__` to `AttributeError`, removed unnecessary `global` declarations in `zarr.core.sync`, made `subprocess.run` calls in tests pass `check=False` explicitly, and applied automatic fixes (`None` moved to the end of type unions, unused `noqa` directives removed). diff --git a/design/chunk-grid.md b/design/chunk-grid.md index eaa5fffad5..0f12e35c4b 100644 --- a/design/chunk-grid.md +++ b/design/chunk-grid.md @@ -162,9 +162,9 @@ class DimensionGrid(Protocol): @property def extent(self) -> int: ... def index_to_chunk(self, idx: int) -> int: ... - def chunk_offset(self, chunk_ix: int) -> int: ... # raises IndexError if OOB - def chunk_size(self, chunk_ix: int) -> int: ... # raises IndexError if OOB - def data_size(self, chunk_ix: int) -> int: ... # raises IndexError if OOB + def chunk_offset(self, chunk_ix: int) -> int: ... # raises IndexError if OOB + def chunk_size(self, chunk_ix: int) -> int: ... # raises IndexError if OOB + def data_size(self, chunk_ix: int) -> int: ... # raises IndexError if OOB def indices_to_chunks(self, indices: NDArray[np.intp]) -> NDArray[np.intp]: ... @property def unique_edge_lengths(self) -> Iterable[int]: ... @@ -181,8 +181,8 @@ The protocol is `@runtime_checkable`, enabling polymorphic handling of both dime ```python @dataclass(frozen=True) class ChunkSpec: - slices: tuple[slice, ...] # valid data region in array coordinates - codec_shape: tuple[int, ...] # buffer shape for codec processing + slices: tuple[slice, ...] # valid data region in array coordinates + codec_shape: tuple[int, ...] # buffer shape for codec processing @property def shape(self) -> tuple[int, ...]: @@ -199,34 +199,34 @@ For interior chunks, `shape == codec_shape`. For boundary chunks of a regular gr ```python # Creating arrays -arr = zarr.create_array(shape=(100, 200), chunks=(10, 20)) # regular -arr = zarr.create_array(shape=(60, 100), chunks=[[10, 20, 30], [25, 25, 25, 25]]) # rectilinear +arr = zarr.create_array(shape=(100, 200), chunks=(10, 20)) # regular +arr = zarr.create_array(shape=(60, 100), chunks=[[10, 20, 30], [25, 25, 25, 25]]) # rectilinear # ChunkGrid as a collection -grid = arr._chunk_grid # ChunkGrid (bound to array shape) -grid.grid_shape # (10, 10) — number of chunks per dimension -grid.ndim # 2 -grid.is_regular # True if all dimensions are Fixed +grid = arr._chunk_grid # ChunkGrid (bound to array shape) +grid.grid_shape # (10, 10) — number of chunks per dimension +grid.ndim # 2 +grid.is_regular # True if all dimensions are Fixed -spec = grid[0, 1] # ChunkSpec for chunk at grid position (0, 1) -spec.slices # (slice(0, 10), slice(20, 40)) -spec.shape # (10, 20) — data shape -spec.codec_shape # (10, 20) — same for interior chunks +spec = grid[0, 1] # ChunkSpec for chunk at grid position (0, 1) +spec.slices # (slice(0, 10), slice(20, 40)) +spec.shape # (10, 20) — data shape +spec.codec_shape # (10, 20) — same for interior chunks -boundary = grid[9, 0] # boundary chunk (extent=100, size=10) -boundary.shape # (10, 20) — data shape -boundary.codec_shape # (10, 20) — codec sees full buffer +boundary = grid[9, 0] # boundary chunk (extent=100, size=10) +boundary.shape # (10, 20) — data shape +boundary.codec_shape # (10, 20) — codec sees full buffer -grid[99, 99] # None — out of bounds +grid[99, 99] # None — out of bounds -for spec in grid: # iterate all chunks +for spec in grid: # iterate all chunks ... # .chunks property: retained for regular grids, raises NotImplementedError for rectilinear -arr.chunks # (10, 20) +arr.chunks # (10, 20) # .read_chunk_sizes / .write_chunk_sizes: works for all grids (dask-style) -arr.write_chunk_sizes # ((10, 10, ..., 10), (20, 20, ..., 20)) +arr.write_chunk_sizes # ((10, 10, ..., 10), (20, 20, ..., 20)) ``` `ChunkGrid.__getitem__` constructs `ChunkSpec` using `chunk_size` for `codec_shape` and `data_size` for `slices`: @@ -274,7 +274,10 @@ When `extent < sum(edges)`, the dimension is always stored as `VaryingDimension` {"name": "regular", "configuration": {"chunk_shape": [10, 20]}} # Rectilinear grid (with RLE compression and "kind" field): -{"name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[10, 20, 30], [[25, 4]]]}} +{ + "name": "rectilinear", + "configuration": {"kind": "inline", "chunk_shapes": [[10, 20, 30], [[25, 4]]]}, +} ``` Both names deserialize to the same `ChunkGrid` class. The serialized form does not include the array extent — that comes from `shape` in array metadata and is combined with the chunk grid when constructing a `ChunkGrid` via `ChunkGrid.from_metadata()`. @@ -324,9 +327,9 @@ The underlying `ChunkGrid.chunk_sizes` property (on the grid, not the array) ret #### Resize ```python -arr.resize((80, 100)) # re-binds extent; FixedDimension stays fixed -arr.resize((200, 100)) # VaryingDimension grows by appending a new chunk -arr.resize((30, 100)) # VaryingDimension shrinks: preserves all edges, re-binds extent +arr.resize((80, 100)) # re-binds extent; FixedDimension stays fixed +arr.resize((200, 100)) # VaryingDimension grows by appending a new chunk +arr.resize((30, 100)) # VaryingDimension shrinks: preserves all edges, re-binds extent ``` Resize uses `ChunkGrid.update_shape(new_shape)`, which delegates to each dimension's `.resize()` method: @@ -363,8 +366,8 @@ When `chunks="keep"`, the logic checks `data._chunk_grid.is_regular`: The indexing pipeline is coupled to regular grid assumptions — every per-dimension indexer takes a scalar `dim_chunk_len: int` and uses `//` and `*`: ```python -dim_chunk_ix = self.dim_sel // self.dim_chunk_len # IntDimIndexer -dim_offset = dim_chunk_ix * self.dim_chunk_len # SliceDimIndexer +dim_chunk_ix = self.dim_sel // self.dim_chunk_len # IntDimIndexer +dim_offset = dim_chunk_ix * self.dim_chunk_len # SliceDimIndexer ``` Replace `dim_chunk_len: int` with the dimension object (`FixedDimension | VaryingDimension`). The shared interface means the indexer code structure stays the same — `dim_sel // dim_chunk_len` becomes `dim_grid.index_to_chunk(dim_sel)`. O(1) for regular, binary search for varying. @@ -590,14 +593,18 @@ If cubed needs to support both old and new zarr-python: def _create_zarr_indexer(selection, shape, chunks): if zarr.__version__[0] == "3": from zarr.core.indexing import OrthogonalIndexer + try: from zarr.core.chunk_grids import ChunkGrid + return OrthogonalIndexer(selection, shape, ChunkGrid.from_sizes(shape, chunks)) except ImportError: from zarr.core.chunk_grids import RegularChunkGrid + return OrthogonalIndexer(selection, shape, RegularChunkGrid(chunk_shape=chunks)) else: from zarr.indexing import OrthogonalIndexer + return OrthogonalIndexer(selection, ZarrArrayIndexingAdaptor(shape, chunks)) ``` @@ -625,13 +632,15 @@ def _resolve_chunk_grid(chunk_grid, shape): """Coerce ChunkGridMetadata to runtime ChunkGrid if needed.""" from zarr.core.chunk_grids import ChunkGrid as _ChunkGrid from zarr.core.metadata.v3 import ChunkGridMetadata + if isinstance(chunk_grid, _ChunkGrid): return chunk_grid if isinstance(chunk_grid, ChunkGridMetadata): warnings.warn( "Passing ChunkGridMetadata to indexers is deprecated. " "Use ChunkGrid.from_sizes() instead.", - DeprecationWarning, stacklevel=2, + DeprecationWarning, + stacklevel=2, ) if hasattr(chunk_grid, "chunk_shape"): return _ChunkGrid.from_sizes(shape, tuple(chunk_grid.chunk_shape)) diff --git a/pyproject.toml b/pyproject.toml index 684ac80b77..663a1ea286 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,7 +129,7 @@ docs = [ "mkdocs-redirects==1.2.3", "markdown-exec[ansi]==1.12.3", "griffe-inherited-docstrings==1.1.3", - "ruff==0.15.22", + "ruff==0.16.0", # Changelog generation {include-group = "release"}, # Optional dependencies to run examples diff --git a/src/zarr/api/synchronous.py b/src/zarr/api/synchronous.py index dc12d5f7af..b03a5af79e 100644 --- a/src/zarr/api/synchronous.py +++ b/src/zarr/api/synchronous.py @@ -998,7 +998,7 @@ def from_array( write_data: bool = True, name: str | None = None, chunks: ChunksLike | Literal["auto", "keep"] = "keep", - shards: ShardsLike | None | Literal["keep"] = "keep", + shards: ShardsLike | Literal["keep"] | None = "keep", filters: FiltersLike | Literal["keep"] = "keep", compressors: CompressorsLike | Literal["keep"] = "keep", serializer: SerializerLike | Literal["keep"] = "keep", diff --git a/src/zarr/codecs/sharding.py b/src/zarr/codecs/sharding.py index cdfdae6c89..e3423bdd09 100644 --- a/src/zarr/codecs/sharding.py +++ b/src/zarr/codecs/sharding.py @@ -841,7 +841,7 @@ def _encode_partial_sync( # `_sentinel` distinguishes "not computed yet" from a memoized `None` # (an empty chunk). _sentinel = object() - scalar_complete_result: Buffer | None | object = _sentinel + scalar_complete_result: Buffer | object | None = _sentinel for chunk_coords, chunk_sel, out_sel, is_complete_chunk in indexer: if is_scalar and is_complete_chunk: diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index cd51dad50c..ad5d9c44ec 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -2181,7 +2181,7 @@ def filters(self) -> tuple[Numcodec, ...] | tuple[ArrayArrayCodec, ...]: return self.async_array.filters @property - def serializer(self) -> None | ArrayBytesCodec: + def serializer(self) -> ArrayBytesCodec | None: """ Array-to-bytes codec to use for serializing the chunks into bytes. """ @@ -4072,7 +4072,7 @@ async def from_array( write_data: bool = True, name: str | None = None, chunks: ChunksLike | Literal["auto", "keep"] = "keep", - shards: ShardsLike | None | Literal["keep"] = "keep", + shards: ShardsLike | Literal["keep"] | None = "keep", filters: FiltersLike | Literal["keep"] = "keep", compressors: CompressorsLike | Literal["keep"] = "keep", serializer: SerializerLike | Literal["keep"] = "keep", @@ -4763,7 +4763,7 @@ async def create_array( def _parse_keep_array_attr( data: AnyArray | npt.ArrayLike, chunks: ChunksLike | Literal["auto", "keep"], - shards: ShardsLike | None | Literal["keep"], + shards: ShardsLike | Literal["keep"] | None, filters: FiltersLike | Literal["keep"], compressors: CompressorsLike | Literal["keep"], serializer: SerializerLike | Literal["keep"], diff --git a/src/zarr/core/dtype/common.py b/src/zarr/core/dtype/common.py index 76d763d267..61cbfe0360 100644 --- a/src/zarr/core/dtype/common.py +++ b/src/zarr/core/dtype/common.py @@ -52,7 +52,7 @@ DTypeName_V2 = StructuredName_V2 | str -class DTypeConfig_V2[TDTypeNameV2: DTypeName_V2, TObjectCodecID: None | str](TypedDict): +class DTypeConfig_V2[TDTypeNameV2: DTypeName_V2, TObjectCodecID: str | None](TypedDict): name: ReadOnly[TDTypeNameV2] object_codec_id: ReadOnly[TObjectCodecID] diff --git a/src/zarr/core/sync.py b/src/zarr/core/sync.py index 160950ba64..724b31a464 100644 --- a/src/zarr/core/sync.py +++ b/src/zarr/core/sync.py @@ -90,7 +90,9 @@ def reset_resources_after_fork() -> None: Ensure that global resources are reset after a fork. Without this function, forked processes will retain invalid references to the parent process's resources. """ - global loop, iothread, _executor + # `loop` and `iothread` are mutated in place rather than rebound, so only + # `_executor` needs the global declaration. + global _executor # These lines are excluded from coverage because this function only runs in a child process, # which is not observed by the test coverage instrumentation. Despite the apparent lack of # test coverage, this function should be adequately tested by any test that uses Zarr IO with @@ -112,7 +114,7 @@ async def _runner[T](coro: Coroutine[Any, Any, T]) -> T | BaseException: """ try: return await coro - except Exception as ex: + except Exception as ex: # noqa: BLE001 -- the caller re-raises the returned exception return ex diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index 7e9c035c69..5ad46648bb 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -297,9 +297,8 @@ def __eq__(self, other: object) -> bool: """ try: return self.store == other.store and self.path == other.path # type: ignore[attr-defined, no-any-return] - except Exception: - pass - return False + except AttributeError: + return False type StoreLike = Store | StorePath | FSMap | Path | str | dict[str, Buffer] diff --git a/src/zarr/testing/store.py b/src/zarr/testing/store.py index d7011440e0..a88ab2c7fe 100644 --- a/src/zarr/testing/store.py +++ b/src/zarr/testing/store.py @@ -113,7 +113,7 @@ def test_store_type(self, store: S) -> None: def test_store_eq(self, store: S, store_kwargs: dict[str, Any]) -> None: # check self equality - assert store == store + assert store == store # noqa: PLR0124 -- self-equality is the property under test # check store equality with same inputs # asserting this is important for being able to compare (de)serialized stores diff --git a/src/zarr/testing/strategies.py b/src/zarr/testing/strategies.py index 99e81b0389..6679dbcee4 100644 --- a/src/zarr/testing/strategies.py +++ b/src/zarr/testing/strategies.py @@ -132,7 +132,7 @@ def clear_store(x: Store) -> Store: @st.composite -def dimension_names(draw: st.DrawFn, *, ndim: int | None = None) -> list[None | str] | None: +def dimension_names(draw: st.DrawFn, *, ndim: int | None = None) -> list[str | None] | None: simple_text = st.text(zarr_key_chars, min_size=0) return draw(st.none() | st.lists(st.none() | simple_text, min_size=ndim, max_size=ndim)) # type: ignore[arg-type] @@ -292,7 +292,7 @@ def arrays( if arrays is None: arrays = numpy_arrays(shapes=shapes) nparray = draw(arrays, label="array data") - dim_names: None | list[str | None] = None + dim_names: list[str | None] | None = None serializer: SerializerLike = "auto" compressors_unsearched: CompressorsLike = "auto" @@ -328,7 +328,7 @@ def arrays( else: chunks_param = draw(chunk_shapes(shape=nparray.shape), label="chunk shape") - if all(s > c and c > 1 for s, c in zip(nparray.shape, chunks_param, strict=True)): + if all(s > c > 1 for s, c in zip(nparray.shape, chunks_param, strict=True)): shard_shape = draw( st.none() | shard_shapes(shape=nparray.shape, chunk_shape=chunks_param), label="shard shape", diff --git a/tests/test_api.py b/tests/test_api.py index cbe8ea3b44..2b831e942d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -419,7 +419,7 @@ async def test_open_group_unspecified_version(tmp_path: Path, zarr_format: ZarrF @pytest.mark.parametrize("n_args", [10, 1, 0]) @pytest.mark.parametrize("n_kwargs", [10, 1, 0]) @pytest.mark.parametrize("path", [None, "some_path"]) -def test_save(store: Store, n_args: int, n_kwargs: int, path: None | str) -> None: +def test_save(store: Store, n_args: int, n_kwargs: int, path: str | None) -> None: data = np.arange(10) args = [np.arange(10) for _ in range(n_args)] kwargs = {f"arg_{i}": data for i in range(n_kwargs)} diff --git a/tests/test_codecs/test_blosc.py b/tests/test_codecs/test_blosc.py index f5f13f4d05..e342dba8bb 100644 --- a/tests/test_codecs/test_blosc.py +++ b/tests/test_codecs/test_blosc.py @@ -74,7 +74,7 @@ async def test_blosc_evolve(dtype: str) -> None: @pytest.mark.parametrize("shuffle", [None, "bitshuffle", "legacy-enum"]) @pytest.mark.parametrize("typesize", [None, 1, 2]) def test_tunable_attrs_param( - shuffle: None | BloscShuffleLiteral | str, typesize: None | int + shuffle: BloscShuffleLiteral | str | None, typesize: int | None ) -> None: """ Test that the tunable_attrs parameter is set as expected when creating a BloscCodec. @@ -83,7 +83,7 @@ def test_tunable_attrs_param( # contaminating the BloscCodec construction below with that warning. if shuffle == "legacy-enum": with pytest.warns(DeprecationWarning, match="BloscShuffle.shuffle"): - shuffle_arg: None | BloscShuffleLiteral | str = BloscShuffle.shuffle + shuffle_arg: BloscShuffleLiteral | str | None = BloscShuffle.shuffle else: shuffle_arg = shuffle diff --git a/tests/test_dtype_registry.py b/tests/test_dtype_registry.py index f0946014fc..40239c1132 100644 --- a/tests/test_dtype_registry.py +++ b/tests/test_dtype_registry.py @@ -170,7 +170,7 @@ def test_entrypoint_dtype(zarr_format: ZarrFormat) -> None: ) def test_parse_data_type( data_type: ZDType[Any, Any], - json_style: tuple[ZarrFormat, None | Literal["internal", "metadata"]], + json_style: tuple[ZarrFormat, Literal["internal", "metadata"] | None], dtype_parser_func: Any, ) -> None: """ diff --git a/tests/test_examples.py b/tests/test_examples.py index 9f8085e8c2..a6634e8cc6 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -80,7 +80,7 @@ def test_scripts_can_run(script_path: Path, tmp_path: Path) -> None: # This allows the example to be useful to users who don't have Zarr installed, but also testable. resave_script(script_path, dest_path) result = subprocess.run( - ["uv", "run", "--refresh", str(dest_path)], capture_output=True, text=True + ["uv", "run", "--refresh", str(dest_path)], capture_output=True, text=True, check=False ) assert result.returncode == 0, ( f"Script at {script_path} failed to run. Output: {result.stdout} Error: {result.stderr}" diff --git a/tests/test_regression/test_v2_dtype_regression.py b/tests/test_regression/test_v2_dtype_regression.py index c7b4a53a52..faba087e32 100644 --- a/tests/test_regression/test_v2_dtype_regression.py +++ b/tests/test_regression/test_v2_dtype_regression.py @@ -215,6 +215,7 @@ def test_roundtrip_v2(source_array_v2: ArrayV2, tmp_path: Path, script_path: Pat ], capture_output=True, text=True, + check=False, ) assert copy_op.returncode == 0, f"stdout {copy_op.stdout}\n stderr{copy_op.stderr}" out_array = zarr.open_array(store=out_path, mode="r", zarr_format=2) @@ -240,6 +241,7 @@ def test_roundtrip_v3(source_array_v3: ArrayV3, tmp_path: Path) -> None: ], capture_output=True, text=True, + check=False, ) assert copy_op.returncode == 0 out_array = zarr.open_array(store=out_path, mode="r", zarr_format=3) diff --git a/tests/test_store/test_core.py b/tests/test_store/test_core.py index d2784e1b4b..4138eebe6a 100644 --- a/tests/test_store/test_core.py +++ b/tests/test_store/test_core.py @@ -33,7 +33,7 @@ ) def store_like( request: pytest.FixtureRequest, -) -> Generator[None | str | Path | StorePath | MemoryStore | dict[Any, Any], None, None]: +) -> Generator[str | Path | StorePath | MemoryStore | dict[Any, Any] | None, None, None]: if request.param == "none": yield None elif request.param == "temp_dir_str": diff --git a/tests/test_store/test_object.py b/tests/test_store/test_object.py index cd85a48eb8..1ea148b3c3 100644 --- a/tests/test_store/test_object.py +++ b/tests/test_store/test_object.py @@ -1,4 +1,3 @@ -# ruff: noqa: E402 import re from pathlib import Path from typing import TypedDict diff --git a/tests/test_unified_chunk_grid.py b/tests/test_unified_chunk_grid.py index 0df5a5d9fd..f0b54519ab 100644 --- a/tests/test_unified_chunk_grid.py +++ b/tests/test_unified_chunk_grid.py @@ -2784,8 +2784,8 @@ def test_rectilinear_roundtrip(json_input: RectilinearChunkGridMetadataJSON) -> pytest.importorskip("hypothesis") -import hypothesis.strategies as st # noqa: E402 -from hypothesis import event, given, settings # noqa: E402 +import hypothesis.strategies as st +from hypothesis import event, given, settings @st.composite diff --git a/tests/test_v2.py b/tests/test_v2.py index 3a063ac509..798687438b 100644 --- a/tests/test_v2.py +++ b/tests/test_v2.py @@ -294,7 +294,7 @@ def test_parse_structured_fill_value_valid( @pytest.mark.parametrize("fill_value", [None, b"x"], ids=["no_fill", "fill"]) -def test_other_dtype_roundtrip(fill_value: None | bytes, tmp_path: Path) -> None: +def test_other_dtype_roundtrip(fill_value: bytes | None, tmp_path: Path) -> None: a = np.array([b"a\0\0", b"bb", b"ccc"], dtype="V7") array_path = tmp_path / "data.zarr" za = zarr.create( diff --git a/uv.lock b/uv.lock index 8eac71caa7..eb43284f0d 100644 --- a/uv.lock +++ b/uv.lock @@ -2888,27 +2888,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, - { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, - { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, - { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, - { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, - { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, - { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, - { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, - { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, ] [[package]] @@ -3589,7 +3589,7 @@ dev = [ { name = "pytest-cov", specifier = "==7.1.0" }, { name = "pytest-xdist", specifier = "==3.8.0" }, { name = "requests", specifier = "==2.34.2" }, - { name = "ruff", specifier = "==0.15.22" }, + { name = "ruff", specifier = "==0.16.0" }, { name = "s3fs", specifier = ">=2023.10.0" }, { name = "tomlkit", specifier = "==0.15.1" }, { name = "towncrier", specifier = "==25.8.0" }, @@ -3608,7 +3608,7 @@ docs = [ { name = "mkdocstrings-python", specifier = "==2.0.5" }, { name = "numcodecs", extras = ["msgpack"] }, { name = "pytest", specifier = "==9.1.1" }, - { name = "ruff", specifier = "==0.15.22" }, + { name = "ruff", specifier = "==0.16.0" }, { name = "s3fs", specifier = ">=2023.10.0" }, { name = "towncrier", specifier = "==25.8.0" }, ]