From 8da13336dfa60c2b4412097bc5e58488aebbb66c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 31 May 2026 19:28:07 +0200 Subject: [PATCH 1/9] chore(deps): bump the actions group across 1 directory with 8 updates (#176) Bumps the actions group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [prefix-dev/setup-pixi](https://github.com/prefix-dev/setup-pixi) | `0.9.5` | `0.9.6` | | [codecov/codecov-action](https://github.com/codecov/codecov-action) | `6.0.0` | `6.0.1` | | [github/issue-metrics](https://github.com/github/issue-metrics) | `4.2.2` | `4.2.7` | | [j178/prek-action](https://github.com/j178/prek-action) | `2.0.3` | `2.0.4` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.0` | `7.0.1` | | [actions/download-artifact](https://github.com/actions/download-artifact) | `7.0.0` | `8.0.1` | | [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) | `1.13.0` | `1.14.0` | | [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) | `0.5.3` | `0.5.6` | Updates `prefix-dev/setup-pixi` from 0.9.5 to 0.9.6 - [Release notes](https://github.com/prefix-dev/setup-pixi/releases) - [Commits](https://github.com/prefix-dev/setup-pixi/compare/1b2de7f3351f171c8b4dfeb558c639cb58ed4ec0...5185adfbffb4bd703da3010310260805d89ebb11) Updates `codecov/codecov-action` from 6.0.0 to 6.0.1 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/57e3a136b779b570ffcdbf80b3bdc90e7fab3de2...e79a6962e0d4c0c17b229090214935d2e33f8354) Updates `github/issue-metrics` from 4.2.2 to 4.2.7 - [Release notes](https://github.com/github/issue-metrics/releases) - [Commits](https://github.com/github/issue-metrics/compare/c9e9838147fd355dace335ba787f01b6641a400a...1e38d5e62363e14db8019ed7d106b9855bdba6cc) Updates `j178/prek-action` from 2.0.3 to 2.0.4 - [Release notes](https://github.com/j178/prek-action/releases) - [Commits](https://github.com/j178/prek-action/compare/6ad80277337ad479fe43bd70701c3f7f8aa74db3...bdca6f102f98e2b4c7029491a53dfd366469e33d) Updates `actions/upload-artifact` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v7...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a) Updates `actions/download-artifact` from 7.0.0 to 8.0.1 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c) Updates `pypa/gh-action-pypi-publish` from 1.13.0 to 1.14.0 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.13.0...cef221092ed1bacb1cc03d23a2d87d1d172e277b) Updates `zizmorcore/zizmor-action` from 0.5.3 to 0.5.6 - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/b1d7e1fb5de872772f31590499237e7cce841e8e...5f14fd08f7cf1cb1609c1e344975f152c7ee938d) --- updated-dependencies: - dependency-name: prefix-dev/setup-pixi dependency-version: 0.9.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: codecov/codecov-action dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: github/issue-metrics dependency-version: 4.2.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: j178/prek-action dependency-version: 2.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: actions/upload-artifact dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: actions/download-artifact dependency-version: 8.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: zizmorcore/zizmor-action dependency-version: 0.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/downstream.yml | 2 +- .github/workflows/gpu_test.yml | 2 +- .github/workflows/hypothesis.yaml | 2 +- .github/workflows/issue-metrics.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 4 ++-- .github/workflows/zarr-metadata-release.yml | 12 ++++++------ .github/workflows/zizmor.yml | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 74026233c4..3eb6898895 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: Set up pixi - uses: prefix-dev/setup-pixi@1b2de7f3351f171c8b4dfeb558c639cb58ed4ec0 # v0.9.5 + uses: prefix-dev/setup-pixi@5185adfbffb4bd703da3010310260805d89ebb11 # v0.9.6 with: manifest-path: xarray/pixi.toml diff --git a/.github/workflows/gpu_test.yml b/.github/workflows/gpu_test.yml index 403441b306..333769cb9e 100644 --- a/.github/workflows/gpu_test.yml +++ b/.github/workflows/gpu_test.yml @@ -76,7 +76,7 @@ jobs: hatch env run --env "$HATCH_ENV" run-coverage - name: Upload coverage - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} flags: gpu diff --git a/.github/workflows/hypothesis.yaml b/.github/workflows/hypothesis.yaml index 4f9467be7d..a456b2aa0a 100644 --- a/.github/workflows/hypothesis.yaml +++ b/.github/workflows/hypothesis.yaml @@ -93,7 +93,7 @@ jobs: key: cache-hypothesis-${{ runner.os }}-${{ github.run_id }} - name: Upload coverage - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests diff --git a/.github/workflows/issue-metrics.yml b/.github/workflows/issue-metrics.yml index 14fba5b9ec..510849ef3e 100644 --- a/.github/workflows/issue-metrics.yml +++ b/.github/workflows/issue-metrics.yml @@ -33,7 +33,7 @@ jobs: echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - name: Run issue-metrics tool - uses: github/issue-metrics@c9e9838147fd355dace335ba787f01b6641a400a # v4.2.2 + uses: github/issue-metrics@1e38d5e62363e14db8019ed7d106b9855bdba6cc # v4.2.7 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:zarr-developers/zarr-python is:issue created:${{ env.last_month }} -reason:"not planned"' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 768e660ec2..fec211b4dd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,4 +30,4 @@ jobs: uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true - - uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3 + - uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 03143d3e5b..62e571856b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,7 +78,7 @@ jobs: hatch env run --env "$HATCH_ENV" run-coverage - name: Upload coverage if: ${{ matrix.dependency-set == 'optional' && matrix.os == 'ubuntu-latest' }} - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests @@ -125,7 +125,7 @@ jobs: run: | hatch env run --env "$HATCH_ENV" run-coverage - name: Upload coverage - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests diff --git a/.github/workflows/zarr-metadata-release.yml b/.github/workflows/zarr-metadata-release.yml index 809d502f16..9639fcfdd3 100644 --- a/.github/workflows/zarr-metadata-release.yml +++ b/.github/workflows/zarr-metadata-release.yml @@ -35,7 +35,7 @@ jobs: - name: Build run: hatch build - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: zarr-metadata-dist path: packages/zarr-metadata/dist @@ -45,7 +45,7 @@ jobs: needs: [build] runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: zarr-metadata-dist path: dist @@ -76,7 +76,7 @@ jobs: id-token: write # required for OIDC trusted publishing attestations: write # required for artifact attestations steps: - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: zarr-metadata-dist path: dist @@ -87,7 +87,7 @@ jobs: subject-path: dist/* - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 upload_testpypi: name: Upload to TestPyPI @@ -101,7 +101,7 @@ jobs: id-token: write attestations: write steps: - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: zarr-metadata-dist path: dist @@ -112,6 +112,6 @@ jobs: subject-path: dist/* - name: Publish package to TestPyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index da19f22421..7ac4fe5d0e 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -32,4 +32,4 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 From 6a38bc18ba927bc356366c48f8011c9a4f4ed6cb Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Jun 2026 13:32:44 +0200 Subject: [PATCH 2/9] refactor: dedupe overwrite-check logic in array creation Extract the repeated "delete existing node if overwriting (and the store supports deletes), otherwise enforce no existing node" dance into a single private helper `_prepare_overwrite`, used by `_create_v3`, `_create_v2`, and `_create_array_metadata`. Co-Authored-By: Claude Fable 5 --- src/zarr/core/array.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index d15c70064b..246bca220d 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -298,6 +298,22 @@ async def get_array_metadata( return metadata_dict +async def _prepare_overwrite( + store_path: StorePath, *, zarr_format: ZarrFormat, overwrite: bool +) -> None: + """ + Prepare a store path for writing a new node. + + If ``overwrite`` is true and the store supports deletes, any existing node at + ``store_path`` is deleted. Otherwise, the absence of an existing node is enforced + (raising if one is present). + """ + if overwrite and store_path.store.supports_deletes: + await store_path.delete_dir() + else: + await ensure_no_existing_node(store_path, zarr_format=zarr_format) + + @dataclass(frozen=True) class AsyncArray[T_ArrayMetadata: (ArrayV2Metadata, ArrayV3Metadata)]: """ @@ -576,13 +592,7 @@ async def _create_v3( attributes: dict[str, JSON] | None = None, overwrite: bool = False, ) -> AsyncArrayV3: - if overwrite: - if store_path.store.supports_deletes: - await store_path.delete_dir() - else: - await ensure_no_existing_node(store_path, zarr_format=3) - else: - await ensure_no_existing_node(store_path, zarr_format=3) + await _prepare_overwrite(store_path, zarr_format=3, overwrite=overwrite) if isinstance(chunk_key_encoding, tuple): chunk_key_encoding = ( @@ -657,13 +667,7 @@ async def _create_v2( attributes: dict[str, JSON] | None = None, overwrite: bool = False, ) -> AsyncArrayV2: - if overwrite: - if store_path.store.supports_deletes: - await store_path.delete_dir() - else: - await ensure_no_existing_node(store_path, zarr_format=2) - else: - await ensure_no_existing_node(store_path, zarr_format=2) + await _prepare_overwrite(store_path, zarr_format=2, overwrite=overwrite) compressor_parsed: CompressorLikev2 if compressor == "auto": @@ -4401,10 +4405,7 @@ async def init_array( chunk_key_encoding, zarr_format=zarr_format ) - if overwrite and store_path.store.supports_deletes: - await store_path.delete_dir() - else: - await ensure_no_existing_node(store_path, zarr_format=zarr_format) + await _prepare_overwrite(store_path, zarr_format=zarr_format, overwrite=overwrite) # Validate rectilinear chunks constraints if _is_rectilinear_chunks(chunks): From 967677ffb2c4c16384106f805bf9a611dbc26cad Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Jun 2026 13:34:06 +0200 Subject: [PATCH 3/9] refactor: inline single-caller get-selection wrappers The module-level `_get_orthogonal_selection`, `_get_mask_selection`, and `_get_coordinate_selection` helpers each constructed an indexer and delegated to `_get_selection`, and each had exactly one caller (the corresponding `AsyncArray.get_*_selection` method). Inline the indexer construction into those methods and delete the wrappers. Co-Authored-By: Claude Fable 5 --- src/zarr/core/array.py | 201 ++++------------------------------------- 1 file changed, 19 insertions(+), 182 deletions(-) diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index 246bca220d..b20da4bb9b 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -1493,13 +1493,16 @@ async def get_orthogonal_selection( fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: - return await _get_orthogonal_selection( + if prototype is None: + prototype = default_buffer_prototype() + indexer = OrthogonalIndexer(selection, self.metadata.shape, self._chunk_grid) + return await _get_selection( self.store_path, self.metadata, self.codec_pipeline, self.config, self._chunk_grid, - selection, + indexer=indexer, out=out, fields=fields, prototype=prototype, @@ -1513,13 +1516,16 @@ async def get_mask_selection( fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: - return await _get_mask_selection( + if prototype is None: + prototype = default_buffer_prototype() + indexer = MaskIndexer(mask, self.metadata.shape, self._chunk_grid) + return await _get_selection( self.store_path, self.metadata, self.codec_pipeline, self.config, self._chunk_grid, - mask, + indexer=indexer, out=out, fields=fields, prototype=prototype, @@ -1533,17 +1539,24 @@ async def get_coordinate_selection( fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: - return await _get_coordinate_selection( + if prototype is None: + prototype = default_buffer_prototype() + indexer = CoordinateIndexer(selection, self.metadata.shape, self._chunk_grid) + out_array = await _get_selection( self.store_path, self.metadata, self.codec_pipeline, self.config, self._chunk_grid, - selection, + indexer=indexer, out=out, fields=fields, prototype=prototype, ) + if hasattr(out_array, "shape"): + # restore shape + out_array = cast("NDArrayLikeOrScalar", np.array(out_array).reshape(indexer.sel_shape)) + return out_array async def _save_metadata(self, metadata: ArrayMetadata, ensure_parents: bool = False) -> None: """ @@ -5524,182 +5537,6 @@ async def _getitem( ) -async def _get_orthogonal_selection( - store_path: StorePath, - metadata: ArrayMetadata, - codec_pipeline: CodecPipeline, - config: ArrayConfig, - chunk_grid: ChunkGrid, - selection: OrthogonalSelection, - *, - out: NDBuffer | None = None, - fields: Fields | None = None, - prototype: BufferPrototype | None = None, -) -> NDArrayLikeOrScalar: - """ - Get an orthogonal selection from the array. - - Parameters - ---------- - store_path : StorePath - The store path of the array. - metadata : ArrayMetadata - The array metadata. - codec_pipeline : CodecPipeline - The codec pipeline for encoding/decoding. - config : ArrayConfig - The array configuration. - chunk_grid : ChunkGrid - The chunk grid. - selection : OrthogonalSelection - The orthogonal selection specification. - out : NDBuffer | None, optional - An output buffer to write the data to. - fields : Fields | None, optional - Fields to select from structured arrays. - prototype : BufferPrototype | None, optional - A buffer prototype to use for the retrieved data. - - Returns - ------- - NDArrayLikeOrScalar - The selected data. - """ - if prototype is None: - prototype = default_buffer_prototype() - indexer = OrthogonalIndexer(selection, metadata.shape, chunk_grid) - return await _get_selection( - store_path, - metadata, - codec_pipeline, - config, - chunk_grid, - indexer=indexer, - out=out, - fields=fields, - prototype=prototype, - ) - - -async def _get_mask_selection( - store_path: StorePath, - metadata: ArrayMetadata, - codec_pipeline: CodecPipeline, - config: ArrayConfig, - chunk_grid: ChunkGrid, - mask: MaskSelection, - *, - out: NDBuffer | None = None, - fields: Fields | None = None, - prototype: BufferPrototype | None = None, -) -> NDArrayLikeOrScalar: - """ - Get a mask selection from the array. - - Parameters - ---------- - store_path : StorePath - The store path of the array. - metadata : ArrayMetadata - The array metadata. - codec_pipeline : CodecPipeline - The codec pipeline for encoding/decoding. - config : ArrayConfig - The array configuration. - chunk_grid : ChunkGrid - The chunk grid. - mask : MaskSelection - The boolean mask specifying the selection. - out : NDBuffer | None, optional - An output buffer to write the data to. - fields : Fields | None, optional - Fields to select from structured arrays. - prototype : BufferPrototype | None, optional - A buffer prototype to use for the retrieved data. - - Returns - ------- - NDArrayLikeOrScalar - The selected data. - """ - if prototype is None: - prototype = default_buffer_prototype() - indexer = MaskIndexer(mask, metadata.shape, chunk_grid) - return await _get_selection( - store_path, - metadata, - codec_pipeline, - config, - chunk_grid, - indexer=indexer, - out=out, - fields=fields, - prototype=prototype, - ) - - -async def _get_coordinate_selection( - store_path: StorePath, - metadata: ArrayMetadata, - codec_pipeline: CodecPipeline, - config: ArrayConfig, - chunk_grid: ChunkGrid, - selection: CoordinateSelection, - *, - out: NDBuffer | None = None, - fields: Fields | None = None, - prototype: BufferPrototype | None = None, -) -> NDArrayLikeOrScalar: - """ - Get a coordinate selection from the array. - - Parameters - ---------- - store_path : StorePath - The store path of the array. - metadata : ArrayMetadata - The array metadata. - codec_pipeline : CodecPipeline - The codec pipeline for encoding/decoding. - config : ArrayConfig - The array configuration. - chunk_grid : ChunkGrid - The chunk grid. - selection : CoordinateSelection - The coordinate selection specification. - out : NDBuffer | None, optional - An output buffer to write the data to. - fields : Fields | None, optional - Fields to select from structured arrays. - prototype : BufferPrototype | None, optional - A buffer prototype to use for the retrieved data. - - Returns - ------- - NDArrayLikeOrScalar - The selected data. - """ - if prototype is None: - prototype = default_buffer_prototype() - indexer = CoordinateIndexer(selection, metadata.shape, chunk_grid) - out_array = await _get_selection( - store_path, - metadata, - codec_pipeline, - config, - chunk_grid, - indexer=indexer, - out=out, - fields=fields, - prototype=prototype, - ) - - if hasattr(out_array, "shape"): - # restore shape - out_array = cast("NDArrayLikeOrScalar", np.array(out_array).reshape(indexer.sel_shape)) - return out_array - - async def _set_selection( store_path: StorePath, metadata: ArrayMetadata, From 7dad0bfc301b70ccbdaf848d54f66fdc6723d506 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Jun 2026 13:35:02 +0200 Subject: [PATCH 4/9] refactor: unify zarr-dtype access via metadata.dtype `ArrayV2Metadata.dtype` and `ArrayV3Metadata.dtype` (the latter an alias for `data_type`) both already return the zarr dtype object, so the repeated `metadata.dtype if zarr_format == 2 else metadata.data_type` dispatch in `AsyncArray._zdtype`, `_get_selection`, and `_set_selection` collapses to a single `metadata.dtype` access. Co-Authored-By: Claude Fable 5 --- src/zarr/core/array.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index b20da4bb9b..daf0cd5233 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -973,10 +973,9 @@ def _zdtype(self) -> ZDType[TBaseDType, TBaseScalar]: """ The zarr-specific representation of the array data type """ - if self.metadata.zarr_format == 2: - return self.metadata.dtype - else: - return self.metadata.data_type + # `dtype` returns the zarr dtype object for both v2 and v3 metadata + # (on v3 it is an alias for `data_type`). + return self.metadata.dtype @property def dtype(self) -> TBaseDType: @@ -5403,11 +5402,8 @@ async def _get_selection( NDArrayLikeOrScalar The selected data. """ - # Get dtype from metadata - if metadata.zarr_format == 2: - zdtype = metadata.dtype - else: - zdtype = metadata.data_type + # `dtype` returns the zarr dtype object for both v2 and v3 metadata. + zdtype = metadata.dtype dtype = zdtype.to_native_dtype() # Determine memory order @@ -5573,11 +5569,8 @@ async def _set_selection( fields : Fields | None, optional Fields to select from structured arrays. """ - # Get dtype from metadata - if metadata.zarr_format == 2: - zdtype = metadata.dtype - else: - zdtype = metadata.data_type + # `dtype` returns the zarr dtype object for both v2 and v3 metadata. + zdtype = metadata.dtype dtype = zdtype.to_native_dtype() # check fields are sensible From 3557c5f0931d0a7ec5a3a79cf890f16c67d341c2 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Jun 2026 13:35:31 +0200 Subject: [PATCH 5/9] refactor: make Array.cdata_shape delegate to _chunk_grid_shape `Array.cdata_shape` and `Array._chunk_grid_shape` had identical bodies; the public `cdata_shape` now delegates to `_chunk_grid_shape` instead of duplicating the access. Co-Authored-By: Claude Fable 5 --- src/zarr/core/array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index daf0cd5233..8449ce1e12 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -2181,7 +2181,7 @@ def cdata_shape(self) -> tuple[int, ...]: When sharding is used, this counts inner chunks (not shards) per dimension. """ - return self.async_array._chunk_grid_shape + return self._chunk_grid_shape @property def _chunk_grid_shape(self) -> tuple[int, ...]: From 14ff0455934ec1024e134fb0775f90c57729cee6 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Jun 2026 13:36:08 +0200 Subject: [PATCH 6/9] refactor: inline no-op default_filters_v3 `default_filters_v3` unconditionally returned an empty tuple. Inline that empty tuple at its two call sites (with a short comment) and delete the function. Co-Authored-By: Claude Fable 5 --- src/zarr/core/array.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index 8449ce1e12..2659dad469 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -539,7 +539,8 @@ def _create_metadata_v3( shape = parse_shapelike(shape) if codecs is None: - filters = default_filters_v3(dtype) + # no data types have default filters + filters = () serializer = default_serializer_v3(dtype) compressors = default_compressors_v3(dtype) @@ -4846,15 +4847,6 @@ def _parse_chunk_key_encoding( return result -def default_filters_v3(dtype: ZDType[Any, Any]) -> tuple[ArrayArrayCodec, ...]: - """ - Given a data type, return the default filters for that data type. - - This is an empty tuple. No data types have default filters. - """ - return () - - def default_compressors_v3(dtype: ZDType[Any, Any]) -> tuple[BytesBytesCodec, ...]: """ Given a data type, return the default compressors for that data type. @@ -5007,7 +4999,8 @@ def _parse_chunk_encoding_v3( if filters is None: out_array_array: tuple[ArrayArrayCodec, ...] = () elif filters == "auto": - out_array_array = default_filters_v3(dtype) + # no data types have default filters + out_array_array = () else: maybe_array_array: Iterable[Codec | dict[str, JSON]] if isinstance(filters, dict | Codec): From c1af23dc7a51cad9b3d6f1a747fef5cde437f44b Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Jun 2026 13:37:01 +0200 Subject: [PATCH 7/9] refactor: dedupe contains_group V3 branch via _contains_node_v3 The V3 branch of `contains_group` re-implemented the node-type detection that `_contains_node_v3` already performs. Replace it with `(await _contains_node_v3(store_path)) == "group"`, which is behaviorally identical for all cases (missing document, array, group, malformed/non-object JSON, and missing node_type key all map to the same result). `contains_array` is intentionally left unchanged: its V3 branch falls through to a `ValueError` when a *group* node is present, which `_contains_node_v3` does not reproduce, so swapping it would change observable behavior. Co-Authored-By: Claude Fable 5 --- src/zarr/storage/_common.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index 1e13a9ac3f..86b6c41573 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -643,18 +643,7 @@ async def contains_group(store_path: StorePath, zarr_format: ZarrFormat) -> bool """ if zarr_format == 3: - extant_meta_bytes = await (store_path / ZARR_JSON).get() - if extant_meta_bytes is None: - return False - else: - try: - extant_meta_json = buffer_to_json_object(extant_meta_bytes) - # we avoid constructing a full metadata document here in the name of speed. - result: bool = extant_meta_json["node_type"] == "group" - except (ValueError, KeyError, TypeError): - return False - else: - return result + return (await _contains_node_v3(store_path)) == "group" elif zarr_format == 2: return await (store_path / ZGROUP_JSON).exists() msg = f"Invalid zarr_format provided. Got {zarr_format}, expected 2 or 3" # type: ignore[unreachable] From 1cdc29942612e9b41633c4816871c50cd2c3dab2 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Jun 2026 13:38:22 +0200 Subject: [PATCH 8/9] refactor: extract not-yet-implemented kwarg warning helper `open_group` and `create` each had repeated `if x is not None: warnings.warn(f"...not yet implemented...")` blocks. Extract a `_warn_unimplemented_kwargs` helper taking a name->value mapping. The helper uses stacklevel=3 to compensate for the extra call frame, so the warning still points at the same call site (verified: identical message, category, source location, and emission order). Co-Authored-By: Claude Fable 5 --- src/zarr/api/asynchronous.py | 54 +++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index 7f185535df..6e72deffde 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -108,6 +108,19 @@ def _infer_overwrite(mode: AccessModeLiteral) -> bool: return mode in _OVERWRITE_MODES +def _warn_unimplemented_kwargs(kwargs: dict[str, Any]) -> None: + """ + Emit a "not yet implemented" warning for each provided keyword argument that is not None. + + ``kwargs`` maps a keyword argument name to its supplied value. The ``stacklevel`` is chosen + so the warning points at the caller of the public API function (the same location as an + inline ``warnings.warn(..., stacklevel=2)`` would). + """ + for name, value in kwargs.items(): + if value is not None: + warnings.warn(f"{name} is not yet implemented", ZarrRuntimeWarning, stacklevel=3) + + def _get_shape_chunks(a: ArrayLike | Any) -> tuple[tuple[int, ...] | None, tuple[int, ...] | None]: """Helper function to get the shape and chunks from an array-like object""" shape = None @@ -813,14 +826,14 @@ async def open_group( The new group. """ - if cache_attrs is not None: - warnings.warn("cache_attrs is not yet implemented", ZarrRuntimeWarning, stacklevel=2) - if synchronizer is not None: - warnings.warn("synchronizer is not yet implemented", ZarrRuntimeWarning, stacklevel=2) - if meta_array is not None: - warnings.warn("meta_array is not yet implemented", ZarrRuntimeWarning, stacklevel=2) - if chunk_store is not None: - warnings.warn("chunk_store is not yet implemented", ZarrRuntimeWarning, stacklevel=2) + _warn_unimplemented_kwargs( + { + "cache_attrs": cache_attrs, + "synchronizer": synchronizer, + "meta_array": meta_array, + "chunk_store": chunk_store, + } + ) store_path = await make_store_path(store, mode=mode, storage_options=storage_options, path=path) if attributes is None: @@ -1010,20 +1023,17 @@ async def create( if zarr_format is None: zarr_format = _default_zarr_format() - if synchronizer is not None: - warnings.warn("synchronizer is not yet implemented", ZarrRuntimeWarning, stacklevel=2) - if chunk_store is not None: - warnings.warn("chunk_store is not yet implemented", ZarrRuntimeWarning, stacklevel=2) - if cache_metadata is not None: - warnings.warn("cache_metadata is not yet implemented", ZarrRuntimeWarning, stacklevel=2) - if cache_attrs is not None: - warnings.warn("cache_attrs is not yet implemented", ZarrRuntimeWarning, stacklevel=2) - if object_codec is not None: - warnings.warn("object_codec is not yet implemented", ZarrRuntimeWarning, stacklevel=2) - if read_only is not None: - warnings.warn("read_only is not yet implemented", ZarrRuntimeWarning, stacklevel=2) - if meta_array is not None: - warnings.warn("meta_array is not yet implemented", ZarrRuntimeWarning, stacklevel=2) + _warn_unimplemented_kwargs( + { + "synchronizer": synchronizer, + "chunk_store": chunk_store, + "cache_metadata": cache_metadata, + "cache_attrs": cache_attrs, + "object_codec": object_codec, + "read_only": read_only, + "meta_array": meta_array, + } + ) if write_empty_chunks is not None: _warn_write_empty_chunks_kwarg() From 1af0faa75751fdd6cd8126e18b855433d81e9a50 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Jun 2026 13:40:19 +0200 Subject: [PATCH 9/9] refactor: fold fill_value extraction into _like_args `_like_args` now records the source array's fill_value, so `empty_like`, `full_like`, and `open_like` no longer need to re-check isinstance and setdefault it. `ones_like`/`zeros_like` drop the inherited fill_value (they supply their own), preserving exact prior behavior including the existing duplicate-keyword TypeError when an explicit fill_value is passed. Update the `_like_args` unit test to include the new key. Co-Authored-By: Claude Fable 5 --- src/zarr/api/asynchronous.py | 18 ++++++++++-------- tests/test_api/test_asynchronous.py | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index 6e72deffde..44c1526ccf 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -147,6 +147,7 @@ class _LikeArgs(TypedDict): filters: NotRequired[tuple[Numcodec, ...] | None] compressor: NotRequired[CompressorLikev2] codecs: NotRequired[tuple[Codec, ...]] + fill_value: NotRequired[Any] def _like_args(a: ArrayLike) -> _LikeArgs: @@ -164,6 +165,7 @@ def _like_args(a: ArrayLike) -> _LikeArgs: new["dtype"] = a.dtype if isinstance(a, AsyncArray | Array): + new["fill_value"] = a.metadata.fill_value if isinstance(a.metadata, ArrayV2Metadata): new["order"] = a.order new["compressor"] = a.metadata.compressor @@ -1121,8 +1123,6 @@ async def empty_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: and these are not guaranteed to be stable from one access to the next. """ like_kwargs = _like_args(a) | kwargs - if isinstance(a, (AsyncArray | Array)): - like_kwargs.setdefault("fill_value", a.metadata.fill_value) return await empty(**like_kwargs) # type: ignore[arg-type] @@ -1165,8 +1165,6 @@ async def full_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: The new array. """ like_kwargs = _like_args(a) | kwargs - if isinstance(a, (AsyncArray | Array)): - like_kwargs.setdefault("fill_value", a.metadata.fill_value) return await full(**like_kwargs) # type: ignore[arg-type] @@ -1204,7 +1202,10 @@ async def ones_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: Array The new array. """ - like_kwargs = _like_args(a) | kwargs + like_args = _like_args(a) + # `ones` supplies its own fill_value, so drop any inherited from `a`. + like_args.pop("fill_value", None) + like_kwargs = like_args | kwargs return await ones(**like_kwargs) # type: ignore[arg-type] @@ -1280,8 +1281,6 @@ async def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyAsyncArray: The opened array. """ like_kwargs = _like_args(a) | kwargs - if isinstance(a, (AsyncArray | Array)): - like_kwargs.setdefault("fill_value", a.metadata.fill_value) return await open_array(path=path, **like_kwargs) # type: ignore[arg-type] @@ -1319,5 +1318,8 @@ async def zeros_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: Array The new array. """ - like_kwargs = _like_args(a) | kwargs + like_args = _like_args(a) + # `zeros` supplies its own fill_value, so drop any inherited from `a`. + like_args.pop("fill_value", None) + like_kwargs = like_args | kwargs return await zeros(**like_kwargs) # type: ignore[arg-type] diff --git a/tests/test_api/test_asynchronous.py b/tests/test_api/test_asynchronous.py index 362195e858..6ebec36bbd 100644 --- a/tests/test_api/test_asynchronous.py +++ b/tests/test_api/test_asynchronous.py @@ -75,6 +75,7 @@ def test_get_shape_chunks( "chunks": (10,), "shape": (100,), "dtype": np.dtype("f8"), + "fill_value": np.float64(0.0), "compressor": None, "filters": None, "order": "C",