pax_global_header 0000666 0000000 0000000 00000000064 15232676200 0014515 g ustar 00root root 0000000 0000000 52 comment=53e6dc66b834988912194985a1051ad4db4f0141
zarr-python-3.3.0/ 0000775 0000000 0000000 00000000000 15232676200 0014015 5 ustar 00root root 0000000 0000000 zarr-python-3.3.0/.git-blame-ignore-revs 0000664 0000000 0000000 00000000232 15232676200 0020112 0 ustar 00root root 0000000 0000000 # lint codebase with black and ruff
4e348d6b80c96da461fd866576c971b8a659ba15
# migrate from black to ruff format
22cea005629913208a85799372e045f353744add
zarr-python-3.3.0/.git_archival.txt 0000664 0000000 0000000 00000000201 15232676200 0017261 0 ustar 00root root 0000000 0000000 node: 53e6dc66b834988912194985a1051ad4db4f0141
node-date: 2026-07-30T18:32:00+02:00
describe-name: v3.3.0
ref-names: tag: v3.3.0
zarr-python-3.3.0/.gitattributes 0000664 0000000 0000000 00000000134 15232676200 0016706 0 ustar 00root root 0000000 0000000 *.py linguist-language=python
*.ipynb linguist-documentation
.git_archival.txt export-subst
zarr-python-3.3.0/.github/ 0000775 0000000 0000000 00000000000 15232676200 0015355 5 ustar 00root root 0000000 0000000 zarr-python-3.3.0/.github/CODEOWNERS 0000664 0000000 0000000 00000000070 15232676200 0016745 0 ustar 00root root 0000000 0000000 zarr/_storage/absstore.py @zarr-developers/azure-team
zarr-python-3.3.0/.github/CONTRIBUTING.md 0000664 0000000 0000000 00000000262 15232676200 0017606 0 ustar 00root root 0000000 0000000 Contributing
============
Please see the [project documentation](https://zarr.readthedocs.io/en/stable/developers/contributing.html) for information about contributing to Zarr.
zarr-python-3.3.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15232676200 0017540 5 ustar 00root root 0000000 0000000 zarr-python-3.3.0/.github/ISSUE_TEMPLATE/bug_report.yml 0000664 0000000 0000000 00000005133 15232676200 0022435 0 ustar 00root root 0000000 0000000 name: Bug Report
description: Report incorrect behaviour in the library.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Please provide the following information.
- type: input
id: Zarr-version
attributes:
label: Zarr version
description: Value of ``zarr.__version__``
placeholder: v2.10.2, v2.11.3, v2.12.0, etc.
validations:
required: true
- type: input
id: Numcodecs-version
attributes:
label: Numcodecs version
description: Value of ``numcodecs.__version__``
placeholder: v0.8.1, v0.9.0, v0.10.0, etc.
validations:
required: true
- type: input
id: Python-version
attributes:
label: Python Version
description: Version of Python interpreter
placeholder: 3.10, 3.11, 3.12 etc.
validations:
required: true
- type: input
id: OS
attributes:
label: Operating System
description: Operating System
placeholder: (Linux/Windows/Mac)
validations:
required: true
- type: input
id: installation
attributes:
label: Installation
description: How was Zarr installed?
placeholder: e.g., "using pip into virtual environment", or "using conda"
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Explain why the current behavior is a problem, what the expected output/behaviour is, and why the expected output/behaviour is a better solution.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Minimal, reproducible code sample. Must list dependencies in [inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/#example). When put in a file named `issue.py` calling `uv run issue.py` should show the issue.
value: |
```python
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "zarr@git+https://github.com/zarr-developers/zarr-python.git@main",
# ]
# ///
#
# This script automatically imports the development branch of zarr to check for issues
import zarr
# your reproducer code
# zarr.print_debug_info()
```
validations:
required: true
- type: textarea
id: additional-output
attributes:
label: Additional output
description: If you think it might be relevant, please provide the output from ``pip freeze`` or ``conda env export`` depending on which was used to install Zarr.
zarr-python-3.3.0/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000001222 15232676200 0021525 0 ustar 00root root 0000000 0000000 blank_issues_enabled: true
contact_links:
- name: Propose a new Zarr specification feature
url: https://github.com/zarr-developers/zarr-specs
about: A new feature for the Zarr storage specification should be opened on the zarr-specs repository.
- name: Discuss something on ZulipChat
url: https://ossci.zulipchat.com/
about: For questions like "How do I do X with Zarr?", consider posting your question to our developer chat.
- name: Discuss something on GitHub Discussions
url: https://github.com/zarr-developers/zarr-python/discussions
about: For questions like "How do I do X with Zarr?", you can move to GitHub Discussions.
zarr-python-3.3.0/.github/ISSUE_TEMPLATE/documentation.yml 0000664 0000000 0000000 00000001202 15232676200 0023127 0 ustar 00root root 0000000 0000000 name: Documentation Improvement
description: Report missing or wrong documentation. Alternatively, you can just open a pull request with the suggested change.
title: "DOC: "
labels: [documentation, help wanted]
body:
- type: textarea
attributes:
label: Describe the issue linked to the documentation
description: >
Please provide a description of what documentation you believe needs to be fixed/improved.
validations:
required: true
- type: textarea
attributes:
label: Suggested fix for documentation
description: >
Please explain the suggested fix and why it's better than the existing documentation.
zarr-python-3.3.0/.github/ISSUE_TEMPLATE/feature_request.yml 0000664 0000000 0000000 00000000511 15232676200 0023463 0 ustar 00root root 0000000 0000000 name: Feature Request
description: Request a new feature for zarr-python
# labels: []
body:
- type: textarea
attributes:
label: Describe the new feature you'd like
description: >
Please provide a description of what new feature or functionality you'd like to see in zarr-python.
validations:
required: true
zarr-python-3.3.0/.github/ISSUE_TEMPLATE/release-checklist.md 0000664 0000000 0000000 00000005706 15232676200 0023461 0 ustar 00root root 0000000 0000000 ---
name: Zarr-Python release checklist
about: Checklist for a new Zarr-Python release. [For project maintainers only!]
title: Release Zarr-Python vX.Y.Z
labels: release-checklist
assignees: ''
---
**Release**: [v3.x.x](https://github.com/zarr-developers/zarr-python/milestones/?)
**Scheduled Date**: 20YY/MM/DD
**Priority PRs/issues to complete prior to release**
- [ ] Priority pull request #X
**Before release**:
- [ ] Check [SPEC 0](https://scientific-python.org/specs/spec-0000/#support-window) to see if the minimum supported version of Python or NumPy needs bumping.
- [ ] Verify that the latest CI workflows on `main` are passing: [Tests](https://github.com/zarr-developers/zarr-python/actions/workflows/test.yml), [GPU Tests](https://github.com/zarr-developers/zarr-python/actions/workflows/gpu_test.yml), [Hypothesis](https://github.com/zarr-developers/zarr-python/actions/workflows/hypothesis.yaml), [Docs](https://github.com/zarr-developers/zarr-python/actions/workflows/docs.yml), [Lint](https://github.com/zarr-developers/zarr-python/actions/workflows/lint.yml), [Wheels](https://github.com/zarr-developers/zarr-python/actions/workflows/releases.yml).
- [ ] Run the [downstream tests](https://github.com/zarr-developers/zarr-python/actions/workflows/downstream.yml) against `main`: go to the workflow page, click "Run workflow", and select the `main` branch. Verify that the Xarray and numcodecs integration tests pass.
- [ ] Open a release PR with the changelog entries for the upcoming release, generated with `uv run --only-group release towncrier build --version x.y.z`.
- [ ] Review the release PR and verify the changelog in `docs/release-notes.md` looks correct.
- [ ] Merge the release PR.
**Release**:
- [ ] [Draft a new GitHub Release](https://github.com/zarr-developers/zarr-python/releases/new) with tag `vX.Y.Z` targeting `main`. Use "Generate release notes" for the description.
- [ ] Verify the release is published on [PyPI](https://pypi.org/project/zarr/) and [ReadTheDocs](https://zarr.readthedocs.io/en/stable/).
**After release**:
- [ ] Review and merge the pull request on the conda-forge [zarr-feedstock](https://github.com/conda-forge/zarr-feedstock) that will be automatically generated.
---
- [ ] Party :tada:
---
Releasing from a branch other than main
In rare cases (e.g. patch releases for an older minor version), you may need to release from a dedicated release branch (e.g. `3.1.x`):
- Create the release branch from the appropriate tag if it doesn't already exist.
- Cherry-pick or backport the necessary commits onto the branch.
- Run `towncrier build --version x.y.z` and open the release PR against the release branch instead of `main`.
- Run the downstream tests against the release branch instead of `main`.
- When drafting the GitHub Release, set the target to the release branch instead of `main`.
- After the release, ensure any relevant changelog updates are also reflected on `main`.
zarr-python-3.3.0/.github/PULL_REQUEST_TEMPLATE.md 0000664 0000000 0000000 00000002540 15232676200 0021157 0 ustar 00root root 0000000 0000000
## Summary
[Describe what this PR changes and why, in your own words.]
## For reviewers
[What would you most value a second look at? What are you already confident in? For a refactor, say whether behavior is meant to be unchanged.]
## Author attestation
- [ ] I am a human, these are my changes, and I have reviewed and understood every change and can explain why each is correct.
## TODO
* [ ] Add unit tests and/or doctests in docstrings
* [ ] Add docstrings and API docs for any new/modified user-facing classes and functions
* [ ] New/modified features documented in `docs/user-guide/*.md`
* [ ] Changes documented as a new file in `changes/`
* [ ] GitHub Actions have all passed
* [ ] Test coverage is 100% (Codecov passes)
zarr-python-3.3.0/.github/dependabot.yml 0000664 0000000 0000000 00000001615 15232676200 0020210 0 ustar 00root root 0000000 0000000 ---
version: 2
updates:
# Updates for main
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "*"
cooldown:
default-days: 7
# Keep the pinned dev tooling in pyproject.toml's [dependency-groups] and the
# uv.lock current. Without this the exact pins (e.g. pytest) would never be
# bumped automatically and would silently rot.
#
# `allow: dependency-type: direct` restricts updates to dependencies declared
# in pyproject.toml. Transitive deps in uv.lock are then only updated as a
# side effect of a direct bump, never via a standalone PR.
- package-ecosystem: "uv"
directory: "/"
allow:
- dependency-type: "direct"
schedule:
interval: "weekly"
groups:
python-dependencies:
patterns:
- "*"
cooldown:
default-days: 7
zarr-python-3.3.0/.github/labeler.yml 0000664 0000000 0000000 00000000143 15232676200 0017504 0 ustar 00root root 0000000 0000000 needs release notes:
- all:
- changed-files:
- all-globs-to-all-files: '!changes/*.md'
zarr-python-3.3.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15232676200 0017412 5 ustar 00root root 0000000 0000000 zarr-python-3.3.0/.github/workflows/check_changelogs.yml 0000664 0000000 0000000 00000001445 15232676200 0023410 0 ustar 00root root 0000000 0000000 name: Check changelog entries
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check-changelogs:
name: Check changelog entries
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- name: Check zarr-python changelog entries
run: uv run --no-sync python ci/check_changelog_entries.py
- name: Check zarr-metadata changelog entries
run: uv run --no-sync python ci/check_changelog_entries.py packages/zarr-metadata/changes
zarr-python-3.3.0/.github/workflows/codspeed.yml 0000664 0000000 0000000 00000002147 15232676200 0021727 0 ustar 00root root 0000000 0000000 name: CodSpeed Benchmarks
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9am UTC
pull_request:
types: [labeled]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
benchmarks:
name: Run benchmarks
runs-on: codspeed-macro
if: |
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'benchmark'))
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Install Hatch
uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc
with:
version: '1.16.5'
- name: Run the benchmarks
uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5
env:
ZARR_BENCHMARK_CLEAR_CACHE: '1'
with:
mode: walltime
run: hatch run test.py3.12-minimal:pytest tests/benchmarks --codspeed
zarr-python-3.3.0/.github/workflows/docs.yml 0000664 0000000 0000000 00000002541 15232676200 0021067 0 ustar 00root root 0000000 0000000 name: Docs
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
docs:
name: Check docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- run: uv sync --group docs
# Fast source-level guards that need no built site, so they run before the (slower)
# build for a quick failure: every public export is in the API reference, and no
# docstring/Markdown carries reStructuredText markup that MkDocs won't render.
- run: uv run python ci/check_documented_exports.py docs/api
- run: uv run python ci/lint_docs.py
# --strict turns warnings into errors, so a docs code block that fails to execute
# at build time (e.g. a non-exec python fence disrupting a later exec="true" block)
# fails CI instead of merging as a silent warning.
- run: uv run mkdocs build --strict
env:
DISABLE_MKDOCS_2_WARNING: "true"
NO_MKDOCS_2_WARNING: "true"
- run: uv run python ci/check_unlinked_types.py
continue-on-error: true
zarr-python-3.3.0/.github/workflows/downstream.yml 0000664 0000000 0000000 00000010455 15232676200 0022325 0 ustar 00root root 0000000 0000000 name: Downstream
on:
workflow_dispatch:
pull_request:
types: [labeled, synchronize, opened, reopened]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
xarray:
name: Xarray zarr backend tests
if: |
github.event_name == 'workflow_dispatch'
|| contains(github.event.pull_request.labels.*.name, 'run-downstream')
runs-on: ubuntu-latest
steps:
- name: Check out zarr-python
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Check out xarray
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: pydata/xarray
path: xarray
persist-credentials: false
# We install xarray with plain pip/uv rather than pixi. pixi solves
# xarray's entire manifest (it has no committed lockfile), which drags in
# the `mypy-upstream` environment; that environment sources numcodecs from
# git and fails to build under newer pixi with
# `meson-python: error: Unknown option "pixi-conda-environment"`, breaking
# the job before any test runs. Tests that need a backend we don't install
# are skipped via xarray's `requires_*` markers, not failed.
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- name: Install xarray and test dependencies
working-directory: xarray
run: |
uv venv
# xarray's pytest tooling lives in the PEP 735 `dev` dependency group;
# the zarr-relevant backends come from the `io` and `parallel` extras.
uv pip install --group dev ".[io,parallel,accel]"
- name: Override zarr-python with branch version
working-directory: xarray
run: uv pip install --no-deps ..
- name: Show versions
working-directory: xarray
run: |
uv run python -c "
import zarr; print(f'zarr {zarr.__version__}')
import xarray; print(f'xarray {xarray.__version__}')
"
- name: Run xarray zarr backend tests
working-directory: xarray
run: |
uv run python -m pytest --no-header -q \
xarray/tests/test_backends.py \
xarray/tests/test_backends_api.py \
xarray/tests/test_backends_datatree.py
numcodecs:
name: numcodecs zarr3 codec tests
if: |
github.event_name == 'workflow_dispatch'
|| contains(github.event.pull_request.labels.*.name, 'run-downstream')
runs-on: ubuntu-latest
steps:
- name: Check out zarr-python
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Check out numcodecs
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: zarr-developers/numcodecs
fetch-depth: 0
path: numcodecs
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- name: Install numcodecs with test-zarr-main group
working-directory: numcodecs
run: |
uv venv
uv pip install --group dev
uv sync --group dev --group test-zarr-main
uv pip install --no-build-isolation -e .
- name: Override zarr-python with branch version
working-directory: numcodecs
run: uv pip install --no-deps ..
- name: Show versions
working-directory: numcodecs
run: |
uv run python -c "
import zarr; print(f'zarr {zarr.__version__}')
import numcodecs; print(f'numcodecs {numcodecs.__version__}')
"
- name: Run numcodecs zarr3 tests
working-directory: numcodecs
run: uv run python -m pytest -x --no-header -q tests/test_zarr3.py
zarr-python-3.3.0/.github/workflows/gpu_test.yml 0000664 0000000 0000000 00000005163 15232676200 0021774 0 ustar 00root root 0000000 0000000 # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: GPU Test
on:
push:
branches: [ main, 3.1.x ]
pull_request:
branches: [ main, 3.1.x ]
workflow_dispatch:
env:
LD_LIBRARY_PATH: /usr/local/cuda/extras/CUPTI/lib64:/usr/local/cuda/lib64
# Use the uv from astral-sh/setup-uv instead of hatch's bundled (pyapp) uv.
HATCH_ENV_TYPE_VIRTUAL_UV_PATH: uv
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: py=${{ matrix.python-version }}
environment:
name: codecov-upload
deployment: false
runs-on: gpu-runner
strategy:
matrix:
python-version: ['3.12']
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # grab all branches and tags
persist-credentials: false
# - name: cuda-toolkit
# uses: Jimver/cuda-toolkit@v0.2.16
# id: cuda-toolkit
# with:
# cuda: '12.4.1'
- name: Set up CUDA
run: |
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get -y install cuda-toolkit-12-6
echo "/usr/local/cuda/bin" >> $GITHUB_PATH
- name: GPU check
run: |
nvidia-smi
echo $PATH
echo $LD_LIBRARY_PATH
nvcc -V
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- name: Install Hatch
uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc
with:
version: '1.16.5'
- name: Set Up Hatch Env
env:
HATCH_ENV: gputest.py${{ matrix.python-version }}
run: |
hatch env create "$HATCH_ENV"
hatch env run -e "$HATCH_ENV" list-env
- name: Run Tests
env:
HATCH_ENV: gputest.py${{ matrix.python-version }}
run: |
hatch env run --env "$HATCH_ENV" run-coverage
- name: Upload coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: gpu
verbose: true # optional (default = false)
zarr-python-3.3.0/.github/workflows/hypothesis.yaml 0000664 0000000 0000000 00000007375 15232676200 0022511 0 ustar 00root root 0000000 0000000 name: Slow Hypothesis CI
on:
push:
branches: [main, 3.1.x]
pull_request:
branches: [main, 3.1.x]
types: [opened, reopened, synchronize, labeled]
schedule:
- cron: "0 0 * * *" # Daily “At 00:00” UTC
workflow_dispatch: # allows you to trigger manually
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 3
# Use the uv from astral-sh/setup-uv instead of hatch's bundled (pyapp) uv.
HATCH_ENV_TYPE_VIRTUAL_UV_PATH: uv
jobs:
hypothesis:
name: Slow Hypothesis Tests
environment:
name: codecov-upload
deployment: false
runs-on: "ubuntu-latest"
defaults:
run:
shell: bash -l {0}
strategy:
matrix:
python-version: ['3.12']
dependency-set: ["optional"]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set HYPOTHESIS_PROFILE based on trigger
env:
EVENT_NAME: ${{ github.event_name }}
run: |
if [[ "$EVENT_NAME" == "schedule" || "$EVENT_NAME" == "workflow_dispatch" ]]; then
echo "HYPOTHESIS_PROFILE=nightly" >> $GITHUB_ENV
else
echo "HYPOTHESIS_PROFILE=ci" >> $GITHUB_ENV
fi
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- name: Install Hatch
uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc
with:
version: '1.16.5'
- name: Set Up Hatch Env
env:
HATCH_ENV: test.py${{ matrix.python-version }}-${{ matrix.dependency-set }}
run: |
hatch env create "$HATCH_ENV"
hatch env run -e "$HATCH_ENV" list-env
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#update-a-cache
- name: Restore cached hypothesis directory
id: restore-hypothesis-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .hypothesis/
key: cache-hypothesis-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
cache-hypothesis-
- name: Run slow Hypothesis tests
if: success()
id: status
env:
HATCH_ENV: test.py${{ matrix.python-version }}-${{ matrix.dependency-set }}
run: |
echo "Using Hypothesis profile: $HYPOTHESIS_PROFILE"
hatch env run --env "$HATCH_ENV" run-hypothesis
# explicitly save the cache so it gets updated, also do this even if it fails.
- name: Save cached hypothesis directory
id: save-hypothesis-cache
if: always() && steps.status.outcome != 'skipped'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .hypothesis/
key: cache-hypothesis-${{ runner.os }}-${{ github.run_id }}
- name: Upload coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: tests
verbose: true # optional (default = false)
- name: Generate and publish the report
if: |
failure()
&& steps.status.outcome == 'failure'
&& github.event_name == 'schedule'
&& github.repository_owner == 'zarr-developers'
uses: scientific-python/issue-from-pytest-log-action@87351a8f864e969567cda22a25a2f214cbe2340f # v1.6.0
with:
log-path: output-${{ matrix.python-version }}-log.jsonl
issue-title: "Nightly Hypothesis tests failed"
issue-label: "topic-hypothesis"
zarr-python-3.3.0/.github/workflows/issue-metrics.yml 0000664 0000000 0000000 00000002673 15232676200 0022741 0 ustar 00root root 0000000 0000000 name: Monthly issue metrics
on:
workflow_dispatch:
schedule:
- cron: '3 2 1 * *'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: issue metrics
runs-on: ubuntu-latest
permissions:
issues: write # Required to create the metrics report issue
pull-requests: read # Required to read PR metrics
steps:
- name: Get dates for last month
shell: bash
run: |
# Calculate the first day of the previous month
first_day=$(date -d "last month" +%Y-%m-01)
# Calculate the last day of the previous month
last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d)
#Set an environment variable with the date range
echo "$first_day..$last_day"
echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV"
- name: Run issue-metrics tool
uses: github-community-projects/issue-metrics@df8c49d20958f9345281fa2124858bd0ad227e1f # v5.0.0
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SEARCH_QUERY: 'repo:zarr-developers/zarr-python is:issue created:${{ env.last_month }} -reason:"not planned"'
- name: Create issue
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0
with:
title: Monthly issue metrics report
token: ${{ secrets.GITHUB_TOKEN }}
content-filepath: ./issue_metrics.md
zarr-python-3.3.0/.github/workflows/links.yml 0000664 0000000 0000000 00000001640 15232676200 0021256 0 ustar 00root root 0000000 0000000 name: Check links
on:
repository_dispatch:
workflow_dispatch:
# pull_request:
schedule:
- cron: "00 18 * * 1" # weekly, Mondays at 18:00 UTC
jobs:
linkChecker:
runs-on: ubuntu-latest
permissions:
issues: write # required for peter-evans/create-issue-from-file
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Link Checker
id: lychee
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
with:
fail: false
- name: Create Issue From File
if: steps.lychee.outputs.exit_code != 0
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0
with:
title: Link Checker Report
content-filepath: ./lychee/out.md
labels: report, automated issue
zarr-python-3.3.0/.github/workflows/lint.yml 0000664 0000000 0000000 00000001477 15232676200 0021114 0 ustar 00root root 0000000 0000000 name: Lint
on:
push:
branches: [main, 3.1.x]
pull_request:
branches: [main, 3.1.x]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
- uses: j178/prek-action@e98a699c41eb69ab013a45817a0406469a748f8d # v2.0.5
zarr-python-3.3.0/.github/workflows/needs_release_notes.yml 0000664 0000000 0000000 00000001637 15232676200 0024152 0 ustar 00root root 0000000 0000000 name: "Pull Request Labeler"
on:
# pull_request_target is needed to label PRs from forks.
# This workflow only runs actions/labeler (no code checkout), so it's safe.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types: [opened, reopened, synchronize]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
labeler:
name: Label pull request
if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'pre-commit-ci[bot]' }}
permissions:
contents: read # Required to read label configuration
pull-requests: write # Required to add labels to PRs
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
sync-labels: true
zarr-python-3.3.0/.github/workflows/nightly_wheels.yml 0000664 0000000 0000000 00000002315 15232676200 0023163 0 ustar 00root root 0000000 0000000 name: Nightly Wheels
on:
schedule:
# Run nightly at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_and_upload_nightly:
name: Build and upload nightly wheels
environment:
name: nightly-wheel-upload
deployment: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
submodules: true
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
name: Install Python
with:
python-version: '3.14'
- name: Install Hatch
uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc
with:
version: '1.16.5'
- name: Build wheel and sdist
run: hatch build
- name: Upload nightly wheels
uses: scientific-python/upload-nightly-action@e76cfec8a4611fd02808a801b0ff5a7d7c1b2d99
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
zarr-python-3.3.0/.github/workflows/releases.yml 0000664 0000000 0000000 00000004456 15232676200 0021751 0 ustar 00root root 0000000 0000000 name: Wheels
on:
release:
types:
- published
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_artifacts:
name: Build wheel on ubuntu-latest
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
submodules: true
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
name: Install Python
with:
python-version: '3.12'
- name: Install Hatch
uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc
with:
version: '1.16.5'
- name: Build wheel and sdist
run: hatch build
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: releases
path: dist
test_dist_pypi:
name: Test distribution artifacts
needs: [build_artifacts]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: releases
path: dist
- name: test
run: |
ls
ls dist
upload_pypi:
name: Upload to PyPI
needs: [build_artifacts, test_dist_pypi]
runs-on: ubuntu-latest
if: github.event_name == 'release'
environment:
name: releases
url: https://pypi.org/p/zarr
permissions:
id-token: write # Required for OIDC trusted publishing to PyPI
attestations: write # Required for artifact attestation
artifact-metadata: write # Required for artifact attestation metadata
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: releases
path: dist
- name: Generate artifact attestation
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with:
subject-path: dist/*
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
zarr-python-3.3.0/.github/workflows/test.yml 0000664 0000000 0000000 00000014333 15232676200 0021120 0 ustar 00root root 0000000 0000000 # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Test
on:
push:
branches: [ main, 3.1.x ]
pull_request:
branches: [ main, 3.1.x ]
workflow_dispatch:
permissions:
contents: read
env:
# Use the uv from astral-sh/setup-uv; without an explicit path hatch
# bootstraps its own (pyapp) uv, which fails on non-3.12 runners.
HATCH_ENV_TYPE_VIRTUAL_UV_PATH: uv
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: os=${{ matrix.os }}, py=${{ matrix.python-version }}, deps=${{ matrix.dependency-set }}
environment:
name: codecov-upload
deployment: false
defaults:
run:
shell: bash
strategy:
matrix:
python-version: ['3.12', '3.13', '3.14']
dependency-set: ["minimal", "optional"]
os: ["ubuntu-latest"]
include:
- python-version: '3.12'
dependency-set: 'optional'
os: 'macos-latest'
- python-version: '3.14'
dependency-set: 'optional'
os: 'macos-latest'
- python-version: '3.12'
dependency-set: 'optional'
os: 'windows-latest'
- python-version: '3.14'
dependency-set: 'optional'
os: 'windows-latest'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # grab all branches and tags
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- name: Install Hatch
run: python -m pip install hatch==1.16.5
- name: Set Up Hatch Env
env:
HATCH_ENV: test.py${{ matrix.python-version }}-${{ matrix.dependency-set }}
run: |
hatch env create "$HATCH_ENV"
hatch env run -e "$HATCH_ENV" list-env
- name: Run Tests
env:
HYPOTHESIS_PROFILE: ci
HATCH_ENV: test.py${{ matrix.python-version }}-${{ matrix.dependency-set }}
run: |
hatch env run --env "$HATCH_ENV" run-coverage
- name: Upload coverage
if: ${{ matrix.dependency-set == 'optional' && matrix.os == 'ubuntu-latest' }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: tests
verbose: true # optional (default = false)
test-upstream-and-min-deps:
name: py=${{ matrix.python-version }}-${{ matrix.dependency-set }}
environment:
name: codecov-upload
deployment: false
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.12', "3.14"]
dependency-set: ["upstream", "min_deps"]
exclude:
- python-version: "3.14"
dependency-set: min_deps
- python-version: "3.12"
dependency-set: upstream
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- name: Install Hatch
run: python -m pip install hatch==1.16.5
- name: Set Up Hatch Env
env:
HATCH_ENV: ${{ matrix.dependency-set }}
run: |
hatch env create "$HATCH_ENV"
hatch env run -e "$HATCH_ENV" list-env
- name: Run Tests
env:
HATCH_ENV: ${{ matrix.dependency-set }}
run: |
hatch env run --env "$HATCH_ENV" run-coverage
- name: Upload coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: tests
verbose: true # optional (default = false)
doctests:
name: doctests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # required for hatch version discovery, which is needed for numcodecs.zarr3
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.13'
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- name: Install Hatch
run: python -m pip install hatch==1.16.5
- name: Set Up Hatch Env
run: |
hatch run doctest:pip list
- name: Run Tests
run: |
hatch run doctest:test
benchmarks:
name: Benchmark smoke test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.13'
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- name: Install Hatch
run: python -m pip install hatch==1.16.5
- name: Run Benchmarks
env:
ZARR_BENCHMARK_CLEAR_CACHE: '1'
run: |
hatch env run --env "test.py3.13-minimal" run-benchmark
test-complete:
name: Test complete
needs:
[
test,
test-upstream-and-min-deps,
doctests,
benchmarks
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check failure
if: |
contains(needs.*.result, 'failure') ||
contains(needs.*.result, 'cancelled')
run: exit 1
- name: Success
run: echo Success!
zarr-python-3.3.0/.github/workflows/zarr-metadata-release.yml 0000664 0000000 0000000 00000006700 15232676200 0024312 0 ustar 00root root 0000000 0000000 name: zarr-metadata release
on:
workflow_dispatch:
push:
tags:
- 'zarr_metadata-v*'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
name: Build wheel and sdist
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: packages/zarr-metadata
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0 # hatch-vcs needs full history + tags
- name: Install Hatch
uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc
with:
version: '1.16.5'
- name: Build
run: hatch build
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: zarr-metadata-dist
path: packages/zarr-metadata/dist
test_artifacts:
name: Test built artifacts
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: zarr-metadata-dist
path: dist
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: false
- name: Set up Python
run: uv python install 3.12
- name: Install built wheel and run import smoke test
run: |
wheel=$(ls dist/*.whl)
uv run --with "${wheel}" --python 3.12 --no-project \
python -c "import zarr_metadata; print('zarr_metadata', zarr_metadata.__version__)"
upload_pypi:
name: Upload to PyPI
needs: [build, test_artifacts]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/zarr_metadata-v')
runs-on: ubuntu-latest
environment:
name: zarr-metadata-releases
url: https://pypi.org/p/zarr-metadata
permissions:
id-token: write # required for OIDC trusted publishing
attestations: write # required for artifact attestations
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: zarr-metadata-dist
path: dist
- name: Generate artifact attestation
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with:
subject-path: dist/*
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
upload_testpypi:
name: Upload to TestPyPI
needs: [build, test_artifacts]
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
environment:
name: zarr-metadata-releases-test
url: https://test.pypi.org/p/zarr-metadata
permissions:
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: zarr-metadata-dist
path: dist
- name: Generate artifact attestation
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with:
subject-path: dist/*
- name: Publish package to TestPyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
repository-url: https://test.pypi.org/legacy/
zarr-python-3.3.0/.github/workflows/zarr-metadata.yml 0000664 0000000 0000000 00000007226 15232676200 0022700 0 ustar 00root root 0000000 0000000 name: zarr-metadata
# Job steps delegate to packages/zarr-metadata/justfile, the single source of
# truth for this package's verbs; CI owns only the python matrix and caching.
on:
push:
branches: [main]
paths:
- 'packages/zarr-metadata/**'
- '.github/workflows/zarr-metadata.yml'
pull_request:
paths:
- 'packages/zarr-metadata/**'
- '.github/workflows/zarr-metadata.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: pytest py=${{ matrix.python-version }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: packages/zarr-metadata
strategy:
fail-fast: false
matrix:
python-version: ['3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Sync test dependency group
run: uv sync --group test --python ${{ matrix.python-version }}
- name: Run pytest
run: just test
ruff:
name: ruff
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: packages/zarr-metadata
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
- name: Run ruff
run: just lint
pyright:
name: pyright
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: packages/zarr-metadata
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
- name: Run pyright
# The pyright version and interpreter pins live in the justfile.
run: just typecheck
docs:
name: docs
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: packages/zarr-metadata
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
- name: Build docs
run: just docs-check
zarr-metadata-complete:
name: zarr-metadata complete
needs: [test, ruff, pyright, docs]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check failure
if: |
contains(needs.*.result, 'failure') ||
contains(needs.*.result, 'cancelled')
run: exit 1
- name: Success
run: echo Success!
zarr-python-3.3.0/.github/workflows/zizmor.yml 0000664 0000000 0000000 00000001502 15232676200 0021465 0 ustar 00root root 0000000 0000000 name: GitHub Actions Security Analysis
on:
push:
branches: [main]
paths:
- '.github/workflows/**'
- '.github/actions/**'
pull_request:
branches: ["**"]
paths:
- '.github/workflows/**'
- '.github/actions/**'
workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
zizmor:
name: Run zizmor
runs-on: ubuntu-latest
permissions:
security-events: write # Required by zizmor-action to upload SARIF files
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0
zarr-python-3.3.0/.gitignore 0000664 0000000 0000000 00000002234 15232676200 0016006 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
.venv/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.coverage
.coverage.*
.cache
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Documentation
site/
docs/_build/
docs/data
data
data.zip
# PyBuilder
target/
# PyCharm
.idea
# Jupyter
.ipynb_checkpoints/
# VCS versioning
src/zarr/_version.py
# emacs
*~
# VSCode
.vscode/
# test data
#*.zarr
#*.zip
#example*
#doesnotexist
#test_sync*
data/*
src/fixture/
fixture/
junit.xml
.DS_Store
tests/.hypothesis
.hypothesis/
zarr/version.py
zarr.egg-info/
# Local agent / planning notes (not versioned)
.claude/
CLAUDE.md
docs/superpowers/
# zarr-metadata package lockfile (a library, not an app)
packages/zarr-metadata/uv.lock
zarr-python-3.3.0/.markdownlint-cli2.jsonc 0000664 0000000 0000000 00000004665 15232676200 0020504 0 ustar 00root root 0000000 0000000 // markdownlint-cli2 configuration for zarr-python docs.
//
// We keep the rules that catch real rendering/structure problems and disable those that
// are pure style, conflict with house conventions, or fire false positives against our
// MkDocs/mkdocstrings + pymdownx toolchain. Complementary, not overlapping, with
// ci/lint_docs.py (RST residue + list-breaking fences) and `mkdocs build --strict`.
{
"config": {
"default": true,
// House style: Markdown paragraphs are single unwrapped lines, so line length is not
// a meaningful constraint.
"MD013": false,
// Purely stylistic marker/emphasis choices -- not worth the churn across existing docs.
"MD004": false, // ul bullet style (-, *, +)
"MD007": false, // ul indentation width
"MD050": false, // strong (bold) style
"MD035": false, // hr style
// False positives from our toolchain:
// mkdocstrings cross-refs `[`X`][zarr.X]` read as undefined reference links (MD052);
// pymdownx.magiclink auto-links bare URLs (MD034);
// md_in_html lets us embed intentional raw HTML (MD033);
// generated/included files (api stubs, snippets) need not open with an H1 (MD041).
"MD052": false,
"MD034": false,
"MD033": false,
"MD041": false,
// Duplicate headings are legitimate under different sections (e.g. repeated
// "Documentation"); only flag true sibling duplicates.
"MD024": { "siblings_only": true },
// Opinionated table/link/command rules with low value for these docs.
"MD055": false, // table pipe style
"MD060": false, // table column style
"MD059": false, // "descriptive" link text (no "click here")
"MD014": false, // $ before commands without shown output
// markdownlint does not understand MkDocs `!!!` admonitions, so it reads their
// 4-space-indented bodies as indented code blocks and flags them (and, via inferred
// file style, flags real fenced blocks too). Cannot coexist with our admonitions.
"MD046": false // code block style (fenced vs indented)
// Kept on (structural / real rendering bugs): MD012 (multiple blanks), MD022/MD031/MD032
// (blanks around headings/fences/lists), MD025 (single H1), MD029 (ordered-list prefix),
// MD040 (fenced code language), MD042 (empty links),
// MD047 (trailing newline), MD056 (table column count), among others.
},
"globs": ["docs/**/*.md"],
"ignores": [
"docs/api/**" // mkdocstrings stubs (`::: zarr.X`)
]
}
zarr-python-3.3.0/.pre-commit-config.yaml 0000664 0000000 0000000 00000004645 15232676200 0020307 0 ustar 00root root 0000000 0000000 ci:
autoupdate_commit_msg: "chore: update pre-commit hooks"
autoupdate_schedule: "monthly"
autofix_prs: false
# 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]
default_language_version:
python: python3.12
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20
hooks:
- id: ruff-check
args: ["--fix", "--show-fixes"]
- id: ruff-format
- repo: https://github.com/codespell-project/codespell
rev: v2.4.2
hooks:
- id: codespell
args: ["-L", "fo,ihs,kake,te", "-S", "fixture"]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-yaml
exclude: mkdocs.yml
- id: trailing-whitespace
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.22.1
hooks:
# Markdown structure/hygiene. Rule selection and ignores are in
# .markdownlint-cli2.jsonc; complements ci/lint_docs.py (RST residue,
# list-breaking fences) and `mkdocs build --strict`. Scoped to docs/ to
# match the config's globs (pre-commit passes filenames, which would
# otherwise override that scoping and lint all repo Markdown).
- id: markdownlint-cli2
files: ^docs/
- repo: local
hooks:
- id: mypy
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.06.18
hooks:
- id: sp-repo-review
- repo: https://github.com/numpy/numpydoc
rev: v1.11.0rc0
hooks:
- id: numpydoc-validation
- repo: local
hooks:
- id: ban-lstrip-rstrip
name: ban lstrip/rstrip
language: pygrep
# Matches .lstrip() or .rstrip() where the string argument is 2+ characters.
entry: "\\.(lstrip|rstrip)\\([\"'][^\"']{2,}[\"']\\)"
types: [python]
files: ^(src|tests)/
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.26.1
hooks:
- id: zizmor
- repo: https://github.com/twisted/towncrier
rev: 25.8.0
hooks:
- id: towncrier-check
zarr-python-3.3.0/.python-version 0000664 0000000 0000000 00000000005 15232676200 0017015 0 ustar 00root root 0000000 0000000 3.12
zarr-python-3.3.0/.pyup.yml 0000664 0000000 0000000 00000000513 15232676200 0015612 0 ustar 00root root 0000000 0000000 # pyup.io config file
# see https://pyup.io/docs/configuration/ for all available options
schedule: every month
requirements:
- requirements_dev_minimal.txt:
pin: True
update: all
- requirements_dev_numpy.txt:
pin: True
update: all
- requirements_dev_optional.txt:
pin: True
update: all
zarr-python-3.3.0/.readthedocs.yaml 0000664 0000000 0000000 00000002140 15232676200 0017241 0 ustar 00root root 0000000 0000000 version: 2
build:
os: ubuntu-22.04
tools:
python: "3.12"
jobs:
post_checkout:
# Cancel pull request builds whose changes are confined to the
# zarr-metadata package, which has its own Read the Docs project. Exit
# code 183 cancels the build and reports success to the Git provider.
# Scoped to PR builds ("external" versions) because origin/main is only
# a meaningful diff base there. Read the Docs strips shell quoting from
# commands, so the exclude pathspec must use the quote-free :! form,
# not ':(exclude)'.
- |
if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- :!packages/zarr-metadata;
then
exit 183;
fi
install:
- pip install --upgrade pip
- pip install .[remote] --group docs
pre_build:
- |
if [ "$READTHEDOCS_VERSION_TYPE" != "tag" ];
then
towncrier build --version Unreleased --yes;
fi
build:
html:
- mkdocs build --strict --site-dir $READTHEDOCS_OUTPUT/html
mkdocs:
configuration: mkdocs.yml
zarr-python-3.3.0/FUNDING.yml 0000664 0000000 0000000 00000000103 15232676200 0015624 0 ustar 00root root 0000000 0000000 github: [numfocus]
custom: ['https://numfocus.org/donate-to-zarr']
zarr-python-3.3.0/LICENSE.txt 0000664 0000000 0000000 00000002144 15232676200 0015641 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright (c) 2015-2025 Zarr Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
zarr-python-3.3.0/README.md 0000664 0000000 0000000 00000005206 15232676200 0015277 0 ustar 00root root 0000000 0000000
# Zarr
[](https://pypi.org/project/zarr/)
[](https://anaconda.org/anaconda/zarr/)
[](https://pypi.org/project/zarr/)
[](https://github.com/zarr-developers/zarr-python/blob/main/LICENSE.txt)
[](https://app.codecov.io/gh/zarr-developers/zarr-python)
[](https://zarr.readthedocs.io/en/stable/)
[](https://ossci.zulipchat.com/#narrow/channel/423692-Zarr-Python)
[](https://doi.org/10.5281/zenodo.3773450)
## What is it?
Zarr is a Python package providing an implementation of compressed, chunked, N-dimensional arrays, designed for use in parallel computing. See the [documentation](https://zarr.readthedocs.io/en/stable/) for more information.
## Main Features
- [**Create**](https://zarr.readthedocs.io/en/stable/user-guide/arrays/#creating-an-array) N-dimensional arrays with any NumPy `dtype`.
- [**Chunk arrays**](https://zarr.readthedocs.io/en/stable/user-guide/performance/#chunk-optimizations) along any dimension.
- [**Compress**](https://zarr.readthedocs.io/en/stable/user-guide/arrays/#compressors) and/or filter chunks using any NumCodecs codec.
- [**Store arrays**](https://zarr.readthedocs.io/en/stable/user-guide/storage/) in memory, on disk, inside a zip file, on S3, etc...
- [**Read**](https://zarr.readthedocs.io/en/stable/user-guide/arrays/#reading-and-writing-data) an array [**concurrently**](https://zarr.readthedocs.io/en/stable/user-guide/performance/#parallel-computing-and-synchronization) from multiple threads or processes.
- [**Write**](https://zarr.readthedocs.io/en/stable/user-guide/arrays/#reading-and-writing-data) to an array concurrently from multiple threads or processes.
- Organize arrays into hierarchies via [**groups**](https://zarr.readthedocs.io/en/stable/quick-start/#hierarchical-groups).
## Where to get it
Zarr can be installed from PyPI using `pip`:
```bash
pip install zarr
```
or via `conda`:
```bash
conda install -c conda-forge zarr
```
For more details, including how to install from source, see the [installation documentation](https://zarr.readthedocs.io/en/stable/#installation).
zarr-python-3.3.0/TEAM.md 0000664 0000000 0000000 00000001403 15232676200 0015063 0 ustar 00root root 0000000 0000000 ## Active core-developers
- @joshmoore (Josh Moore)
- @jni (Juan Nunez-Iglesias)
- @rabernat (Ryan Abernathey)
- @jhamman (Joe Hamman)
- @d-v-b (Davis Bennett)
- @jakirkham (jakirkham)
- @martindurant (Martin Durant)
- @normanrz (Norman Rzepka)
- @dstansby (David Stansby)
- @dcherian (Deepak Cherian)
- @TomAugspurger (Tom Augspurger)
- @maxrjones (Max Jones)
- @ilan-gold (Ilan Gold)
## Emeritus core-developers
- @alimanfoo (Alistair Miles)
- @shoyer (Stephan Hoyer)
- @ryan-williams (Ryan Williams)
- @jrbourbeau (James Bourbeau)
- @mzjp2 (Zain Patel)
- @grlee77 (Gregory Lee)
## Former core-developers
- @jeromekelleher (Jerome Kelleher)
- @tjcrone (Tim Crone)
- @funkey (Jan Funke)
- @shikharsg
- @Carreau (Matthias Bussonnier)
- @dazzag24
- @WardF (Ward Fisher)
zarr-python-3.3.0/bench/ 0000775 0000000 0000000 00000000000 15232676200 0015074 5 ustar 00root root 0000000 0000000 zarr-python-3.3.0/bench/compress_normal.py 0000664 0000000 0000000 00000001655 15232676200 0020660 0 ustar 00root root 0000000 0000000 import sys
import timeit
import blosc
import line_profiler
import numpy as np
import zarr
if __name__ == "__main__":
sys.path.insert(0, "..")
# setup
a = np.random.normal(2000, 1000, size=200000000).astype("u2")
z = zarr.empty_like(
a,
chunks=1000000,
compression="blosc",
compression_opts={"cname": "lz4", "clevel": 5, "shuffle": 2},
)
print(z)
print("*" * 79)
# time
t = timeit.repeat("z[:] = a", repeat=10, number=1, globals=globals())
print(t)
print(min(t))
print(z)
# profile
profile = line_profiler.LineProfiler(blosc.compress)
profile.run("z[:] = a")
profile.print_stats()
print("*" * 79)
# time
t = timeit.repeat("z[:]", repeat=10, number=1, globals=globals())
print(t)
print(min(t))
# profile
profile = line_profiler.LineProfiler(blosc.decompress)
profile.run("z[:]")
profile.print_stats()
zarr-python-3.3.0/bench/compress_normal.txt 0000664 0000000 0000000 00000025234 15232676200 0021046 0 ustar 00root root 0000000 0000000 zarr.core.Array((200000000,), uint16, chunks=(1000000,), order=C)
compression: blosc; compression_opts: {'clevel': 5, 'cname': 'lz4', 'shuffle': 2}
nbytes: 381.5M; nbytes_stored: 294; ratio: 1360544.2; initialized: 0/200
store: builtins.dict
*******************************************************************************
[0.27119584499996563, 0.2855067059999783, 0.2887747180002407, 0.3058794240005227, 0.3139041080003153, 0.3021271820007314, 0.31543190899992624, 0.31403100900024583, 0.3272544129995367, 0.31834129100025166]
0.27119584499996563
zarr.core.Array((200000000,), uint16, chunks=(1000000,), order=C)
compression: blosc; compression_opts: {'clevel': 5, 'cname': 'lz4', 'shuffle': 2}
nbytes: 381.5M; nbytes_stored: 314.1M; ratio: 1.2; initialized: 200/200
store: builtins.dict
Timer unit: 1e-06 s
Total time: 0.297223 s
File: /home/aliman/code/github/alimanfoo/zarr/zarr/blosc.pyx
Function: compress at line 137
Line # Hits Time Per Hit % Time Line Contents
==============================================================
137 def compress(source, char* cname, int clevel, int shuffle):
138 """Compress data in a numpy array.
139
140 Parameters
141 ----------
142 source : array-like
143 Data to be compressed.
144 cname : bytes
145 Name of compression library to use.
146 clevel : int
147 Compression level.
148 shuffle : int
149 Shuffle filter.
150
151 Returns
152 -------
153 dest : bytes-like
154 Compressed data.
155
156 """
157
158 cdef:
159 char *source_ptr
160 char *dest_ptr
161 Py_buffer source_buffer
162 size_t nbytes, cbytes, itemsize
163 200 506 2.5 0.2 array.array char_array_template = array.array('b', [])
164 array.array dest
165
166 # setup source buffer
167 200 458 2.3 0.2 PyObject_GetBuffer(source, &source_buffer, PyBUF_ANY_CONTIGUOUS)
168 200 119 0.6 0.0 source_ptr = source_buffer.buf
169
170 # setup destination
171 200 239 1.2 0.1 nbytes = source_buffer.len
172 200 103 0.5 0.0 itemsize = source_buffer.itemsize
173 200 2286 11.4 0.8 dest = array.clone(char_array_template, nbytes + BLOSC_MAX_OVERHEAD,
174 zero=False)
175 200 129 0.6 0.0 dest_ptr = dest.data.as_voidptr
176
177 # perform compression
178 200 1734 8.7 0.6 if _get_use_threads():
179 # allow blosc to use threads internally
180 200 167 0.8 0.1 compressor_set = blosc_set_compressor(cname)
181 200 94 0.5 0.0 if compressor_set < 0:
182 raise ValueError('compressor not supported: %r' % cname)
183 200 288570 1442.8 97.1 with nogil:
184 cbytes = blosc_compress(clevel, shuffle, itemsize, nbytes,
185 source_ptr, dest_ptr,
186 nbytes + BLOSC_MAX_OVERHEAD)
187
188 else:
189 with nogil:
190 cbytes = blosc_compress_ctx(clevel, shuffle, itemsize, nbytes,
191 source_ptr, dest_ptr,
192 nbytes + BLOSC_MAX_OVERHEAD, cname,
193 0, 1)
194
195 # release source buffer
196 200 616 3.1 0.2 PyBuffer_Release(&source_buffer)
197
198 # check compression was successful
199 200 120 0.6 0.0 if cbytes <= 0:
200 raise RuntimeError('error during blosc compression: %d' % cbytes)
201
202 # resize after compression
203 200 1896 9.5 0.6 array.resize(dest, cbytes)
204
205 200 186 0.9 0.1 return dest
*******************************************************************************
[0.24293352799941204, 0.2324290420001489, 0.24935673900017719, 0.25716222699975333, 0.24246313799994823, 0.23272456500035332, 0.2636815870000646, 0.2576046349995522, 0.2781278639995435, 0.23824110699933954]
0.2324290420001489
Timer unit: 1e-06 s
Total time: 0.240178 s
File: /home/aliman/code/github/alimanfoo/zarr/zarr/blosc.pyx
Function: decompress at line 75
Line # Hits Time Per Hit % Time Line Contents
==============================================================
75 def decompress(source, dest):
76 """Decompress data.
77
78 Parameters
79 ----------
80 source : bytes-like
81 Compressed data, including blosc header.
82 dest : array-like
83 Object to decompress into.
84
85 Notes
86 -----
87 Assumes that the size of the destination buffer is correct for the size of
88 the uncompressed data.
89
90 """
91 cdef:
92 int ret
93 char *source_ptr
94 char *dest_ptr
95 Py_buffer source_buffer
96 array.array source_array
97 Py_buffer dest_buffer
98 size_t nbytes
99
100 # setup source buffer
101 200 573 2.9 0.2 if PY2 and isinstance(source, array.array):
102 # workaround fact that array.array does not support new-style buffer
103 # interface in PY2
104 release_source_buffer = False
105 source_array = source
106 source_ptr = source_array.data.as_voidptr
107 else:
108 200 112 0.6 0.0 release_source_buffer = True
109 200 144 0.7 0.1 PyObject_GetBuffer(source, &source_buffer, PyBUF_ANY_CONTIGUOUS)
110 200 98 0.5 0.0 source_ptr = source_buffer.buf
111
112 # setup destination buffer
113 200 552 2.8 0.2 PyObject_GetBuffer(dest, &dest_buffer,
114 PyBUF_ANY_CONTIGUOUS | PyBUF_WRITEABLE)
115 200 100 0.5 0.0 dest_ptr = dest_buffer.buf
116 200 84 0.4 0.0 nbytes = dest_buffer.len
117
118 # perform decompression
119 200 1856 9.3 0.8 if _get_use_threads():
120 # allow blosc to use threads internally
121 200 235286 1176.4 98.0 with nogil:
122 ret = blosc_decompress(source_ptr, dest_ptr, nbytes)
123 else:
124 with nogil:
125 ret = blosc_decompress_ctx(source_ptr, dest_ptr, nbytes, 1)
126
127 # release buffers
128 200 754 3.8 0.3 if release_source_buffer:
129 200 326 1.6 0.1 PyBuffer_Release(&source_buffer)
130 200 165 0.8 0.1 PyBuffer_Release(&dest_buffer)
131
132 # handle errors
133 200 128 0.6 0.1 if ret <= 0:
134 raise RuntimeError('error during blosc decompression: %d' % ret)
zarr-python-3.3.0/changes/ 0000775 0000000 0000000 00000000000 15232676200 0015425 5 ustar 00root root 0000000 0000000 zarr-python-3.3.0/changes/.gitignore 0000664 0000000 0000000 00000000014 15232676200 0017410 0 ustar 00root root 0000000 0000000 !.gitignore
zarr-python-3.3.0/changes/README.md 0000664 0000000 0000000 00000000575 15232676200 0016713 0 ustar 00root root 0000000 0000000 Writing a changelog entry
-------------------------
Please put a new file in this directory named `xxxx..md`, where
- `xxxx` is the pull request number associated with this entry
- `` is one of:
- feature
- bugfix
- doc
- removal
- misc
Inside the file, please write a short description of what you have changed, and how it impacts users of `zarr-python`.
zarr-python-3.3.0/ci/ 0000775 0000000 0000000 00000000000 15232676200 0014410 5 ustar 00root root 0000000 0000000 zarr-python-3.3.0/ci/check_changelog_entries.py 0000664 0000000 0000000 00000004307 15232676200 0021603 0 ustar 00root root 0000000 0000000 """
Check changelog entries have the correct filename structure.
Usage:
python check_changelog_entries.py [DIRECTORY]
DIRECTORY defaults to the repo-root `changes/`.
"""
import sys
from pathlib import Path
VALID_CHANGELOG_TYPES = ["feature", "bugfix", "doc", "removal", "misc"]
REPO_ROOT = Path(__file__).parent.parent.resolve()
DEFAULT_DIRECTORY = REPO_ROOT / "changes"
def is_int(s: str) -> bool:
try:
int(s)
except ValueError:
return False
else:
return True
def check(directory: Path) -> int:
print(f"Looking for changelog entries in {directory}")
entries = list(directory.glob("*"))
entries = [e for e in entries if e.name not in [".gitignore", "README.md"]]
print(f"Found {len(entries)} entries")
print()
bad_suffix = [e for e in entries if e.suffix != ".md"]
bad_issue_no = [e for e in entries if not is_int(e.name.split(".")[0])]
# Only flag bad_type for files that have already passed the prior two
# checks; otherwise `e.name.split(".")[1]` may raise IndexError on a
# malformed name like `notes.md`.
bad_type = [
e
for e in entries
if e.suffix == ".md"
and is_int(e.name.split(".")[0])
and e.name.split(".")[1] not in VALID_CHANGELOG_TYPES
]
if bad_suffix or bad_issue_no or bad_type:
if bad_suffix:
print("Changelog entries without .md suffix")
print("-------------------------------------")
print("\n".join(p.name for p in bad_suffix))
print()
if bad_issue_no:
print("Changelog entries without integer issue number")
print("----------------------------------------------")
print("\n".join(p.name for p in bad_issue_no))
print()
if bad_type:
print("Changelog entries without valid type")
print("------------------------------------")
print("\n".join(p.name for p in bad_type))
print(f"Valid types are: {VALID_CHANGELOG_TYPES}")
print()
return 1
return 0
if __name__ == "__main__":
directory = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else DEFAULT_DIRECTORY
sys.exit(check(directory))
zarr-python-3.3.0/ci/check_documented_exports.py 0000664 0000000 0000000 00000013530 15232676200 0022034 0 ustar 00root root 0000000 0000000 """Check that every public top-level export is in the API reference.
The API reference is authored as explicit mkdocstrings directives (``::: target``)
under ``docs/api/`` -- one per documented symbol -- rather than autodoc, so a newly
added ``zarr.__all__`` entry will not appear in the docs until someone writes a page
for it (or it becomes a rendered member of an already-documented module). This script
catches that gap: it resolves every ``:::`` target, expands module directives into the
members they render (honoring ``members: false``), and asserts each name in
``zarr.__all__`` resolves to a documented object.
Usage:
python ci/check_documented_exports.py [API_DOCS_DIR]
API_DOCS_DIR defaults to the repo-root ``docs/api``. Exits non-zero (and prints the
undocumented exports to stderr) if any public export is missing from the reference.
"""
from __future__ import annotations
import importlib
import re
import sys
from pathlib import Path
from types import ModuleType
from typing import TYPE_CHECKING, Any
import zarr
if TYPE_CHECKING:
from collections.abc import Iterator
REPO_ROOT = Path(__file__).parent.parent.resolve()
DEFAULT_API_DOCS_ROOT = REPO_ROOT / "docs" / "api"
# Names in zarr.__all__ that are intentionally absent from the API reference.
# Keep this list short and justified -- it is the only escape hatch from the guard.
EXEMPT_EXPORTS = {
"__version__", # version string, not an API symbol
"print_debug_info", # debugging helper, deliberately not in the reference
}
# A mkdocstrings autodoc directive: `::: some.dotted.target` at the start of a line.
DIRECTIVE_RE = re.compile(r"^:::[ \t]+(?P\S+)")
# `members: false` (or `members: []`) within a directive's option block disables
# rendering of a module's members.
MEMBERS_DISABLED_RE = re.compile(r"^\s+members:\s*(false|\[\s*\])\s*$")
def resolve(target: str) -> Any:
"""Resolve a `:::` target (a dotted path) to the Python object it documents."""
try:
return importlib.import_module(target)
except ImportError:
pass
module_path, _, attr = target.rpartition(".")
try:
return getattr(importlib.import_module(module_path), attr)
except (ImportError, AttributeError):
return None
def iter_directives(text: str) -> Iterator[tuple[str, bool]]:
"""Yield ``(target, members_enabled)`` for each ``:::`` directive in ``text``.
The file is split into lines once; for each directive we scan its indented option
block -- stopping at the first non-indented line, which ends the block -- so options
belonging to a later directive are never consulted. ``members_enabled`` is False when
that block sets ``members: false`` (or ``members: []``)."""
lines = text.splitlines()
i = 0
while i < len(lines):
match = DIRECTIVE_RE.match(lines[i])
if match is None:
i += 1
continue
members_enabled = True
i += 1
while i < len(lines):
line = lines[i]
if line.strip() == "":
i += 1
continue
if not line.startswith((" ", "\t")):
break # non-indented line: end of this directive's option block
if MEMBERS_DISABLED_RE.match(line):
members_enabled = False
i += 1
yield match.group("target"), members_enabled
def module_member_ids(module: ModuleType) -> Iterator[int]:
"""Yield the id() of each public member a module directive renders.
The rendered members are the module's ``__all__`` if defined, else its public
(non-underscore) attributes."""
member_names = getattr(module, "__all__", None) or [
name for name in dir(module) if not name.startswith("_")
]
for name in member_names:
member = getattr(module, name, None)
if member is not None:
yield id(member)
def documented_object_ids(api_docs_root: Path) -> set[int]:
"""Collect the id()s of every object rendered by a `:::` directive under api_docs_root.
A directive pointing at an object documents that object. A directive pointing at a
module documents the module's public members unless the directive sets
``members: false``."""
documented: set[int] = set()
for md_file in sorted(api_docs_root.rglob("*.md")):
for target, members_enabled in iter_directives(md_file.read_text(encoding="utf-8")):
obj = resolve(target)
if obj is None:
continue
documented.add(id(obj))
if isinstance(obj, ModuleType) and members_enabled:
documented.update(module_member_ids(obj))
return documented
def find_undocumented_exports(api_docs_root: Path) -> list[str]:
documented = documented_object_ids(api_docs_root)
return sorted(
name
for name in zarr.__all__
if name not in EXEMPT_EXPORTS and id(getattr(zarr, name)) not in documented
)
def main() -> int:
args = sys.argv[1:]
api_docs_root = Path(args[0]).resolve() if args else DEFAULT_API_DOCS_ROOT
if not api_docs_root.exists():
print(f"{api_docs_root} does not exist.", file=sys.stderr)
return 1
missing = find_undocumented_exports(api_docs_root)
if not missing:
print(f"All {len(zarr.__all__)} public exports are documented.")
return 0
print(
f"Found {len(missing)} public export(s) in zarr.__all__ missing from the API "
"reference (docs/api/):\n",
file=sys.stderr,
)
for name in missing:
print(f" - zarr.{name}", file=sys.stderr)
print(
"\nAdd a `::: zarr.` page under docs/api/zarr/ (and register it in "
"mkdocs.yml and docs/api/zarr/index.md), or -- if the export is intentionally "
"undocumented -- add it to EXEMPT_EXPORTS in this script with a reason.",
file=sys.stderr,
)
return 1
if __name__ == "__main__":
sys.exit(main())
zarr-python-3.3.0/ci/check_unlinked_types.py 0000664 0000000 0000000 00000005517 15232676200 0021164 0 ustar 00root root 0000000 0000000 """Check for unlinked type annotations in built documentation.
mkdocstrings renders resolved types as links and unresolved
types as Name without an anchor.
This script finds all such unlinked types in the built HTML and reports them.
Usage:
python ci/check_unlinked_types.py [site_dir]
Raises ValueError if unlinked types are found.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
# Matches the griffe/mkdocstrings pattern for unlinked cross-references:
# Name
UNLINKED_PATTERN = re.compile(
r'(?P[^<]+)'
)
# Patterns to exclude from the report
EXCLUDE_PATTERNS = [
# TypeVars and type parameters (single brackets like Foo[T])
re.compile(r"\[.+\]$"),
# Dataclass field / namedtuple field references (contain parens)
re.compile(r"\("),
# Private names
re.compile(r"\._"),
# Dunder attributes
re.compile(r"\.__\w+__$"),
# Testing utilities
re.compile(r"^zarr\.testing\."),
# Third-party types (hypothesis, pytest, etc.)
re.compile(r"^(hypothesis|pytest|typing_extensions|builtins|dataclasses)\."),
]
def should_exclude(qualname: str) -> bool:
return any(p.search(qualname) for p in EXCLUDE_PATTERNS)
def find_unlinked_types(site_dir: Path) -> dict[str, set[str]]:
"""Find all unlinked types in built HTML files.
Returns a dict mapping qualified type names to the set of pages where they appear.
"""
api_dir = site_dir / "api"
if not api_dir.exists():
raise FileNotFoundError(f"{api_dir} does not exist. Run 'mkdocs build' first.")
unlinked: dict[str, set[str]] = {}
for html_file in api_dir.rglob("*.html"):
content = html_file.read_text(errors="replace")
rel_path = str(html_file.relative_to(site_dir))
for match in UNLINKED_PATTERN.finditer(content):
qualname = match.group("qualname")
if not should_exclude(qualname):
unlinked.setdefault(qualname, set()).add(rel_path)
return unlinked
def main() -> None:
site_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("site")
unlinked = find_unlinked_types(site_dir)
if not unlinked:
print("No unlinked types found.")
return
lines = [f"Found {len(unlinked)} unlinked types:\n"]
for qualname in sorted(unlinked):
pages = sorted(unlinked[qualname])
lines.append(f" {qualname}")
lines.extend(f" - {page}" for page in pages)
all_pages = {p for ps in unlinked.values() for p in ps}
lines.append(f"\nTotal: {len(unlinked)} unlinked types across {len(all_pages)} pages")
report = "\n".join(lines)
raise ValueError(report)
if __name__ == "__main__":
main()
zarr-python-3.3.0/ci/lint_docs.py 0000664 0000000 0000000 00000034616 15232676200 0016752 0 ustar 00root root 0000000 0000000 """Lint docstrings and Markdown for reStructuredText markup that won't render.
This project renders API docs with mkdocstrings (``docstring_style: numpy``) and prose
with MkDocs + Markdown -- not Sphinx/reStructuredText. RST constructs that survive from
older docstrings (or muscle memory) are not interpreted: a Sphinx role passes through as
literal text instead of becoming a link, an ``.. note::`` directive renders as a stray
line, and a ``:param:`` field list never becomes a documented parameter.
Crucially, none of this is caught by the rest of the docs CI. ``mkdocs build --strict``
sees the residue as ordinary prose (no warning), and ``ci/check_unlinked_types.py`` only
finds cross-references mkdocstrings *attempted* to resolve -- a raw ``:class:`` role is
never attempted, so it leaves no unlinked-type span. This linter fills that gap with a
fast, source-level check that needs no docs build.
Checks fall into two groups -- RST markup that silently fails under MkDocs/mkdocstrings,
and Markdown structural problems that render as valid-but-wrong HTML (so `mkdocs build`
emits no warning):
sphinx-role :class:`X`, :func:`X`, :py:meth:`X` -> [`X`][zarr.X]
rst-directive .. note:: / .. code-block:: python -> MkDocs admonition / fenced code
rst-field :param x:, :returns:, :rtype: -> numpydoc Parameters/Returns/Raises
rst-link `text `_ -> [text](https://example)
list-break unindented code fence between list items -> indent the fence under its item
list-indent continuation block indented < 4 spaces -> indent it 4 spaces
list-blank list item directly after indented block -> blank line before the item
The ``list-break`` check catches a fenced code block at column 0 placed *between* two list
items: because the fence is not indented into the preceding item, Markdown ends the list at
the fence and the following item starts a fresh list -- renumbering an ordered list (1, 1, 2
instead of 1, 2, 3) or breaking the grouping/spacing of any list. markdownlint's MD029 only
notices this for sequentially-numbered ordered lists; lazily-numbered (1., 1.) and unordered
lists slip past it, so this structural check covers the gap.
The ``list-indent`` and ``list-blank`` checks catch the two halves of Python-Markdown's
strict list-continuation rules, which differ from CommonMark. A blank-line-separated
block (paragraph, nested list, table) belongs to a list item only when indented at least
4 spaces; at the 2-space indent other renderers accept, Python-Markdown ends the list and
the block escapes to the top level (``list-indent``). And a new list item can not start
directly after an indented continuation block: without a blank line first, the ``- `` line
is lazily absorbed into the preceding paragraph as literal text (``list-blank``). Both
produced silently-broken changelog rendering in ``docs/release-notes.md``.
Usage:
python ci/lint_docs.py [PATH ...]
PATH defaults to the repo-root ``src/zarr`` and ``docs``. Each PATH may be a file or a
directory (directories are searched for ``*.py`` and ``*.md``). Exits non-zero if any
issues are found.
"""
from __future__ import annotations
import ast
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import NamedTuple
REPO_ROOT = Path(__file__).parent.parent.resolve()
DEFAULT_PATHS = (REPO_ROOT / "src" / "zarr", REPO_ROOT / "docs")
# A Sphinx interpreted-text role: an optional domain, a role name, then a backtick
# target -- e.g. :class:`Foo` or :py:meth:`Foo.bar`. Requires the trailing backtick so
# plain "::" (RST literal markers, time strings, mkdocs-material :icon: shortcodes) and
# URLs ("https://") never match.
SPHINX_ROLE = re.compile(r":[a-zA-Z_]\w*(?::[a-zA-Z_]\w*)?:`[^`\n]+`")
# An RST directive line: ".. name::" (with or without an argument after it). RST hyperlink
# targets (".. _label:") and comments (".. text") lack the "::" and are not flagged.
RST_DIRECTIVE = re.compile(r"^\s*\.\.[ \t]+[\w-]+::")
# An RST field-list entry used for docstring fields. The role names above (class, func,
# ...) are deliberately excluded so a role is reported as a role, not a field.
RST_FIELD = re.compile(
r"^\s*:(param|parameter|arg|argument|key|keyword|kwarg|type|returns?|rtype"
r"|raises?|except|exception|yields?|ytype|var|cvar|ivar)\b[^:]*:"
)
# An RST external hyperlink: `text `_
RST_LINK = re.compile(r"`[^`\n]+\n]+>`_")
# A list item at column 0: an ordered marker (1. / 1)) or a bullet (-, *, +) followed by
# whitespace and content. Leading-whitespace (nested/continuation) lines are intentionally
# not matched -- the list-break check only fires on top-level items.
LIST_ITEM = re.compile(r"^(?:\d+[.)]|[-*+])\s+\S")
class Check(NamedTuple):
"""One docs-residue check: its category, the line pattern that flags it (None for a
structural check matched outside ``_scan_line``), and the user-facing remediation
shown by ``main()``. Keeping ``example``/``fix`` here makes this the single source for
the help text, so adding a check can't leave the help out of date."""
category: str
pattern: re.Pattern[str] | None
example: str
fix: str
# The ``list-*`` checks carry no pattern -- they are detected structurally, not by scanning
# a single line -- but they appear here so they share the remediation help.
CHECKS = (
Check("sphinx-role", SPHINX_ROLE, ":class:`X`", "[`X`][zarr.X]"),
Check("rst-directive", RST_DIRECTIVE, ".. note::", "MkDocs admonition (!!! note)"),
Check("rst-field", RST_FIELD, ":param x:", "numpydoc Parameters/Returns/Raises section"),
Check("rst-link", RST_LINK, "`text `_", "[text](url)"),
Check("list-break", None, "fence between items", "indent the fence 4 spaces to nest it"),
Check("list-indent", None, "2-space continuation", "indent the block 4 spaces under its item"),
Check("list-blank", None, "item after indented block", "add a blank line before the item"),
)
@dataclass(frozen=True)
class Finding:
path: Path
line: int
category: str
snippet: str
def format(self) -> str:
try:
location: Path | str = self.path.relative_to(REPO_ROOT)
except ValueError:
location = self.path
return f" {location}:{self.line}: [{self.category}] {self.snippet.strip()}"
def _scan_line(text: str) -> list[str]:
"""Return every RST-residue category found in a single line (a line can carry more
than one, e.g. a role and an external link)."""
return [c.category for c in CHECKS if c.pattern is not None and c.pattern.search(text)]
def lint_python(path: Path) -> list[Finding]:
"""Scan the docstrings (module, classes, functions) of a Python file.
Only docstrings are checked -- they are what mkdocstrings renders -- so RST-looking
text inside ordinary code or string literals is never misreported."""
source = path.read_text(encoding="utf-8")
try:
tree = ast.parse(source)
except SyntaxError as exc: # pragma: no cover - surfaced, not silently skipped
return [Finding(path, exc.lineno or 0, "syntax-error", str(exc.msg))]
doc_nodes = (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
# node.body[0].value is the docstring literal; its lineno is the line the string opens
# on, so content line i maps to source line (start + i).
docstrings = [
(docstring, node.body[0].value.lineno) # type: ignore[attr-defined]
for node in ast.walk(tree)
if isinstance(node, doc_nodes)
if (docstring := ast.get_docstring(node, clean=False))
]
return [
Finding(path, start + offset, category, line)
for docstring, start in docstrings
for offset, line in enumerate(docstring.splitlines())
for category in _scan_line(line)
]
class Fence(NamedTuple):
"""A fenced code block, by 0-based line index. ``terminated`` is False when the fence
has no closing delimiter before EOF, in which case ``close`` is the last line."""
open: int
close: int
terminated: bool
def fenced_blocks(lines: list[str]) -> list[Fence]:
"""Index every fenced code block in ``lines``.
An unterminated fence is malformed Markdown that `mkdocs build` surfaces anyway; it is
still returned (with ``terminated=False``, ``close`` at the last line) so callers that
skip code can skip to EOF."""
blocks: list[Fence] = []
fence: str | None = None
open_idx = -1
for i, line in enumerate(lines):
stripped = line.lstrip()
if fence is None:
if stripped.startswith(("```", "~~~")):
fence, open_idx = stripped[:3], i
elif stripped.startswith(fence):
blocks.append(Fence(open_idx, i, terminated=True))
fence = None
if fence is not None:
blocks.append(Fence(open_idx, len(lines) - 1, terminated=False))
return blocks
def find_list_breaking_fences(lines: list[str], blocks: list[Fence]) -> list[tuple[int, str]]:
"""Return ``(lineno, snippet)`` for each fenced code block at column 0 that splits a
list -- i.e. one whose nearest non-blank neighbours on both sides are top-level list
items. Such a fence is not indented into the preceding item, so Markdown closes the
list at the fence and the following item starts a new one. The fix is to indent the
fence (4 spaces) so it nests inside its list item. See the module docstring.
Conservative on purpose: it requires a list item *directly* before and after (a
continuation line or paragraph in between is not matched), keeping false positives low
for a check that fails CI. Unterminated fences are ignored."""
def neighbour(start: int, step: int) -> str | None:
j = start + step
while 0 <= j < len(lines):
if lines[j].strip():
return lines[j]
j += step
return None
def splits_a_list(open_i: int, close_i: int) -> bool:
if lines[open_i][:1].isspace():
return False # indented fence: already nested in the list item, not a break
before = neighbour(open_i, -1)
after = neighbour(close_i, +1)
return bool(before and after and LIST_ITEM.match(before) and LIST_ITEM.match(after))
return [
(fence.open + 1, lines[fence.open])
for fence in blocks
if fence.terminated and splits_a_list(fence.open, fence.close)
]
def find_list_continuation_issues(
lines: list[str], in_code: set[int]
) -> list[tuple[int, str, str]]:
"""Return ``(lineno, category, snippet)`` for list continuations Python-Markdown will
mis-render (see the module docstring):
- ``list-indent``: a blank-line-separated block inside a list item indented 1-3
spaces. Python-Markdown requires 4; at less, the block escapes the list.
- ``list-blank``: a top-level list item directly after a line indented 4+ spaces.
Without a blank line in between, the item is absorbed into the preceding paragraph
as literal ``- `` text.
Lazy continuations (an indented line with no blank line before it) are valid at any
indent and are not flagged. Fenced-code lines are opaque: never flagged themselves,
but they keep the item scope open and their indent feeds the ``list-blank`` check so
an item directly after an indented fence is still caught."""
findings: list[tuple[int, str, str]] = []
in_item = False # inside a top-level list item's scope
prev_blank = True
prev_indent = 0
for i, line in enumerate(lines):
stripped = line.strip()
if not stripped:
prev_blank = True
continue
indent = len(line) - len(line.lstrip(" "))
if i not in in_code:
if indent == 0:
is_item = bool(LIST_ITEM.match(line))
if is_item and in_item and not prev_blank and prev_indent >= 4:
findings.append((i + 1, "list-blank", line))
in_item = is_item
elif in_item and prev_blank and indent < 4:
findings.append((i + 1, "list-indent", line))
prev_blank = False
prev_indent = indent
return findings
def lint_markdown(path: Path) -> list[Finding]:
"""Scan a Markdown file: RST residue in prose (skipping fenced code blocks), plus
list-structure problems (see find_list_breaking_fences and
find_list_continuation_issues)."""
lines = path.read_text(encoding="utf-8").splitlines()
blocks = fenced_blocks(lines)
in_code = {i for fence in blocks for i in range(fence.open, fence.close + 1)}
prose = [
Finding(path, lineno, category, line)
for lineno, line in enumerate(lines, start=1)
if lineno - 1 not in in_code
for category in _scan_line(line)
]
breaks = [
Finding(path, lineno, "list-break", snippet)
for lineno, snippet in find_list_breaking_fences(lines, blocks)
]
continuations = [
Finding(path, lineno, category, snippet)
for lineno, category, snippet in find_list_continuation_issues(lines, in_code)
]
return prose + breaks + continuations
def iter_files(paths: tuple[Path, ...]) -> list[Path]:
files: list[Path] = []
for path in paths:
if path.is_file():
files.append(path)
elif path.is_dir():
files.extend(sorted(path.rglob("*.py")))
files.extend(sorted(path.rglob("*.md")))
else:
raise FileNotFoundError(f"{path} does not exist")
return files
LINTERS = {".py": lint_python, ".md": lint_markdown}
def lint(paths: tuple[Path, ...]) -> list[Finding]:
return [
finding
for file in iter_files(paths)
if file.suffix in LINTERS
for finding in LINTERS[file.suffix](file)
]
def main() -> int:
args = sys.argv[1:]
paths = tuple(Path(a).resolve() for a in args) if args else DEFAULT_PATHS
findings = lint(paths)
if not findings:
print("No reStructuredText residue or list-breaking fences found in docs.")
return 0
print(
f"Found {len(findings)} docs issue(s) -- RST markup that will not render under "
"MkDocs/mkdocstrings, or Markdown that renders as valid-but-wrong HTML:\n",
file=sys.stderr,
)
for finding in findings:
print(finding.format(), file=sys.stderr)
remediation = "\n".join(f" {c.category:<13} {c.example:<19} -> {c.fix}" for c in CHECKS)
print(
f"\nFix each issue (see ci/lint_docs.py header):\n{remediation}",
file=sys.stderr,
)
return 1
if __name__ == "__main__":
sys.exit(main())
zarr-python-3.3.0/codecov.yml 0000664 0000000 0000000 00000001277 15232676200 0016171 0 ustar 00root root 0000000 0000000 coverage:
status:
patch:
default:
target: auto
informational: true
project:
default:
target: auto
threshold: 0.1
flags:
- tests
flags:
tests:
paths:
- src/
carryforward: true
gpu:
paths:
- src/
carryforward: true
codecov:
notify:
# 6 = test.yml: 3 (optional+ubuntu) + 2 (upstream + min_deps), hypothesis: 1
after_n_builds: 6
wait_for_ci: yes
comment:
layout: "diff, files"
behavior: default
require_changes: true # if true: only post the comment if coverage changes
branches: # branch names that can post comment
- "main"
github_checks:
annotations: false
zarr-python-3.3.0/design/ 0000775 0000000 0000000 00000000000 15232676200 0015266 5 ustar 00root root 0000000 0000000 zarr-python-3.3.0/design/chunk-grid.md 0000664 0000000 0000000 00000133506 15232676200 0017653 0 ustar 00root root 0000000 0000000 # Unified Chunk Grid
Version: 6
Design document for adding rectilinear (variable) chunk grid support to **zarr-python**, conforming to the [rectilinear chunk grid extension spec](https://github.com/zarr-developers/zarr-extensions/pull/25).
**Related:**
- [#3750](https://github.com/zarr-developers/zarr-python/issues/3750) (single ChunkGrid proposal)
- [#3534](https://github.com/zarr-developers/zarr-python/pull/3534) (rectilinear implementation)
- [#3735](https://github.com/zarr-developers/zarr-python/pull/3735) (chunk grid module/registry)
- [ZEP0003](https://github.com/zarr-developers/zeps/blob/main/draft/ZEP0003.md) (variable chunking spec)
- [zarr-specs#370](https://github.com/zarr-developers/zarr-specs/pull/370) (sharding v1.1: non-divisible subchunks)
- [zarr-extensions#25](https://github.com/zarr-developers/zarr-extensions/pull/25) (rectilinear extension)
- [zarr-extensions#34](https://github.com/zarr-developers/zarr-extensions/issues/34) (sharding + rectilinear)
## Problem
Chunk grids form a hierarchy — the rectilinear grid is strictly more general than the regular grid. Any regular grid is expressible as a rectilinear grid. There is no known chunk grid that is both (a) more general than rectilinear and (b) retains the axis-aligned tessellation properties Zarr assumes. All known grids are special cases:
| Grid type | Description | Example |
|---|---|---|
| Regular | Uniform chunk size, boundary chunks padded with fill_value | `[10, 10, 10, 10]` |
| Regular-bounded (zarrs) | Uniform chunk size, boundary chunks trimmed to array extent | `[10, 10, 10, 5]` |
| HPC boundary-padded | Regular interior, larger boundary chunks ([VirtualiZarr#217](https://github.com/zarr-developers/VirtualiZarr/issues/217)) | `[10, 8, 8, 8, 10]` |
| Fully variable | Arbitrary per-chunk sizes | `[5, 12, 3, 20]` |
Prior iterations on the chunk grid design were based on the Zarr V3 spec's definition of chunk grids as an extension point alongside codecs, dtypes, etc. Therefore, we started designing the chunk grid implementation following a similar registry-based approach. However, in practice chunk grids are fundamentally different than codecs. Codecs are independent; supporting `zstd` tells you nothing about `gzip`. Chunk grids are not: every regular grid is a valid rectilinear grid. A registry-based plugin system makes sense for codecs but adds complexity without clear benefit for chunk grids. Here we start from some basic goals and propose a more fitting design for supporting different chunk grids in zarr-python.
## Goals
1. **Follow the zarr extension proposal.** The implementation should conform to the [rectilinear chunk grid spec](https://github.com/zarr-developers/zarr-extensions/tree/main/chunk-grids/rectilinear), not innovate on the metadata format.
2. **Minimize changes to the public API.** Users creating regular arrays should see no difference. Rectilinear is additive.
3. **Maintain backwards compatibility.** Existing code using `.chunks`, `isinstance` checks, or importing `RegularChunkGrid`/`RectilinearChunkGrid` from `zarr.core.chunk_grids` should continue to work where practical (with deprecation warnings where appropriate). Internal code paths/imports may be broken with justification.
4. **Design for future iteration.** The internal architecture should allow refactoring (e.g., metadata/array separation, new dimension types) without breaking the public API.
5. **Minimize downstream changes.** xarray, VirtualiZarr, Icechunk, Cubed, etc. should need minimal updates.
6. **Minimize time to stable release.** Ship behind a feature flag, stabilize through real-world usage, promote to stable API.
7. **The new API should be useful.** `read_chunk_sizes`/`write_chunk_sizes`, `ChunkGrid.__getitem__`, `is_regular` — these should solve real problems, not just expose internals.
8. **Extensible for other serialization structures.** The per-dimension design should support future encodings (tile, temporal) without changes to indexing or codecs.
## Design
### Design choices
1. **A chunk grid is a concrete arrangement of chunks.** Not an abstract tiling pattern. This means that the chunk grid is bound to specific array dimensions, which enables the chunk grid to answer any question about any chunk (offset, size, count) without external parameters.
2. **One implementation, multiple serialization forms.** A single `ChunkGrid` class handles all chunking logic. The serialization format (`"regular"` vs `"rectilinear"`) is chosen by the metadata layer, not the grid.
3. **No chunk grid registry.** Simple name-based dispatch in the metadata layer's `parse_chunk_grid()`.
4. **Fixed vs Varying per dimension.** `FixedDimension(size, extent)` for uniform chunks; `VaryingDimension(edges, extent)` for per-chunk edge lengths with precomputed prefix sums. Avoids expanding regular dimensions into lists of identical values.
5. **Transparent transitions.** Operations like `resize()` can move an array from regular to rectilinear chunking.
### Internal representation
```python
@dataclass(frozen=True)
class FixedDimension:
"""Uniform chunk size. Boundary chunks contain less data but are
encoded at full size by the codec pipeline."""
size: int # chunk edge length (>= 0)
extent: int # array dimension length
def __post_init__(self) -> None:
# validates size >= 0 and extent >= 0
@property
def nchunks(self) -> int:
if self.size == 0:
return 0
return ceildiv(self.extent, self.size)
def index_to_chunk(self, idx: int) -> int:
return idx // self.size # raises IndexError if OOB
def chunk_offset(self, chunk_ix: int) -> int:
return chunk_ix * self.size # raises IndexError if OOB
def chunk_size(self, chunk_ix: int) -> int:
return self.size # always uniform; raises IndexError if OOB
def data_size(self, chunk_ix: int) -> int:
return max(0, min(self.size, self.extent - chunk_ix * self.size)) # raises IndexError if OOB
@property
def unique_edge_lengths(self) -> Iterable[int]:
return (self.size,) # O(1)
def indices_to_chunks(self, indices: NDArray) -> NDArray:
return indices // self.size
def with_extent(self, new_extent: int) -> FixedDimension:
return FixedDimension(size=self.size, extent=new_extent)
def resize(self, new_extent: int) -> FixedDimension:
return FixedDimension(size=self.size, extent=new_extent)
@dataclass(frozen=True)
class VaryingDimension:
"""Explicit per-chunk sizes. The last chunk may extend past the array
extent (extent < sum(edges)), in which case data_size clips to the
valid region while chunk_size returns the full edge length for codec
processing. This underflow is allowed to match how regular grids
handle boundary chunks, and to support shrinking an array without
rewriting chunk edges (the spec allows trailing edges beyond the extent)."""
edges: tuple[int, ...] # per-chunk edge lengths (all > 0)
cumulative: tuple[int, ...] # prefix sums for O(log n) lookup
extent: int # array dimension length (may be < sum(edges))
def __init__(self, edges: Sequence[int], extent: int) -> None:
# validates edges non-empty, all > 0, extent >= 0, extent <= sum(edges)
# computes cumulative via itertools.accumulate
# uses object.__setattr__ for frozen dataclass
@property
def nchunks(self) -> int:
# number of chunks that overlap [0, extent)
if extent == 0:
return 0
return bisect.bisect_left(self.cumulative, extent) + 1
@property
def ngridcells(self) -> int:
return len(self.edges)
def index_to_chunk(self, idx: int) -> int:
return bisect.bisect_right(self.cumulative, idx) # raises IndexError if OOB
def chunk_offset(self, chunk_ix: int) -> int:
return self.cumulative[chunk_ix - 1] if chunk_ix > 0 else 0 # raises IndexError if OOB
def chunk_size(self, chunk_ix: int) -> int:
return self.edges[chunk_ix] # raises IndexError if OOB
def data_size(self, chunk_ix: int) -> int:
offset = self.chunk_offset(chunk_ix)
return max(0, min(self.edges[chunk_ix], self.extent - offset)) # raises IndexError if OOB
@property
def unique_edge_lengths(self) -> Iterable[int]:
# lazy generator: yields unseen values, short-circuits deduplication
def indices_to_chunks(self, indices: NDArray) -> NDArray:
return np.searchsorted(self.cumulative, indices, side='right')
def with_extent(self, new_extent: int) -> VaryingDimension:
# validates cumulative[-1] >= new_extent (O(1)), re-binds extent
return VaryingDimension(self.edges, extent=new_extent)
def resize(self, new_extent: int) -> VaryingDimension:
# grow past edge sum: append chunk of size (new_extent - sum(edges))
# shrink or grow within edge sum: preserve all edges, re-bind extent
```
Both types implement the `DimensionGrid` protocol: `nchunks`, `extent`, `index_to_chunk`, `chunk_offset`, `chunk_size`, `data_size`, `indices_to_chunks`, `unique_edge_lengths`, `with_extent`, `resize`. Memory usage scales with the number of *varying* dimensions, not total chunks.
All per-chunk methods (`chunk_offset`, `chunk_size`, `data_size`) raise `IndexError` for out-of-bounds chunk indices, providing consistent fail-fast behavior across both dimension types.
The two size methods serve different consumers:
| Method | Returns | Consumer |
|---|---|---|
| `chunk_size` | Buffer size for codec processing | Codec pipeline (`ArraySpec.shape`) |
| `data_size` | Valid data region within the buffer | Indexing pipeline (`chunk_selection` slicing) |
For `FixedDimension`, these differ only at the boundary. For `VaryingDimension`, these differ only when the last chunk extends past the extent (i.e., `extent < sum(edges)`). This matches current zarr-python behavior: `get_chunk_spec` passes the full `chunk_shape` to the codec for all chunks, and the indexer generates a `chunk_selection` that clips the decoded buffer.
### DimensionGrid Protocol
```python
@runtime_checkable
class DimensionGrid(Protocol):
"""Structural interface shared by FixedDimension and VaryingDimension."""
@property
def nchunks(self) -> int: ...
@property
def ngridcells(self) -> int: ...
@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 indices_to_chunks(self, indices: NDArray[np.intp]) -> NDArray[np.intp]: ...
@property
def unique_edge_lengths(self) -> Iterable[int]: ...
def with_extent(self, new_extent: int) -> DimensionGrid: ...
def resize(self, new_extent: int) -> DimensionGrid: ...
```
The protocol is `@runtime_checkable`, enabling polymorphic handling of both dimension types without `isinstance` checks.
`nchunks` and `ngridcells` differ when `extent < sum(edges)`: `nchunks` counts only chunks that overlap `[0, extent)`, while `ngridcells` counts total defined grid cells (i.e., `len(edges)`). For `FixedDimension`, both are equal. For `VaryingDimension`, they differ after a resize that shrinks the extent below the edge sum.
### ChunkSpec
```python
@dataclass(frozen=True)
class ChunkSpec:
slices: tuple[slice, ...] # valid data region in array coordinates
codec_shape: tuple[int, ...] # buffer shape for codec processing
@property
def shape(self) -> tuple[int, ...]:
return tuple(s.stop - s.start for s in self.slices)
@property
def is_boundary(self) -> bool:
return self.shape != self.codec_shape
```
For interior chunks, `shape == codec_shape`. For boundary chunks of a regular grid, `codec_shape` is the full declared chunk size while `shape` is clipped. For rectilinear grids, `shape == codec_shape` unless the last chunk extends past the extent.
### API
```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
# 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
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
grid[99, 99] # None — out of bounds
for spec in grid: # iterate all chunks
...
# .chunks property: retained for regular grids, raises NotImplementedError for rectilinear
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))
```
`ChunkGrid.__getitem__` constructs `ChunkSpec` using `chunk_size` for `codec_shape` and `data_size` for `slices`:
```python
def __getitem__(self, coords: int | tuple[int, ...]) -> ChunkSpec | None:
if isinstance(coords, int):
coords = (coords,)
slices = []
codec_shape = []
for dim, ix in zip(self.dimensions, coords):
if ix < 0 or ix >= dim.nchunks:
return None
offset = dim.chunk_offset(ix)
slices.append(slice(offset, offset + dim.data_size(ix)))
codec_shape.append(dim.chunk_size(ix))
return ChunkSpec(tuple(slices), tuple(codec_shape))
```
#### Construction
`from_sizes` requires `array_shape`, binding the extent per dimension at construction time. This is a core design choice: a chunk grid is a concrete arrangement for a specific array, not an abstract tiling pattern.
```python
# Regular grid — all FixedDimension
grid = ChunkGrid.from_sizes(array_shape=(100, 200), chunk_sizes=(10, 20))
# Rectilinear grid — extent = sum(edges) when shape matches
grid = ChunkGrid.from_sizes(array_shape=(60, 100), chunk_sizes=[[10, 20, 30], [25, 25, 25, 25]])
# Rectilinear grid with boundary clipping — last chunk extends past array extent
# e.g., shape=(55, 90) but edges sum to (60, 100): data_size clips at extent
grid = ChunkGrid.from_sizes(array_shape=(55, 90), chunk_sizes=[[10, 20, 30], [25, 25, 25, 25]])
# Direct construction
grid = ChunkGrid(dimensions=(FixedDimension(10, 100), VaryingDimension([10, 20, 30], 55)))
```
When `extent < sum(edges)`, the dimension is always stored as `VaryingDimension` (even if all edges are identical) to preserve the explicit edge count. The last chunk's `chunk_size` returns the full declared edge (codec buffer) while `data_size` clips to the extent. This mirrors how `FixedDimension` handles boundary chunks in regular grids.
#### Serialization
```python
# Regular grid:
{"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]]]}}
```
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()`.
**The `ChunkGrid` does not serialize itself.** The format choice (`"regular"` vs `"rectilinear"`) belongs to `ArrayV3Metadata`. Serialization and deserialization are handled by the metadata-layer chunk grid classes (`RegularChunkGridMetadata` and `RectilinearChunkGridMetadata` in `metadata/v3.py`), which provide `to_dict()` and `from_dict()` methods.
For `create_array`, the format is inferred from the `chunks` argument: a flat tuple produces `"regular"`, a nested list produces `"rectilinear"`. The `_is_rectilinear_chunks()` helper detects nested sequences like `[[10, 20], [5, 5]]`.
##### Rectilinear spec compliance
The rectilinear format requires `"kind": "inline"` (validated by `validate_rectilinear_kind()`). Per the spec, each element of `chunk_shapes` can be:
- A bare integer `m`: repeated until `sum >= array_extent`
- A list of bare integers: explicit per-chunk sizes
- A mixed array of bare integers and `[value, count]` RLE pairs
RLE compression is used when serializing: runs of identical sizes become `[value, count]` pairs, singletons stay as bare integers.
```python
# compress_rle([10, 10, 10, 5]) -> [[10, 3], 5]
# expand_rle([[10, 3], 5]) -> [10, 10, 10, 5]
```
For a single-element `chunk_shapes` tuple like `(10,)`, `RectilinearChunkGridMetadata.to_dict()` serializes it as a bare integer `10`. Per the rectilinear spec, a bare integer is repeated until the sum >= extent, preserving the full codec buffer size for boundary chunks.
**Zero-extent handling:** Regular grids serialize zero-extent dimensions without issue (the format encodes only `chunk_shape`, no edges). Rectilinear grids cannot represent zero-extent dimensions because the spec requires at least one positive-integer edge length per axis.
#### read_chunk_sizes / write_chunk_sizes
The `read_chunk_sizes` and `write_chunk_sizes` properties provide universal access to per-dimension chunk data sizes, matching the dask `Array.chunks` convention. They work for both regular and rectilinear grids:
- `write_chunk_sizes`: always returns outer (storage) chunk sizes
- `read_chunk_sizes`: returns inner chunk sizes when sharding is used, otherwise same as `write_chunk_sizes`
```python
>>> arr = zarr.create_array(store, shape=(100, 80), chunks=(30, 40))
>>> arr.write_chunk_sizes
((30, 30, 30, 10), (40, 40))
>>> arr = zarr.create_array(store, shape=(60, 100), chunks=[[10, 20, 30], [50, 50]])
>>> arr.write_chunk_sizes
((10, 20, 30), (50, 50))
```
The underlying `ChunkGrid.chunk_sizes` property (on the grid, not the array) returns the same as `write_chunk_sizes`.
#### 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
```
Resize uses `ChunkGrid.update_shape(new_shape)`, which delegates to each dimension's `.resize()` method:
- `FixedDimension.resize()`: simply re-binds the extent (identical to `with_extent`)
- `VaryingDimension.resize()`: grow past `sum(edges)` appends a chunk covering the gap; shrink or grow within `sum(edges)` preserves all edges and re-binds the extent (the spec allows trailing edges beyond the array extent)
**Known limitation (deferred):** When growing a `VaryingDimension`, the current implementation always appends a single chunk covering the new region. For example, `[10, 10, 10]` resized from 30 to 45 produces `[10, 10, 10, 15]` instead of the more natural `[10, 10, 10, 10, 10]`. A future improvement should add an optional `chunks` parameter to `resize()` that controls how the new region is partitioned, with a sane default (e.g., repeating the last chunk size). This is safely deferrable because:
- `FixedDimension` already handles resize correctly (regular grids stay regular)
- The single-chunk default produces valid state, just suboptimal chunk layout
- Rectilinear arrays are behind an experimental feature flag
- Adding an optional parameter is backwards-compatible
Open design questions for the `chunks` parameter:
- Does it describe the new region only, or the entire post-resize array?
- Must the overlapping portion agree with existing chunks (no rechunking)?
- What is the type? Same as `chunks` in `create_array`?
#### from_array
The `from_array()` function handles both regular and rectilinear source arrays:
```python
src = zarr.create_array(store, shape=(60, 100), chunks=[[10, 20, 30], [50, 50]])
new = zarr.from_array(data=src, store=new_store, chunks="keep")
# Preserves rectilinear structure: new.write_chunk_sizes == ((10, 20, 30), (50, 50))
```
When `chunks="keep"`, the logic checks `data._chunk_grid.is_regular`:
- Regular: extracts `data.chunks` (flat tuple) and preserves shards
- Rectilinear: extracts `data.write_chunk_sizes` (nested tuples) and forces shards to None
### Indexing
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
```
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.
### Codec pipeline
Today, `get_chunk_spec()` returns the same `ArraySpec(shape=chunk_grid.chunk_shape)` for every chunk. For rectilinear grids, each chunk has a different codec shape:
```python
def get_chunk_spec(self, chunk_coords, array_config, prototype) -> ArraySpec:
spec = self._chunk_grid[chunk_coords]
return ArraySpec(shape=spec.codec_shape, ...)
```
Note `spec.codec_shape`, not `spec.shape`. For regular grids, `codec_shape` is uniform (preserving current behavior). The boundary clipping flow is unchanged:
```
Write: user data → pad to codec_shape with fill_value → encode → store
Read: store → decode to codec_shape → slice via chunk_selection → user data
```
### Sharding
The `ShardingCodec` constructs a `ChunkGrid` per shard using the shard shape as extent and the subchunk shape as `FixedDimension`. Each shard is self-contained — it doesn't need to know whether the outer grid is regular or rectilinear. Validation checks that every unique edge length per dimension is divisible by the inner chunk size, using `dim.unique_edge_lengths` for efficient polymorphic iteration (O(1) for fixed dimensions, lazy-deduplicated for varying).
```
Level 1 — Outer chunk grid (shard boundaries): regular or rectilinear
Level 2 — Inner subchunk grid (within each shard): always regular
Level 3 — Shard index: ceil(shard_dim / subchunk_dim) entries per dimension
```
[zarr-specs#370](https://github.com/zarr-developers/zarr-specs/pull/370) lifts the requirement that subchunk shapes evenly divide the shard shape. With the proposed `ChunkGrid`, this just means removing the `shard_shape % subchunk_shape == 0` validation — `FixedDimension` already handles boundary clipping via `data_size`.
| Outer grid | Subchunk divisibility | Required change |
|---|---|---|
| Regular | Evenly divides (v1.0) | None |
| Regular | Non-divisible (v1.1) | Remove divisibility validation |
| Rectilinear | Evenly divides | Remove "sharding incompatible" guard |
| Rectilinear | Non-divisible | Both changes |
### What this replaces
| Current | Proposed |
|---|---|
| `ChunkGrid` ABC + `RegularChunkGrid` subclass | Single concrete `ChunkGrid` with `is_regular` |
| `RectilinearChunkGrid` (#3534) | Same `ChunkGrid` class |
| Chunk grid registry + entrypoints (#3735) | Direct name dispatch |
| `arr.chunks` | Retained for regular; `arr.read_chunk_sizes`/`arr.write_chunk_sizes` for general use |
| `get_chunk_shape(shape, coord)` | `grid[coord].codec_shape` or `grid[coord].shape` |
## Design decisions
### Why store the extent in ChunkGrid?
The chunk grid is a concrete arrangement, not an abstract tiling pattern. A finite collection naturally has an extent. Storing it enables `__getitem__`, eliminates `dim_len` parameters from every method, and makes the grid self-describing.
This does *not* mean `ArrayV3Metadata.shape` should delegate to the grid. The array shape remains an independent field in metadata. The extent is passed into the grid at construction time so it can answer boundary questions without external parameters. It is **not** serialized as part of the chunk grid JSON — it comes from the `shape` field in array metadata and is combined with the chunk grid configuration in `ChunkGrid.from_metadata()`.
### Why distinguish chunk_size from data_size?
A chunk in a regular grid has two sizes. `chunk_size` is the buffer size the codec processes — always `size` for `FixedDimension`, even at the boundary (padded with `fill_value`). `data_size` is the valid data region — clipped to `extent % size` at the boundary. The indexing layer uses `data_size` to generate `chunk_selection` slices.
This matches current zarr-python behavior and matters for:
1. **Backward compatibility.** Existing stores have boundary chunks encoded at full `chunk_shape`.
2. **Codec simplicity.** Codecs assume uniform input shapes for regular grids.
3. **Shard index correctness.** The index assumes `subchunk_dim`-sized entries.
For `VaryingDimension`, `chunk_size == data_size` when `extent == sum(edges)`. When `extent < sum(edges)` (e.g., after a resize that keeps the last chunk oversized), `data_size` clips the last chunk. This is the fundamental difference: `FixedDimension` has a declared size plus an extent that clips data; `VaryingDimension` has explicit sizes that normally *are* the extent but can also extend past it.
### Why not a chunk grid registry?
There is no known chunk grid outside the rectilinear family that retains the tessellation properties zarr-python assumes. A `match` on the grid name is sufficient.
### Why a single ChunkGrid class instead of RegularChunkGrid + RectilinearChunkGrid?
[Discussed in #3534.](https://github.com/zarr-developers/zarr-python/pull/3534) @d-v-b argued that `RegularChunkGrid` is unnecessary since rectilinear is more general; @dcherian argued that downstream libraries need a fast way to detect regular grids without inspecting potentially millions of chunk edges (see [xarray#9808](https://github.com/pydata/xarray/pull/9808)).
The resolution: a single `ChunkGrid` class with an `is_regular` property (O(1), cached at construction). This gives downstream code the fast-path detection @dcherian needed without the class hierarchy complexity @d-v-b wanted to avoid. The metadata document's `name` field (`"regular"` vs `"rectilinear"`) is also available for clients who inspect JSON directly.
A backwards-compatibility shim in `chunk_grids.py` preserves the old `RegularChunkGrid` / `RectilinearChunkGrid` import paths with deprecation warnings — see [Backwards compatibility](#backwards-compatibility).
### Why is ChunkGrid a concrete class instead of a Protocol/ABC?
The old design had `ChunkGrid` as an ABC with `RegularChunkGrid` as its only subclass. #3534 added `RectilinearChunkGrid` as a second subclass. This branch makes `ChunkGrid` a single concrete class instead, with separate metadata DTOs (`RegularChunkGridMetadata` and `RectilinearChunkGridMetadata` in `metadata/v3.py`) for serialization.
All known grids are special cases of rectilinear, so there's no need for a class hierarchy at the grid level. A `ChunkGrid` Protocol/ABC would mean every caller programs against an abstract interface and adding a grid type requires implementing ~15 methods. A single class is simpler.
Note: the *dimension* types (`FixedDimension`, `VaryingDimension`) do use a `DimensionGrid` Protocol — that's where the polymorphism lives. The grid-level class is concrete; the dimension-level types are polymorphic. If a genuinely novel grid type emerges that can't be expressed as a combination of per-dimension types, a grid-level Protocol can be extracted.
### Why `.chunks` raises for rectilinear grids
[Debated in #3534.](https://github.com/zarr-developers/zarr-python/pull/3534) @d-v-b suggested making `.chunks` return `tuple[tuple[int, ...], ...]` (dask-style) for all grids. @dcherian strongly objected: every downstream consumer expects `tuple[int, ...]`, and silently returning a different type would be worse than raising. Materializing O(10M) chunk edges into a Python tuple is also a real performance risk ([xarray#8902](https://github.com/pydata/xarray/issues/8902#issuecomment-2546127373)).
The resolution:
- `.chunks` is retained for regular grids (returns `tuple[int, ...]` as before)
- `.chunks` raises `NotImplementedError` for rectilinear grids with a message pointing to `.read_chunk_sizes`/`.write_chunk_sizes`
- `.read_chunk_sizes` and `.write_chunk_sizes` return `tuple[tuple[int, ...], ...]` (dask convention) for all grids
@maxrjones noted in review that deprecating `.chunks` for regular grids was not desirable. The current branch does not deprecate it.
### User control over grid serialization format
@d-v-b raised in #3534 that users need a way to say "these chunks are regular, but serialize as rectilinear" (e.g., to allow future append/extend workflows without format changes). @jhamman initially made nested-list input always produce `RectilinearChunkGridMetadata`.
The current branch resolves this via the metadata-layer chunk grid classes. When metadata is deserialized, the original name (from `{"name": "regular"}` or `{"name": "rectilinear"}`) determines which metadata class is instantiated (`RegularChunkGridMetadata` or `RectilinearChunkGridMetadata`), and that class handles serialization via `to_dict()`. Current inference behavior for `create_array`:
- `chunks=(10, 20)` (flat tuple) → infers `"regular"`
- `chunks=[[10, 20], [5, 5]]` (nested lists with varying sizes) → infers `"rectilinear"`
- `chunks=[[10, 10], [20, 20]]` (nested lists with uniform sizes) → `from_sizes` collapses to `FixedDimension`, so `is_regular=True` and infers `"regular"`
**Open question:** Should uniform nested lists preserve `"rectilinear"` to support future append workflows without a format change? This could be addressed by checking the input form before collapsing, or by allowing users to pass `chunk_grid_name` explicitly through the `create_array` API.
### Deferred: Tiled/periodic chunk patterns
[#3750 discussion](https://github.com/zarr-developers/zarr-python/issues/3750) identified periodic chunk patterns as a use case not efficiently served by RLE alone. RLE compresses runs of identical values (`np.repeat`), but periodic patterns like days-per-month (`[31, 28, 31, 30, ...]` repeated 30 years) need a tile encoding (`np.tile`). Real-world examples include:
- **Oceanographic models** (ROMS): HPC boundary-padded chunks like `[10, 8, 8, 8, 10]` — handled by RLE
- **Temporal axes**: days-per-month, hours-per-day — need tile encoding for compact metadata
- **Temporal-aware grids**: date/time-aware chunk grids that layer over other axes (raised by @LDeakin)
A `TiledDimension` prototype was built ([commit 9c0f582](https://github.com/maxrjones/zarr-python/commit/9c0f582f)) demonstrating that the per-dimension design supports this without changes to indexing or the codec pipeline. However, it was intentionally excluded from this release because:
1. **Metadata format must come first.** Tile encoding requires a new `kind` value in the rectilinear spec (currently only `"inline"` is defined). This should go through [zarr-extensions#25](https://github.com/zarr-developers/zarr-extensions/pull/25), not zarr-python unilaterally.
2. **The per-dimension architecture doesn't preclude it.** A future `TiledDimension` can implement the `DimensionGrid` protocol alongside `FixedDimension` and `VaryingDimension` with no changes to indexing, codecs, or the `ChunkGrid` class.
3. **RLE covers the MVP.** Most real-world variable chunk patterns (HPC boundaries, irregular partitions) are efficiently encoded with RLE. Tile encoding is an optimization for a specific (temporal) subset.
### Metadata / Array separation (partially implemented)
An earlier design doc proposed decoupling `ChunkGrid` (runtime) from `ArrayV3Metadata` (serialization), so that metadata would store only a plain dict and the array layer would construct the `ChunkGrid`.
The current implementation partially realizes this separation:
- **Metadata DTOs** (`RegularChunkGridMetadata`, `RectilinearChunkGridMetadata` in `metadata/v3.py`): Pure data, frozen dataclasses, no array shape. These live on `ArrayV3Metadata.chunk_grid` and represent only what goes into `zarr.json`.
- **`ChunkGrid`** (`chunk_grids.py`): Shape-bound, supports indexing, iteration, and chunk specs. Lives on `AsyncArray._chunk_grid`, constructed from metadata + `shape` via `ChunkGrid.from_metadata()`.
This means `ArrayV3Metadata.chunk_grid` is now a `ChunkGridMetadata` (the DTO union type), **not** the runtime `ChunkGrid`. Code that previously accessed runtime methods on `metadata.chunk_grid` (e.g., `all_chunk_coords()`, `__getitem__`) must now use the grid from the array layer instead.
The name controls serialization format; each metadata DTO class provides its own `to_dict()` method for serialization. The `ChunkGrid` handles all runtime queries.
## Prior art
**zarrs (Rust):** Three independent grid types behind a `ChunkGridTraits` trait. Key patterns adopted: Fixed vs Varying per dimension, prefix sums + binary search, `Option` for out-of-bounds, `NonZeroU64` for chunk dimensions, separate subchunk grid per shard, array shape at construction.
**TensorStore (C++):** Stores only `chunk_shape` — boundary clipping via `valid_data_bounds` at query time. Both `RegularGridRef` and `IrregularGrid` internally. No registry.
## Migration
### Public API compatibility
The user-facing API is fully backward-compatible. Existing code that creates, opens, reads, and writes zarr arrays continues to work without changes:
- `zarr.create_array`, `zarr.open`, `zarr.open_array`, `zarr.open_group` -- unchanged signatures. The `chunks` parameter type is *widened* (now also accepts nested sequences for rectilinear grids), but all existing call patterns still work.
- `arr.chunks` -- returns `tuple[int, ...]` for regular arrays, same as before.
- `arr.shape`, `arr.dtype`, `arr.ndim`, `arr.shards` -- unchanged.
- Top-level `zarr` exports -- unchanged.
- Rectilinear chunks are gated behind `zarr.config.set({'array.rectilinear_chunks': True})`, so they cannot be created accidentally.
New additions (purely additive): `arr.read_chunk_sizes`, `arr.write_chunk_sizes`, `zarr.experimental.ChunkGrid`, `zarr.experimental.ChunkSpec`.
The breaking changes discussed below are confined to **internal modules** (`zarr.core.chunk_grids`, `zarr.core.metadata.v3`, `zarr.core.indexing`) that downstream libraries like cubed and VirtualiZarr access directly.
### Internal API compatibility trade-off analysis
This section analyzes the internal breaking changes from the metadata/array separation and evaluates two strategies: (A) add backward-compatibility shims in zarr-python, vs. (B) require downstream packages to update. The baseline is **no shims at all**.
#### What breaks without any shims
Three API changes affect downstream code:
1. **`RegularChunkGrid` class removed from `zarr.core.chunk_grids`.** On `main`, `RegularChunkGrid` is defined in `chunk_grids.py` as a `Metadata` subclass. This branch replaces it with `RegularChunkGridMetadata` in `metadata/v3.py`. Without a shim, `from zarr.core.chunk_grids import RegularChunkGrid` raises `ImportError`.
2. **`RegularChunkGrid` no longer available from `zarr.core.metadata.v3`.** On `main`, `v3.py` imports `RegularChunkGrid` from `chunk_grids.py` for internal use. VirtualiZarr imports it from this location (`from zarr.core.metadata.v3 import RegularChunkGrid`). Without the internal import, this raises `ImportError`.
3. **`OrthogonalIndexer` constructor expects `ChunkGrid`, not `RegularChunkGrid`/`RegularChunkGridMetadata`.** Even if the import shims above resolve to `RegularChunkGridMetadata`, the indexer constructors access `chunk_grid._dimensions`, which only exists on the runtime `ChunkGrid` class. Cubed constructs `OrthogonalIndexer(selection, shape, RegularChunkGrid(chunk_shape=chunks))` directly.
#### Downstream impact without shims
**VirtualiZarr** (5 line changes across 2 files):
```python
# manifests/array.py (line 6): import
- from zarr.core.metadata.v3 import ArrayV3Metadata, RegularChunkGrid
+ from zarr.core.metadata.v3 import ArrayV3Metadata, RegularChunkGridMetadata
# manifests/array.py (line 53): isinstance check
- if not isinstance(_metadata.chunk_grid, RegularChunkGrid):
+ if not isinstance(_metadata.chunk_grid, RegularChunkGridMetadata):
# parsers/zarr.py (line 16): import
- from zarr.core.chunk_grids import RegularChunkGrid
+ from zarr.core.metadata.v3 import RegularChunkGridMetadata
# parsers/zarr.py (line 270): isinstance check
- if not isinstance(array_v3_metadata.chunk_grid, RegularChunkGrid):
+ if not isinstance(array_v3_metadata.chunk_grid, RegularChunkGridMetadata):
# parsers/zarr.py (line 390): cast
- cast(RegularChunkGrid, metadata.chunk_grid).chunk_shape
+ cast(RegularChunkGridMetadata, metadata.chunk_grid).chunk_shape
```
The `manifests/array.py` import is from `zarr.core.metadata.v3` (never a documented export; VirtualiZarr relied on a transitive import). The `parsers/zarr.py` import is from `zarr.core.chunk_grids` (the canonical location on `main`). Both are straightforward renames. The `.chunk_shape` attribute is unchanged on the new class.
If VirtualiZarr needs to support both old and new zarr-python, a version-conditional import adds ~5 more lines.
**Cubed** (3 line changes in 1 file):
```python
# core/ops.py (lines 626-631)
def _create_zarr_indexer(selection, shape, chunks):
if zarr.__version__[0] == "3":
- from zarr.core.chunk_grids import RegularChunkGrid
+ from zarr.core.chunk_grids import ChunkGrid
from zarr.core.indexing import OrthogonalIndexer
- return OrthogonalIndexer(selection, shape, RegularChunkGrid(chunk_shape=chunks))
+ return OrthogonalIndexer(selection, shape, ChunkGrid.from_sizes(shape, chunks))
```
Note that `ChunkGrid` is *not* a renamed class. `RegularChunkGrid(chunk_shape=chunks)` took only chunk sizes; `ChunkGrid.from_sizes(shape, chunks)` also requires the array shape. The `shape` parameter is already available at this call site.
If cubed needs to support both old and new zarr-python:
```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))
```
#### What shims can cover
**Shim 1: `__getattr__` in `chunk_grids.py`** (~15 lines)
Maps `RegularChunkGrid` to `RegularChunkGridMetadata` with a deprecation warning. Covers:
- The `from zarr.core.chunk_grids import RegularChunkGrid` import pattern (used by cubed and VirtualiZarr's `parsers/zarr.py`)
- `isinstance(x, RegularChunkGrid)` checks (because the name resolves to the actual class)
- `RegularChunkGrid(chunk_shape=(...))` construction (because `RegularChunkGridMetadata` accepts the same arguments)
Does **not** cover: passing the result to `OrthogonalIndexer`, because `RegularChunkGridMetadata` lacks `._dimensions`.
**Shim 2: `__getattr__` in `metadata/v3.py`** (~12 lines)
Same pattern, covers VirtualiZarr's import from `zarr.core.metadata.v3`. Mirrors Shim 1 for a different import path.
**Shim 3: Auto-coerce `ChunkGridMetadata` in indexer constructors** (~30 lines)
A helper function + 1-line insertion in each of `BasicIndexer`, `OrthogonalIndexer`, `CoordinateIndexer`, and `MaskIndexer`:
```python
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,
)
if hasattr(chunk_grid, "chunk_shape"):
return _ChunkGrid.from_sizes(shape, tuple(chunk_grid.chunk_shape))
return _ChunkGrid.from_sizes(shape, chunk_grid.chunk_shapes)
raise TypeError(f"Expected ChunkGrid or ChunkGridMetadata, got {type(chunk_grid)}")
```
This covers cubed's `OrthogonalIndexer(selection, shape, RegularChunkGrid(...))` pattern end-to-end (combined with Shim 1).
#### Comparison
| | No shims | Shims 1+2 only | Shims 1+2+3 |
|---|---|---|---|
| **zarr-python additions** | 0 lines | ~27 lines | ~57 lines |
| **VirtualiZarr changes** | 5 lines | 0 lines | 0 lines |
| **Cubed changes** | 3 lines | 3 lines | 0 lines |
| **Maintenance burden** | None | Low (deprecation shims are well-understood) | Medium (indexer coercion blurs metadata/runtime boundary) |
| **API clarity** | Clean (metadata DTOs and runtime types are distinct) | Good (old names redirect to new names) | Weaker (indexers implicitly accept two type families) |
With Shims 1+2 only, VirtualiZarr's `manifests/array.py` import from `zarr.core.metadata.v3` is covered by Shim 2, and the `parsers/zarr.py` import from `zarr.core.chunk_grids` is covered by Shim 1. The `isinstance` checks work because both shims resolve to `RegularChunkGridMetadata`. The `cast` works because `.chunk_shape` is unchanged. So VirtualiZarr needs 0 changes with Shims 1+2. The 3 lines for cubed remain because Shim 1 resolves the import but `OrthogonalIndexer` still needs a runtime `ChunkGrid`.
### Downstream migration
Migration from `main` (where only `RegularChunkGrid` and the abstract `ChunkGrid` ABC exist):
| Old pattern (on `main`) | New pattern |
|---|---|
| `from zarr.core.chunk_grids import RegularChunkGrid` | `from zarr.core.metadata.v3 import RegularChunkGridMetadata` |
| `from zarr.core.chunk_grids import ChunkGrid` (ABC) | `from zarr.core.chunk_grids import ChunkGrid` (concrete class, different API) |
| `isinstance(cg, RegularChunkGrid)` | `isinstance(cg, RegularChunkGridMetadata)` or `grid.is_regular` on the runtime `ChunkGrid` |
| `cg.chunk_shape` on `RegularChunkGrid` | `cg.chunk_shape` on `RegularChunkGridMetadata` (unchanged) |
| `ChunkGrid.from_dict(data)` | `parse_chunk_grid(data)` from `zarr.core.metadata.v3` |
| `chunk_grid.all_chunk_coords(array_shape)` | `chunk_grid.all_chunk_coords()` (shape now stored in grid) |
| `chunk_grid.get_nchunks(array_shape)` | `chunk_grid.get_nchunks()` (shape now stored in grid) |
During the earlier [#3534](https://github.com/zarr-developers/zarr-python/pull/3534) effort (which used separate `RegularChunkGrid`/`RectilinearChunkGrid` classes), downstream PRs and issues were opened to explore compatibility:
- xarray ([#10880](https://github.com/pydata/xarray/pull/10880)), VirtualiZarr ([#877](https://github.com/zarr-developers/VirtualiZarr/pull/877)), Icechunk ([#1338](https://github.com/earth-mover/icechunk/issues/1338)), cubed ([#876](https://github.com/cubed-dev/cubed/issues/876))
These target #3534's API, not this branch's unified `ChunkGrid` design. New downstream POC branches for this design are linked in [Proofs of concepts](#proofs-of-concepts).
### Credits
This implementation builds on prior work:
- **[#3534](https://github.com/zarr-developers/zarr-python/pull/3534)** (@jhamman) — RLE helpers, validation logic, test cases, and the review discussion that shaped the architecture.
- **[#3737](https://github.com/zarr-developers/zarr-python/pull/3737)** — extent-in-grid idea (adopted per-dimension).
- **[#1483](https://github.com/zarr-developers/zarr-python/pull/1483)** — original variable chunking POC.
- **[#3736](https://github.com/zarr-developers/zarr-python/pull/3736)** — resolved by storing extent per-dimension.
## Open questions
1. **Resize defaults (deferred):** When growing a rectilinear array, should `resize()` accept an optional `chunks` parameter? See the [Resize section](#resize) for details and open design questions. Regular arrays already stay regular on resize.
2. **`ChunkSpec` complexity:** `ChunkSpec` carries both `slices` and `codec_shape`. Should the grid expose separate methods for codec vs data queries instead?
3. **`__getitem__` with slices:** Should `grid[0, :]` or `grid[0:3, :]` return a sub-grid or an iterator of `ChunkSpec`s?
4. **Uniform nested lists:** Should `chunks=[[10, 10], [20, 20]]` serialize as `"rectilinear"` (preserving user intent for future append) or `"regular"` (current behavior, collapses uniform edges)? See [User control over grid serialization format](#user-control-over-grid-serialization-format).
5. **`zarr.open` with rectilinear:** @tomwhite noted in #3534 that `zarr.open(mode="w")` doesn't support rectilinear chunks directly. This could be addressed in a follow-up.
## Proofs of concepts
- Zarr-Python:
- branch - https://github.com/maxrjones/zarr-python/tree/poc/unified-chunk-grid
- diff - https://github.com/zarr-developers/zarr-python/compare/main...maxrjones:zarr-python:poc/unified-chunk-grid?expand=1
- Xarray:
- branch - https://github.com/maxrjones/xarray/tree/poc/unified-zarr-chunk-grid
- diff - https://github.com/pydata/xarray/compare/main...maxrjones:xarray:poc/unified-zarr-chunk-grid?expand=1
- VirtualiZarr:
- branch - https://github.com/maxrjones/VirtualiZarr/tree/poc/unified-chunk-grid
- diff - https://github.com/zarr-developers/VirtualiZarr/compare/main...maxrjones:VirtualiZarr:poc/unified-chunk-grid?expand=1
- Virtual TIFF:
- branch - https://github.com/virtual-zarr/virtual-tiff/tree/poc/unified-chunk-grid
- diff - https://github.com/virtual-zarr/virtual-tiff/compare/main...poc/unified-chunk-grid?expand=1
- Cubed:
- branch - https://github.com/maxrjones/cubed/tree/poc/unified-chunk-grid
- Microbenchmarks:
- https://github.com/maxrjones/zarr-chunk-grid-tests/tree/unified-chunk-grid
zarr-python-3.3.0/docs/ 0000775 0000000 0000000 00000000000 15232676200 0014745 5 ustar 00root root 0000000 0000000 zarr-python-3.3.0/docs/_static/ 0000775 0000000 0000000 00000000000 15232676200 0016373 5 ustar 00root root 0000000 0000000 zarr-python-3.3.0/docs/_static/favicon-96x96.png 0000664 0000000 0000000 00000030652 15232676200 0021337 0 ustar 00root root 0000000 0000000 PNG
IHDR _ ` {` pHYs od tEXtSoftware www.inkscape.org< IDATx}y|Uι7&lU*
Ep\@[kTlUYVZpׯ-l, [s}~$dAk|>瓛s3g33̼!_L;[wG4kڽ3>B~INj4!oPD1۔Ec.:Џ[O觎/B}J 18]Q?~jk Pp_zkg-~)1
Ժ\Q6e`HJܗ6s\3_|77=el0"d~z"oeO U05uम8?:ՑLs\E !A @*G`EG3nyv,Ƽy ``JZ{SNJOV:sϏbgav
LI++?j]-HL2 @m4Pe? W=SRG*>s~RL+Wzh