pax_global_header 0000666 0000000 0000000 00000000064 15270141376 0014520 g ustar 00root root 0000000 0000000 52 comment=a2f2415c86f7d35212ae8c0d5dc5e4a22a6c6191
zarr-developers-zarr-python-a2f2415/ 0000775 0000000 0000000 00000000000 15270141376 0017465 5 ustar 00root root 0000000 0000000 zarr-developers-zarr-python-a2f2415/.git-blame-ignore-revs 0000664 0000000 0000000 00000000232 15270141376 0023562 0 ustar 00root root 0000000 0000000 # lint codebase with black and ruff
4e348d6b80c96da461fd866576c971b8a659ba15
# migrate from black to ruff format
22cea005629913208a85799372e045f353744add
zarr-developers-zarr-python-a2f2415/.git_archival.txt 0000664 0000000 0000000 00000000201 15270141376 0022731 0 ustar 00root root 0000000 0000000 node: a2f2415c86f7d35212ae8c0d5dc5e4a22a6c6191
node-date: 2026-10-27T16:27:58+01:00
describe-name: v3.4.2
ref-names: tag: v3.4.2
zarr-developers-zarr-python-a2f2415/.gitattributes 0000664 0000000 0000000 00000000514 15270141376 0022360 0 ustar 00root root 0000000 0000000 *.py linguist-language=python
*.ipynb linguist-documentation
.git_archival.txt export-subst
# just refuses to format a CRLF file, so `just just-check` would fail on a Windows
# checkout with core.autocrlf=true before the contributor has changed anything.
Justfile text eol=lf
*/justfile text eol=lf
packages/*/justfile text eol=lf
zarr-developers-zarr-python-a2f2415/.github/ 0000775 0000000 0000000 00000000000 15270141376 0021025 5 ustar 00root root 0000000 0000000 zarr-developers-zarr-python-a2f2415/.github/CODEOWNERS 0000664 0000000 0000000 00000000070 15270141376 0022415 0 ustar 00root root 0000000 0000000 zarr/_storage/absstore.py @zarr-developers/azure-team
zarr-developers-zarr-python-a2f2415/.github/CONTRIBUTING.md 0000664 0000000 0000000 00000000262 15270141376 0023256 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-developers-zarr-python-a2f2415/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15270141376 0023210 5 ustar 00root root 0000000 0000000 zarr-developers-zarr-python-a2f2415/.github/ISSUE_TEMPLATE/bug_report.yml 0000664 0000000 0000000 00000005133 15270141376 0026105 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-developers-zarr-python-a2f2415/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000001222 15270141376 0025175 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-developers-zarr-python-a2f2415/.github/ISSUE_TEMPLATE/documentation.yml 0000664 0000000 0000000 00000001202 15270141376 0026577 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-developers-zarr-python-a2f2415/.github/ISSUE_TEMPLATE/feature_request.yml 0000664 0000000 0000000 00000000511 15270141376 0027133 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-developers-zarr-python-a2f2415/.github/ISSUE_TEMPLATE/release-checklist.md 0000664 0000000 0000000 00000005706 15270141376 0027131 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-developers-zarr-python-a2f2415/.github/PULL_REQUEST_TEMPLATE.md 0000664 0000000 0000000 00000002540 15270141376 0024627 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-developers-zarr-python-a2f2415/.github/dependabot.yml 0000664 0000000 0000000 00000001615 15270141376 0023660 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-developers-zarr-python-a2f2415/.github/labeler.yml 0000664 0000000 0000000 00000001177 15270141376 0023164 0 ustar 00root root 0000000 0000000 needs release notes:
- all:
- changed-files:
- all-globs-to-all-files: '!changes/*.md'
# Subpackage labels: a pull request whose changes all live under one package
# directory gets that package's label, which .github/release.yml uses to keep
# subpackage work out of the generated `zarr` release notes.
zarr-metadata:
- all:
- changed-files:
- all-globs-to-all-files: 'packages/zarr-metadata/**'
zarr-indexing:
- all:
- changed-files:
- all-globs-to-all-files: 'packages/zarr-indexing/**'
zarr-http-server:
- all:
- changed-files:
- all-globs-to-all-files: 'packages/zarr-http-server/**'
zarr-developers-zarr-python-a2f2415/.github/release.yml 0000664 0000000 0000000 00000001327 15270141376 0023173 0 ustar 00root root 0000000 0000000 # Configuration for GitHub's "Generate release notes" button.
#
# The zarr-python repository also hosts the companion packages under packages/
# (zarr-metadata, zarr-indexing, zarr-http-server), which have their own release
# cadence, tags and changelogs. Pull requests that only touch a subpackage are
# labelled by the labeler workflow (.github/labeler.yml) and excluded here, so a
# `zarr` release lists only the changes that ship in `zarr`.
changelog:
exclude:
labels:
- zarr-metadata
- zarr-indexing
- zarr-http-server
authors:
- pre-commit-ci[bot]
categories:
- title: Dependency updates
labels:
- dependencies
- title: What's Changed
labels:
- "*"
zarr-developers-zarr-python-a2f2415/.github/workflows/ 0000775 0000000 0000000 00000000000 15270141376 0023062 5 ustar 00root root 0000000 0000000 zarr-developers-zarr-python-a2f2415/.github/workflows/big-endian.yml 0000664 0000000 0000000 00000005145 15270141376 0025607 0 ustar 00root root 0000000 0000000 # Run the byte-order-sensitive tests on a big-endian host: s390x, emulated with QEMU.
# Emulation makes the full suite take about 40 minutes, so this job runs only the tests
# that exercise data types, codecs and metadata, and only when that code changes (plus
# a weekly run to catch anything the path filter misses).
name: Big-endian
on:
pull_request:
branches: [ main ]
paths:
- "src/zarr/codecs/**"
- "src/zarr/core/buffer/**"
- "src/zarr/core/dtype/**"
- "src/zarr/core/metadata/**"
- "src/zarr/metadata/**"
- "tests/test_codecs/**"
- "tests/test_dtype/**"
- "tests/test_metadata/**"
- "ci/big-endian/**"
- ".github/workflows/big-endian.yml"
schedule:
- cron: "0 3 * * 1"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
s390x:
name: s390x (QEMU)
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # hatch-vcs needs the tags to resolve a version
persist-credentials: false
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
platforms: s390x
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Build test image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: ci/big-endian
platforms: linux/s390x
tags: zarr-big-endian
load: true
cache-from: type=gha,scope=big-endian
cache-to: type=gha,scope=big-endian,mode=max
- name: Run tests
run: |
docker run --rm --platform linux/s390x -v "$PWD":/src:ro zarr-big-endian bash -euc '
cp -r /src /work && cd /work
# Resolve dependencies against pyproject.toml rather than trusting whatever the
# image provides, then fail if any installed package falls outside the constraints.
pip install -q --no-build-isolation .
pip check
python -c "import sys; assert sys.byteorder == \"big\", sys.byteorder"
python -m pytest -p no:cacheprovider --hypothesis-profile ci -n auto \
tests/test_dtype tests/test_dtype_registry.py \
tests/test_codecs tests/test_metadata \
tests/test_array.py tests/test_v2.py tests/test_info.py \
tests/test_pipeline_parity.py tests/test_cli/test_migrate_v3.py
'
zarr-developers-zarr-python-a2f2415/.github/workflows/check_changelogs.yml 0000664 0000000 0000000 00000002221 15270141376 0027051 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@3d3c42e5aac5ba805825da76410c181273ba90b1 #Â v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Check zarr-python changelog entries
run: just check-changelogs
- name: Check zarr-metadata changelog entries
run: just check-changelogs packages/zarr-metadata/changes
- name: Check zarr-indexing changelog entries
run: just check-changelogs packages/zarr-indexing/changes
- name: Check zarr-http-server changelog entries
run: just check-changelogs packages/zarr-http-server/changes
zarr-developers-zarr-python-a2f2415/.github/workflows/codspeed.yml 0000664 0000000 0000000 00000003130 15270141376 0025370 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Install Hatch
uses: pypa/hatch@f647ed70d49adb885f53a27d1c7f5bdaeacf2c60
with:
version: '1.16.5'
# No interpreter is set up in this job on purpose. Benchmarks are only comparable
# across runs if the interpreter underneath them does not move: adding a pinned
# Python here measured a uniform ~13% slowdown on every benchmark, because hatch
# then built the environment on that interpreter instead of the runner's own.
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Run the benchmarks
uses: CodSpeedHQ/action@821a40cd6c80e3cfe387caf55668287addd808da # v5.5.0
env:
ZARR_BENCHMARK_CLEAR_CACHE: '1'
with:
mode: walltime
run: HATCH_ENV=test.py3.12-minimal just benchmark-codspeed
zarr-developers-zarr-python-a2f2415/.github/workflows/docs.yml 0000664 0000000 0000000 00000002647 15270141376 0024546 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
# 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: just check-doc-exports
- run: just lint-docs
# --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: just docs-build
env:
DISABLE_MKDOCS_2_WARNING: "true"
NO_MKDOCS_2_WARNING: "true"
- run: just check-doc-links
continue-on-error: true
zarr-developers-zarr-python-a2f2415/.github/workflows/downstream.yml 0000664 0000000 0000000 00000011230 15270141376 0025765 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Check out xarray
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- 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 ..
# Keep the branch override: syncing xarray would reinstall zarr from PyPI.
- name: Show versions
working-directory: xarray
run: |
uv run --no-sync python -c "
import zarr; print(f'zarr {zarr.__version__}')
import xarray; print(f'xarray {xarray.__version__}')
"
# xarray keeps its backend tests in the `xarray/tests/backends` package
# (pydata/xarray#11633 split the former `test_backends*.py` modules into
# it). Running the package covers the zarr store, region/append, API and
# DataTree tests; the other backends are installed by the `io` extra, so
# their tests run too, as they did before the split.
- name: Run xarray backend tests
working-directory: xarray
run: |
uv run --no-sync python -m pytest --no-header -q \
xarray/tests/backends
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Check out numcodecs
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: zarr-developers/numcodecs
fetch-depth: 0
path: numcodecs
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- 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-developers-zarr-python-a2f2415/.github/workflows/gpu_test.yml 0000664 0000000 0000000 00000005655 15270141376 0025452 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- name: Install Hatch
uses: pypa/hatch@f647ed70d49adb885f53a27d1c7f5bdaeacf2c60
with:
version: '1.16.5'
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
# Two names for the same environment on purpose: `just setup` builds whatever
# HATCH_ENV points at, while `just gpu` reads GPU_HATCH_ENV so that an
# ambient HATCH_ENV can never redirect `pytest -m gpu` into a CPU environment.
- name: Set Up Hatch Env
env:
HATCH_ENV: gputest.py${{ matrix.python-version }}
run: |
just setup
- name: Run Tests
env:
GPU_HATCH_ENV: gputest.py${{ matrix.python-version }}
run: |
just gpu
- name: Upload coverage
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: gpu
verbose: true # optional (default = false)
zarr-developers-zarr-python-a2f2415/.github/workflows/hypothesis.yaml 0000664 0000000 0000000 00000007726 15270141376 0026161 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
permissions:
contents: read
issues: write
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- name: Install Hatch
uses: pypa/hatch@f647ed70d49adb885f53a27d1c7f5bdaeacf2c60
with:
version: '1.16.5'
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Set Up Hatch Env
env:
HATCH_ENV: test.py${{ matrix.python-version }}-${{ matrix.dependency-set }}
run: |
just setup
# 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 }}
PYTEST_ADDOPTS: "--report-log=output-${{ matrix.python-version }}-log.jsonl"
run: |
echo "Using Hypothesis profile: $HYPOTHESIS_PROFILE"
just 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@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
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@35b4e0a9e06f8e7e261778289cf4b968b722662d # v1.6.2
with:
log-path: output-${{ matrix.python-version }}-log.jsonl
issue-title: "Nightly Hypothesis tests failed"
issue-label: "automated issue"
zarr-developers-zarr-python-a2f2415/.github/workflows/issue-metrics.yml 0000664 0000000 0000000 00000002673 15270141376 0026411 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@671528652402b9b8a33da44b5c693b3161bc090f # v5.0.3
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-developers-zarr-python-a2f2415/.github/workflows/links.yml 0000664 0000000 0000000 00000001640 15270141376 0024726 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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-developers-zarr-python-a2f2415/.github/workflows/lint.yml 0000664 0000000 0000000 00000003604 15270141376 0024556 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
# `uvx prek` builds each hook's environment from scratch, so cache them the
# way the prek action used to. Keyed on the hook config: a new pinned rev or
# a new hook is exactly when the cached environments stop being valid.
- name: Cache prek hook environments
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/prek
# Justfile is hashed too: prek_version lives there, and a bumped prek
# must not restore a store built by the previous one. No restore-keys
# fallback for the same reason.
key: prek-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml', 'Justfile') }}
- name: Lint
run: just lint
- name: Check justfile formatting
# After the linters, and never masking them: this is cosmetic, and a
# mis-formatted recipe should not cost someone their ruff/mypy results.
if: always()
run: just just-check
zarr-developers-zarr-python-a2f2415/.github/workflows/needs_release_notes.yml 0000664 0000000 0000000 00000001637 15270141376 0027622 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-developers-zarr-python-a2f2415/.github/workflows/nightly_wheels.yml 0000664 0000000 0000000 00000002315 15270141376 0026633 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: true
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
name: Install Python
with:
python-version: '3.14'
- name: Install Hatch
uses: pypa/hatch@f647ed70d49adb885f53a27d1c7f5bdaeacf2c60
with:
version: '1.16.5'
- name: Build wheel and sdist
run: hatch build
- name: Upload nightly wheels
uses: scientific-python/upload-nightly-action@16fa02eacee1655195143de09f03676e60ef2bf5
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
zarr-developers-zarr-python-a2f2415/.github/workflows/releases.yml 0000664 0000000 0000000 00000004456 15270141376 0025421 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: true
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
name: Install Python
with:
python-version: '3.12'
- name: Install Hatch
uses: pypa/hatch@f647ed70d49adb885f53a27d1c7f5bdaeacf2c60
with:
version: '1.16.5'
- name: Build wheel and sdist
run: hatch build
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: releases
path: dist
test_dist_pypi:
name: Test distribution artifacts
needs: [build_artifacts]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
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@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: releases
path: dist
- name: Generate artifact attestation
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/*
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
zarr-developers-zarr-python-a2f2415/.github/workflows/test.yml 0000664 0000000 0000000 00000015270 15270141376 0024571 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # grab all branches and tags
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- name: Install Hatch
run: python -m pip install hatch==1.16.5
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Set Up Hatch Env
env:
HATCH_ENV: test.py${{ matrix.python-version }}-${{ matrix.dependency-set }}
run: |
just setup
- name: Run Tests
env:
HYPOTHESIS_PROFILE: ci
HATCH_ENV: test.py${{ matrix.python-version }}-${{ matrix.dependency-set }}
run: |
just coverage
- name: Upload coverage
if: ${{ matrix.dependency-set == 'optional' && matrix.os == 'ubuntu-latest' }}
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- name: Install Hatch
run: python -m pip install hatch==1.16.5
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Set Up Hatch Env
env:
HATCH_ENV: ${{ matrix.dependency-set }}
run: |
just setup
- name: Run Tests
env:
HATCH_ENV: ${{ matrix.dependency-set }}
run: |
just coverage
- name: Upload coverage
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: tests
verbose: true # optional (default = false)
doctests:
name: doctests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- name: Install Hatch
run: python -m pip install hatch==1.16.5
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Set Up Hatch Env
run: |
HATCH_ENV=doctest just setup
- name: Run Tests
run: |
just doctest
benchmarks:
name: Benchmark smoke test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- name: Install Hatch
run: python -m pip install hatch==1.16.5
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Run Benchmarks
env:
ZARR_BENCHMARK_CLEAR_CACHE: '1'
run: |
HATCH_ENV=test.py3.13-minimal just 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-developers-zarr-python-a2f2415/.github/workflows/zarr-http-server-release.yml 0000664 0000000 0000000 00000011106 15270141376 0030461 0 ustar 00root root 0000000 0000000 name: zarr-http-server release
on:
workflow_dispatch:
push:
tags:
- 'zarr_http_server-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-http-server
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0 # hatch-vcs needs full history + tags
- name: Install Hatch
uses: pypa/hatch@f647ed70d49adb885f53a27d1c7f5bdaeacf2c60
with:
version: '1.16.5'
- name: Build
run: hatch build
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: zarr-http-server-dist
path: packages/zarr-http-server/dist
test_artifacts:
name: Test built artifacts
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: zarr-http-server-dist
path: dist
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
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_http_server; print('zarr_http_server', zarr_http_server.__version__)"
# The sdist has to stand alone, and `hatch build` above builds the wheel
# from the source tree, not from the sdist. So build a wheel from the
# sdist with pip, which unpacks it under the sdist format's rules (no
# links out of the archive), then run the package's own tests and strict
# docs build from the unpacked sdist. This job has no checkout, so
# nothing comes from the repository; zarr, where a package uses it,
# resolves from PyPI.
- name: Build a wheel from the sdist with pip
run: |
sdist=$(ls dist/*.tar.gz)
uvx --python 3.12 pip@26.2.1 wheel --no-deps --wheel-dir sdist-wheel "${sdist}"
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Test and build docs from the unpacked sdist
env:
UV_PYTHON: '3.12'
run: |
mkdir sdist
tar -xzf dist/*.tar.gz -C sdist
cd sdist/*/
just test
just docs-check
upload_pypi:
name: Upload to PyPI
needs: [build, test_artifacts]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/zarr_http_server-v')
runs-on: ubuntu-latest
environment:
name: zarr-http-server-releases
url: https://pypi.org/p/zarr-http-server
permissions:
id-token: write # required for OIDC trusted publishing
attestations: write # required for artifact attestations
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: zarr-http-server-dist
path: dist
- name: Generate artifact attestation
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/*
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
upload_testpypi:
name: Upload to TestPyPI
needs: [build, test_artifacts]
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
environment:
name: zarr-http-server-releases-test
url: https://test.pypi.org/p/zarr-http-server
permissions:
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: zarr-http-server-dist
path: dist
- name: Generate artifact attestation
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/*
- name: Publish package to TestPyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
repository-url: https://test.pypi.org/legacy/
zarr-developers-zarr-python-a2f2415/.github/workflows/zarr-http-server.yml 0000664 0000000 0000000 00000011724 15270141376 0027051 0 ustar 00root root 0000000 0000000 name: zarr-http-server
# Job steps delegate to packages/zarr-http-server/justfile, the single source
# of truth for this package's verbs; CI owns only the python matrix and
# caching. Keeping the commands in one place is what makes `just check`
# locally mean the same thing as a green run here.
on:
push:
branches: [main]
paths:
- 'packages/zarr-http-server/**'
- '.github/workflows/zarr-http-server.yml'
# The docs config and header script shared by all package sites.
- 'packages/mkdocs-base.yml'
- 'packages/source-version.js'
# The package resolves zarr from the repo root for its own tests, so a
# core change can break it. Run this suite when core changes too.
- 'src/zarr/**'
pull_request:
paths:
- 'packages/zarr-http-server/**'
- '.github/workflows/zarr-http-server.yml'
# The docs config and header script shared by all package sites.
- 'packages/mkdocs-base.yml'
- 'packages/source-version.js'
# The package resolves zarr from the repo root for its own tests, so a
# core change can break it. Run this suite when core changes too.
- 'src/zarr/**'
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-http-server
strategy:
fail-fast: false
matrix:
python-version: ['3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Sync test dependency groups
# The examples group carries the deps the README examples need, so the
# test that reads a served array back with a zarr client runs here
# instead of silently skipping.
run: uv sync --group test --group examples --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-http-server
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Run ruff
run: just lint
mypy:
name: mypy
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: packages/zarr-http-server
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
- name: Set up Python
run: uv python install 3.12
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Sync test dependency group
run: uv sync --group test --python 3.12
- name: Run mypy
run: just typecheck
docs:
name: docs
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: packages/zarr-http-server
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Build docs
run: just docs-check
zarr-http-server-complete:
name: zarr-http-server complete
needs: [test, ruff, mypy, 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-developers-zarr-python-a2f2415/.github/workflows/zarr-indexing-release.yml 0000664 0000000 0000000 00000011026 15270141376 0030004 0 ustar 00root root 0000000 0000000 name: zarr-indexing release
on:
workflow_dispatch:
push:
tags:
- 'zarr_indexing-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-indexing
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0 # hatch-vcs needs full history + tags
- name: Install Hatch
uses: pypa/hatch@f647ed70d49adb885f53a27d1c7f5bdaeacf2c60
with:
version: '1.16.5'
- name: Build
run: hatch build
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: zarr-indexing-dist
path: packages/zarr-indexing/dist
test_artifacts:
name: Test built artifacts
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: zarr-indexing-dist
path: dist
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
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_indexing; print('zarr_indexing', zarr_indexing.__version__)"
# The sdist has to stand alone, and `hatch build` above builds the wheel
# from the source tree, not from the sdist. So build a wheel from the
# sdist with pip, which unpacks it under the sdist format's rules (no
# links out of the archive), then run the package's own tests and strict
# docs build from the unpacked sdist. This job has no checkout, so
# nothing comes from the repository; zarr, where a package uses it,
# resolves from PyPI.
- name: Build a wheel from the sdist with pip
run: |
sdist=$(ls dist/*.tar.gz)
uvx --python 3.12 pip@26.2.1 wheel --no-deps --wheel-dir sdist-wheel "${sdist}"
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Test and build docs from the unpacked sdist
env:
UV_PYTHON: '3.12'
run: |
mkdir sdist
tar -xzf dist/*.tar.gz -C sdist
cd sdist/*/
just test
just docs-check
upload_pypi:
name: Upload to PyPI
needs: [build, test_artifacts]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/zarr_indexing-v')
runs-on: ubuntu-latest
environment:
name: zarr-indexing-releases
url: https://pypi.org/p/zarr-indexing
permissions:
id-token: write # required for OIDC trusted publishing
attestations: write # required for artifact attestations
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: zarr-indexing-dist
path: dist
- name: Generate artifact attestation
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/*
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
upload_testpypi:
name: Upload to TestPyPI
needs: [build, test_artifacts]
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
environment:
name: zarr-indexing-releases-test
url: https://test.pypi.org/p/zarr-indexing
permissions:
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: zarr-indexing-dist
path: dist
- name: Generate artifact attestation
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/*
- name: Publish package to TestPyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
repository-url: https://test.pypi.org/legacy/
zarr-developers-zarr-python-a2f2415/.github/workflows/zarr-indexing.yml 0000664 0000000 0000000 00000012334 15270141376 0026371 0 ustar 00root root 0000000 0000000 name: zarr-indexing
on:
push:
branches: [main]
paths:
- 'packages/zarr-indexing/**'
- '.github/workflows/zarr-indexing.yml'
# The docs config and header script shared by all package sites.
- 'packages/mkdocs-base.yml'
- 'packages/source-version.js'
pull_request:
paths:
- 'packages/zarr-indexing/**'
- '.github/workflows/zarr-indexing.yml'
# The docs config and header script shared by all package sites.
- 'packages/mkdocs-base.yml'
- 'packages/source-version.js'
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-indexing
strategy:
fail-fast: false
matrix:
python-version: ['3.12', '3.13', '3.14']
# UV_PYTHON reaches every uv call inside the recipes, selecting the matrix
# interpreter while keeping the justfile interpreter-agnostic for local use.
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
# The suite imports nothing from `zarr`; it runs against the repo-root
# environment, with this package as an editable overlay, to share the
# parent project's pinned test toolchain. The recipes carry that
# invocation; this step installs dependencies up front so their
# install time is reported separately from the tests.
- name: Sync test dependency group
run: uv sync --group test
- name: Check interpreter
run: >-
uv run --group test python -c
"import sys; v = '%d.%d' % sys.version_info[:2];
assert v == '${{ matrix.python-version }}', v"
- name: Run pytest
# Suites and invocation live in packages/zarr-indexing/justfile.
run: just test
- name: Run pytest (tensorstore parity)
run: just test-tensorstore
ruff:
name: ruff
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: packages/zarr-indexing
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Run ruff
# The ruff version pin lives in packages/zarr-indexing/justfile.
run: just lint
pyright:
name: pyright
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: packages/zarr-indexing
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
- name: Set up Python
run: uv python install 3.12
- name: Sync test dependency group
run: uv sync --group test --python 3.12
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Run pyright
# The pyright invocation lives in packages/zarr-indexing/justfile.
run: just typecheck
docs:
name: docs
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: packages/zarr-indexing
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Build docs
# The strict mkdocs build lives in packages/zarr-indexing/justfile.
run: just docs-check
zarr-indexing-complete:
name: zarr-indexing 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-developers-zarr-python-a2f2415/.github/workflows/zarr-metadata-release.yml 0000664 0000000 0000000 00000011026 15270141376 0027757 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0 # hatch-vcs needs full history + tags
- name: Install Hatch
uses: pypa/hatch@f647ed70d49adb885f53a27d1c7f5bdaeacf2c60
with:
version: '1.16.5'
- name: Build
run: hatch build
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
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@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: zarr-metadata-dist
path: dist
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
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__)"
# The sdist has to stand alone, and `hatch build` above builds the wheel
# from the source tree, not from the sdist. So build a wheel from the
# sdist with pip, which unpacks it under the sdist format's rules (no
# links out of the archive), then run the package's own tests and strict
# docs build from the unpacked sdist. This job has no checkout, so
# nothing comes from the repository; zarr, where a package uses it,
# resolves from PyPI.
- name: Build a wheel from the sdist with pip
run: |
sdist=$(ls dist/*.tar.gz)
uvx --python 3.12 pip@26.2.1 wheel --no-deps --wheel-dir sdist-wheel "${sdist}"
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Test and build docs from the unpacked sdist
env:
UV_PYTHON: '3.12'
run: |
mkdir sdist
tar -xzf dist/*.tar.gz -C sdist
cd sdist/*/
just test
just docs-check
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@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: zarr-metadata-dist
path: dist
- name: Generate artifact attestation
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/*
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
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@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: zarr-metadata-dist
path: dist
- name: Generate artifact attestation
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/*
- name: Publish package to TestPyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
repository-url: https://test.pypi.org/legacy/
zarr-developers-zarr-python-a2f2415/.github/workflows/zarr-metadata.yml 0000664 0000000 0000000 00000010605 15270141376 0026343 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'
# The docs config and header script shared by all package sites, and
# zarr-python's docs hook, which this site uses.
- 'packages/mkdocs-base.yml'
- 'packages/source-version.js'
- 'mkdocs_hooks.py'
pull_request:
paths:
- 'packages/zarr-metadata/**'
- '.github/workflows/zarr-metadata.yml'
# The docs config and header script shared by all package sites, and
# zarr-python's docs hook, which this site uses.
- 'packages/mkdocs-base.yml'
- 'packages/source-version.js'
- 'mkdocs_hooks.py'
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- name: Run pyright
# Pyright runs unpinned; the interpreter pin lives 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
- name: Install just
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
with:
repo: casey/just
version: 1.58.0
- 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-developers-zarr-python-a2f2415/.github/workflows/zizmor.yml 0000664 0000000 0000000 00000001502 15270141376 0025135 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@85b2b9fb8e3325e7a23d7d0a9d7ba5ae0f999e3e # v0.7.1
zarr-developers-zarr-python-a2f2415/.gitignore 0000664 0000000 0000000 00000002104 15270141376 0021452 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-developers-zarr-python-a2f2415/.markdownlint-cli2.jsonc 0000664 0000000 0000000 00000004665 15270141376 0024154 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-developers-zarr-python-a2f2415/.pre-commit-config.yaml 0000664 0000000 0000000 00000007035 15270141376 0023753 0 ustar 00root root 0000000 0000000 ci:
autoupdate_commit_msg: "chore: update pre-commit hooks"
autoupdate_schedule: "monthly"
autofix_prs: false
# Both of these are `language: system` hooks that shell out to the local
# toolchain — `uv` for mypy, `uv` and `just` for the docs build — and need
# the repo checkout to resolve their environments from `uv.lock`. Neither is
# available on pre-commit.ci's runners. Each is covered instead by a GitHub
# Actions job (Lint for mypy, zarr-http-server for the docs build) and by
# local prek runs.
skip: [mypy, zarr-http-server-docs]
default_stages: [pre-commit, pre-push]
default_language_version:
python: python3.12
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.10
hooks:
- id: ruff-check
args: ["--fix", "--show-fixes"]
- id: ruff-format
- repo: https://github.com/codespell-project/codespell
rev: v2.4.3
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.23.3
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]
# Builds the zarr-http-server docs site with warnings as errors, which
# catches a dead cross-reference or a nav entry pointing at a file that
# no longer exists before it reaches CI.
#
# `stages: [pre-push]` overrides the default of running on every commit:
# this is a whole-site build, too slow to pay per commit and only
# actionable before the code leaves the machine. `files:` limits it to
# changes that touch the package, and `pass_filenames: false` because
# mkdocs builds the site, not a list of files. Delegating to the
# justfile keeps one definition of the build shared with CI.
- id: zarr-http-server-docs
name: zarr-http-server docs build
language: system
entry: >-
just --justfile packages/zarr-http-server/justfile
--working-directory packages/zarr-http-server docs-check
pass_filenames: false
files: ^packages/zarr-http-server/
stages: [pre-push]
- repo: https://github.com/scientific-python/cookie
rev: 2026.08.14
hooks:
- id: sp-repo-review
- repo: https://github.com/numpy/numpydoc
rev: v1.11.0
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.30.1
hooks:
- id: zizmor
- repo: https://github.com/twisted/towncrier
rev: 26.9.0
hooks:
- id: towncrier-check
zarr-developers-zarr-python-a2f2415/.python-version 0000664 0000000 0000000 00000000005 15270141376 0022465 0 ustar 00root root 0000000 0000000 3.12
zarr-developers-zarr-python-a2f2415/.pyup.yml 0000664 0000000 0000000 00000000513 15270141376 0021262 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-developers-zarr-python-a2f2415/.readthedocs.yaml 0000664 0000000 0000000 00000005374 15270141376 0022725 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 packages
# that have their own Read the Docs projects. 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. Compare against the merge base (origin/main...HEAD), not
# against the tip of main: a two-dot diff also reports everything that
# landed on main after the PR branched, which made every PR that was
# behind main build every site. The checkout is shallow, so fetch the
# full history first; without a merge base the diff fails and the build
# runs, which is the safe default. Read the Docs strips shell quoting from commands, so the
# exclude pathspecs must use the quote-free :! form, not ':(exclude)'.
# The diff is taken against the merge base (origin/main...HEAD), not
# against the tip of main: a two-dot diff also reports everything that
# landed on main after the PR branched, which made every PR that was
# behind main build every site. The checkout is shallow, so fetch the
# full history first; without a merge base the diff fails and the build
# runs, which is the safe default.
- |
if [ "$READTHEDOCS_VERSION_TYPE" = "external" ];
then
git fetch --quiet --unshallow origin main pull/$READTHEDOCS_VERSION_NAME/head || true;
if git diff --quiet origin/main...HEAD -- :!packages/zarr-metadata :!packages/zarr-indexing :!packages/zarr-http-server;
then
exit 183;
fi
fi
install:
- pip install --upgrade pip
# The docs recipes resolve their toolchain from uv.lock, so uv is all that is
# needed here besides just itself.
- pip install uv==0.12.9
# No GitHub Action is available on Read the Docs, so fetch the official just
# release and verify it against the checksum casey/just publishes. Installed
# into the build virtualenv's bin, which is already on PATH.
- |
curl -fsSL -o /tmp/just.tar.gz https://github.com/casey/just/releases/download/1.58.0/just-1.58.0-x86_64-unknown-linux-musl.tar.gz
echo "4a5cc2f53e6f0f8c59092a6cc38291eb729d46a7dd95d3ae582008881b84931d /tmp/just.tar.gz" | sha256sum -c -
tar -xzf /tmp/just.tar.gz -C "$READTHEDOCS_VIRTUALENV_PATH/bin" just
pre_build:
- |
if [ "$READTHEDOCS_VERSION_TYPE" != "tag" ];
then
just changelog-build --version Unreleased --yes;
fi
build:
html:
- just docs-build --site-dir $READTHEDOCS_OUTPUT/html
mkdocs:
configuration: mkdocs.yml
zarr-developers-zarr-python-a2f2415/FUNDING.yml 0000664 0000000 0000000 00000000103 15270141376 0021274 0 ustar 00root root 0000000 0000000 github: [numfocus]
custom: ['https://numfocus.org/donate-to-zarr']
zarr-developers-zarr-python-a2f2415/Justfile 0000664 0000000 0000000 00000014462 15270141376 0021204 0 ustar 00root root 0000000 0000000 # Development and CI verbs live here; Hatch owns Python environments in pyproject.toml.
# Install: uv tool install hatch==1.16.5 && uv tool install rust-just==1.58.0
# Select test dependencies/interpreter: HATCH_ENV=test.py3.13-minimal just test
# On Windows, use Git Bash (the same shell used by the test workflow).
set shell := ["bash", "-eu", "-o", "pipefail", "-c"]
set windows-shell := ["bash", "-eu", "-o", "pipefail", "-c"]
set positional-arguments
hatch_env := env("HATCH_ENV", "test.py3.12-optional")
# Deliberately a different variable from HATCH_ENV: `just gpu` must not inherit a
# CPU test environment that happens to be exported in the caller's shell, which
# would run `pytest -m gpu` against an environment built without the gpu feature.
gpu_env := env("GPU_HATCH_ENV", "gputest.py3.12")
# Pinned so a prek release cannot change what CI lints without a commit here.
prek_version := "0.5.3"
# Documentation and changelog tooling resolves from uv.lock, not a hatch environment.
# There is only ever one docs toolchain, so it can be locked and hash-verified, and
# dependabot's uv ecosystem keeps it current. Hatch still owns the test environments,
# which exist per interpreter and per dependency set and cannot live in one lockfile.
docs_run := "uv run --frozen --group docs"
# The hatch docs environment used to set these; they belong with the mkdocs calls now.
mkdocs_env := "DISABLE_MKDOCS_2_WARNING=true NO_MKDOCS_2_WARNING=true"
# List available recipes
default:
@just --list
# List available Python environments
envs:
hatch env show
# Create the selected Python environment and list its installed packages
setup: && list-env
hatch env create {{ quote(hatch_env) }}
# List packages in the selected Python environment
list-env:
hatch run {{ quote(hatch_env) }}:pip list
# Run unit tests; pass pytest arguments, e.g. just test -k 'array and resize'
test *args:
hatch run {{ quote(hatch_env) }}:pytest --ignore tests/benchmarks "$@"
# Run unit tests and write coverage.xml and junit.xml
coverage *args:
hatch run {{ quote(hatch_env) }}:coverage run --source=src -m pytest --ignore tests/benchmarks --junitxml=junit.xml -o junit_family=legacy "$@"
hatch run {{ quote(hatch_env) }}:coverage xml
# Run unit tests and generate an HTML coverage report
coverage-html *args:
hatch run {{ quote(hatch_env) }}:coverage run --source=src -m pytest --ignore tests/benchmarks "$@"
hatch run {{ quote(hatch_env) }}:coverage html
# Serve the HTML coverage report (default port 8000)
coverage-serve *args:
hatch run {{ quote(hatch_env) }}:python -m http.server -d htmlcov "$@"
# Run slow Hypothesis tests and write coverage.xml
hypothesis *args:
hatch run {{ quote(hatch_env) }}:coverage run --source=src -m pytest -nauto --run-slow-hypothesis tests/test_properties.py tests/test_store/test_stateful* tests/test_array_stateful.py "$@"
hatch run {{ quote(hatch_env) }}:coverage xml
# Validate executable documentation code blocks
doctest *args:
hatch run doctest:pytest tests/test_docs.py -v "$@"
# Run the benchmark suite
benchmark *args:
hatch run {{ quote(hatch_env) }}:pytest --benchmark-enable tests/benchmarks "$@"
# Run benchmarks under CodSpeed
benchmark-codspeed *args:
hatch run {{ quote(hatch_env) }}:pytest tests/benchmarks --codspeed "$@"
# Run GPU tests with coverage; select the environment with GPU_HATCH_ENV
gpu *args:
HATCH_ENV={{ quote(gpu_env) }} just coverage -m gpu "$@"
# Build documentation (warnings are errors)
docs-build *args:
{{ mkdocs_env }} {{ docs_run }} mkdocs build --strict "$@"
# Serve documentation with live reload
docs-serve *args:
{{ mkdocs_env }} {{ docs_run }} mkdocs serve --watch src "$@"
# Check that every public export has API documentation
check-doc-exports *args:
{{ docs_run }} python ci/check_documented_exports.py docs/api "$@"
# Check documentation source conventions
lint-docs *args:
{{ docs_run }} python ci/lint_docs.py "$@"
# Report unlinked types in built documentation
check-doc-links *args:
{{ docs_run }} python ci/check_unlinked_types.py "$@"
# Run source documentation checks followed by a strict build
docs-check: check-doc-exports lint-docs docs-build
# Run all pre-commit hooks (ruff, codespell, mypy, repo-review, ...)
lint *args:
uvx prek@{{ prek_version }} run --show-diff-on-failure --color=always --all-files "$@"
# Run hooks with a custom selection, e.g. just hooks run --last-commit
hooks +args:
uvx prek@{{ prek_version }} "$@"
# prek is installed as a persistent uv tool rather than run through uvx: the hook shim
# prek writes into .git/hooks hard-codes the binary path it was installed from and falls
# back to `prek` on PATH, and a uvx archive path stops existing at the next cache prune.
# Install local pre-commit hooks
hooks-install:
uv tool install prek=={{ prek_version }}
prek install
# Type-check the library using the locked tooling environment
typecheck *args:
uv run --frozen mypy "$@"
# Check that uv.lock is in sync with pyproject.toml
lock-check:
uv lock --check
# Update the dependency lockfile
lock *args:
uv lock "$@"
# Build the source distribution and wheel
build *args:
hatch build "$@"
# Create a changelog fragment (interactive without arguments)
changelog *args:
{{ docs_run }} towncrier create "$@"
# Preview the next release's changelog
changelog-draft *args:
{{ docs_run }} towncrier build --draft --version Unreleased "$@"
# Build release notes; pass --version and --yes when preparing a release
changelog-build *args:
{{ docs_run }} towncrier build "$@"
# Check changelog filenames (default: changes/; accepts a package changes directory)
check-changelogs *args:
uv run --no-project python ci/check_changelog_entries.py "$@"
# Check recipe formatting of the root Justfile and every package justfile
just-check:
just --fmt --check
shopt -s nullglob; for f in packages/*/justfile; do just --justfile "$f" --fmt --check; done
# Run a zarr-metadata recipe, or list its recipes with no arguments
zarr-metadata *args:
just --justfile packages/zarr-metadata/justfile "$@"
# Run a zarr-indexing recipe, or list its recipes with no arguments
zarr-indexing *args:
just --justfile packages/zarr-indexing/justfile "$@"
# Run a zarr-http-server recipe, or list its recipes with no arguments
zarr-http-server *args:
just --justfile packages/zarr-http-server/justfile "$@"
zarr-developers-zarr-python-a2f2415/LICENSE.txt 0000664 0000000 0000000 00000002144 15270141376 0021311 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-developers-zarr-python-a2f2415/README.md 0000664 0000000 0000000 00000006715 15270141376 0020755 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?
The `zarr` library is a Python implementation of the [Zarr storage format](https://zarr.dev/). `zarr` delivers compressed, chunked, N-dimensional arrays that work well for parallel computing and object storage. 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 NumPy-compatible `dtype`s.
- [**Chunk arrays**](https://zarr.readthedocs.io/en/stable/user-guide/performance/#chunk-optimizations) along any dimension.
- [**Encode**](https://zarr.readthedocs.io/en/stable/user-guide/arrays/#compressors) chunks using a variety of useful encodings (e.g., compression).
- [**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).
## Repository sub-packages
In addition to the primary `zarr` implementation, this repository contains other packages that provide specialized functionality with minimal dependencies:
- [`zarr-metadata`](https://github.com/zarr-developers/zarr-python/tree/main/packages/zarr-metadata): Tools for Zarr metadata. Install with `pip install zarr-metadata`.
- [`zarr-indexing`](https://github.com/zarr-developers/zarr-python/tree/main/packages/zarr-indexing): Tools for lazily indexing chunked arrays. Install with `pip install zarr-indexing`.
- [`zarr-http-server`](https://github.com/zarr-developers/zarr-python/tree/main/packages/zarr-http-server): An HTTP server implementation targeting Zarr data. Install with `pip install zarr-http-server`.
zarr-developers-zarr-python-a2f2415/TEAM.md 0000664 0000000 0000000 00000001403 15270141376 0020533 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-developers-zarr-python-a2f2415/bench/ 0000775 0000000 0000000 00000000000 15270141376 0020544 5 ustar 00root root 0000000 0000000 zarr-developers-zarr-python-a2f2415/bench/compress_normal.py 0000664 0000000 0000000 00000001655 15270141376 0024330 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-developers-zarr-python-a2f2415/bench/compress_normal.txt 0000664 0000000 0000000 00000025234 15270141376 0024516 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-developers-zarr-python-a2f2415/changes/ 0000775 0000000 0000000 00000000000 15270141376 0021075 5 ustar 00root root 0000000 0000000 zarr-developers-zarr-python-a2f2415/changes/.gitignore 0000664 0000000 0000000 00000000014 15270141376 0023060 0 ustar 00root root 0000000 0000000 !.gitignore
zarr-developers-zarr-python-a2f2415/changes/README.md 0000664 0000000 0000000 00000000575 15270141376 0022363 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-developers-zarr-python-a2f2415/ci/ 0000775 0000000 0000000 00000000000 15270141376 0020060 5 ustar 00root root 0000000 0000000 zarr-developers-zarr-python-a2f2415/ci/big-endian/ 0000775 0000000 0000000 00000000000 15270141376 0022055 5 ustar 00root root 0000000 0000000 zarr-developers-zarr-python-a2f2415/ci/big-endian/Dockerfile 0000664 0000000 0000000 00000005441 15270141376 0024053 0 ustar 00root root 0000000 0000000 # Test image for a big-endian host (s390x, run under QEMU by .github/workflows/big-endian.yml).
#
# Debian packages are used as a source of prebuilt binaries for s390x, which PyPI does not
# have, so that numpy and friends do not compile under emulation. They do not define the
# test environment: zarr's pyproject.toml does. The workflow installs zarr with its
# dependencies resolved and runs `pip check`, so a Debian package that falls outside the
# declared constraints fails the job at install time instead of through unrelated-looking
# test failures. Anything Debian cannot satisfy is built here from source so the compile is
# cached as an image layer rather than repeated in every job.
#
# Debian does not package Google's crc32c library, so it is built here for the
# google-crc32c C extension.
FROM debian:sid
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
build-essential cmake cython3 git meson ninja-build pkgconf python3 python3-dev \
python3-mesonpy python3-pip python3-setuptools-scm python3-venv \
python3-crc32c python3-donfig python3-fsspec python3-hatch-vcs python3-hatchling \
python3-hypothesis python3-numcodecs python3-numpy python3-numpydoc python3-packaging \
python3-pytest python3-pytest-asyncio python3-pytest-benchmark python3-pytest-xdist \
python3-tomlkit python3-typer python3-typing-extensions \
&& rm -rf /var/lib/apt/lists/*
RUN git clone -q --depth 1 --branch 1.1.2 https://github.com/google/crc32c /tmp/crc32c \
&& cmake -S /tmp/crc32c -B /tmp/crc32c/build -DBUILD_SHARED_LIBS=1 \
-DCRC32C_BUILD_TESTS=0 -DCRC32C_BUILD_BENCHMARKS=0 -DCRC32C_USE_GLOG=0 \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
&& cmake --build /tmp/crc32c/build -j"$(nproc)" \
&& cmake --install /tmp/crc32c/build \
&& ldconfig \
&& rm -rf /tmp/crc32c
RUN python3 -m venv --system-site-packages /venv
ENV PATH=/venv/bin:$PATH
RUN pip install -q --no-binary google-crc32c google-crc32c msgspec pytest-accept
# Debian sid's python3-dateutil 3.0.0 declares a dependency on the `tzdata` distribution,
# which no Debian package installs, so `pip check` fails on the image as built. tzdata is
# pure Python; install it from PyPI.
RUN pip install -q tzdata
# Debian sid ships numcodecs 0.17.0, which zarr excludes (see pyproject.toml; GHSA-jqc8-qjfv-5m6r),
# and PyPI has no s390x wheels, so build a satisfying numcodecs from source into the venv, where
# it shadows the Debian package. Built without isolation so the Debian meson, Cython and numpy
# are used instead of compiling a build-time numpy under emulation. Remove once Debian ships a
# version that satisfies zarr's constraint; `pip check` in the workflow will say when it does not.
RUN pip install -q --no-binary numcodecs --no-build-isolation "numcodecs>=0.16.6,!=0.17.0"
zarr-developers-zarr-python-a2f2415/ci/check_changelog_entries.py 0000664 0000000 0000000 00000004307 15270141376 0025253 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-developers-zarr-python-a2f2415/ci/check_documented_exports.py 0000664 0000000 0000000 00000013530 15270141376 0025504 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-developers-zarr-python-a2f2415/ci/check_unlinked_types.py 0000664 0000000 0000000 00000005517 15270141376 0024634 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-developers-zarr-python-a2f2415/ci/lint_docs.py 0000664 0000000 0000000 00000034616 15270141376 0022422 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-developers-zarr-python-a2f2415/codecov.yml 0000664 0000000 0000000 00000001277 15270141376 0021641 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-developers-zarr-python-a2f2415/design/ 0000775 0000000 0000000 00000000000 15270141376 0020736 5 ustar 00root root 0000000 0000000 zarr-developers-zarr-python-a2f2415/design/chunk-grid.md 0000664 0000000 0000000 00000133023 15270141376 0023315 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-developers-zarr-python-a2f2415/docs/ 0000775 0000000 0000000 00000000000 15270141376 0020415 5 ustar 00root root 0000000 0000000 zarr-developers-zarr-python-a2f2415/docs/_static/ 0000775 0000000 0000000 00000000000 15270141376 0022043 5 ustar 00root root 0000000 0000000 zarr-developers-zarr-python-a2f2415/docs/_static/favicon-96x96.png 0000664 0000000 0000000 00000030652 15270141376 0025007 0 ustar 00root root 0000000 0000000 ‰PNG
IHDR _ ` {`®� pHYs à ÃÇo¨d tEXtSoftware www.inkscape.org›î< IDATxœÝ}y|UÅùþóι7�°ä&l’�Uµ‚€¢–*
Ep\@[kµT�l€âUY²€ÕVÛZpׯ-¨ˆ€âŠl²…, [¶sæ}~Ü$d¹AkÁå÷|>ç“›sž3gÎ33ï̼³!‰_òL;[wG4ä¸òÜkŒ™Ú½3>ü©ãõßB~IâïNêjÁ4!o‚àPD1Û”E±ŸcÍ.:ÒÐ�[OÍøè§Žç÷Å/Bü}Áô¶JÎ 1€©8]Q?~jæk Pp_zˆžk•«ÌÜñÓÅøûág-~Þ½)§1·
±�‚¿Ôº\QÆ6eìÿ`HüJÜ—6Âs•ò\Â3¿ø£ü_Á|7åÇÇ7Áôö÷¥=eÄl0"‡d~©ëz"¦ó†Ûo÷×e˜O U05uì¾à¤®§8Ê?¾Ÿ:Õ‘L�s\Eä !A @*þ†�G`ºö–EG3·n¸yï”ô¢ÖÛv,Ƽy `�„‡¦¯°®`JZï‚{SÇŠOV´ž:sÏ�þbõàgav
¦LI±·+˜+Ä?j]Þ-‚¿�HÁLó…2 @Û—mÊü¸4PÅÐe? � î¡ÿW= ½SRG*¤³Š>ùs¨~R³“L�+˜’þWÀûŠäØzhÇ<èk"¼Š„2@�ª«ÂñÓf<¡oB�Pz_L„ˆTH)b û-
îM»/=½í©{»ïÆO’ó÷'6×rßnÐ ì£â~‘ªœ_âa1XJ'‚ÿªLa„Ç6eûÅO?^áæ§¦&ˆß\Õ#€æÆMËX ù“ÓF‘cšñrèÿÔ_ÑÖïqi‹ÌÌ}§þÍkâGߤI-”ö�ìÊÓµ/“¸_ÙyÄŠ¬1Ô{ tâ/a[;ZW| (HK!@{ˆ|²”†‘J³CÈ$#r¬õŒ�øUüI©cA$P17>##ïä¿yxü(f'Ò¤f©©3Uu(wÃ"T�Ö<Ê�1ïqÈsŒêRý«*Úº|�QQ�æážI‘]g̘£Æ¬‡Õx³•äiµ¹¢bh¹ÁÓ 5ulÎäÉO…µqJ[;;ÓÒ¢*&
ðg@šTY U´2ˆü”yjõ,¼„Pû¦>x žèÓ{
ÊâšÆxÉ©cøÖµÊš¶ `�‡?}ú6 Ûö¥¥�éxìáá½)é·‰â³Ó²+\ [Ϙñ:D$bêU)éÔ+[”0{öÁ“«Êqœñw¦¥E7ôpg#;HN©CP@.€' Y è Ìð XKøÊœ/P/Bù´8Ašm�ŽåÄé ö(ì9É©=a°�ê‰ 8mæÌ/óSÒÎJP÷@䌽Éiç¨è‡¦º ,ŰaN~R‡ßíMI@½9³³÷ŸT‘p’ÍÎþÔÔ&ùãÓ¦4p±‹Ä½°‰PóPŸø6¨òJP@ù:¬Ü … è5ëaå
QÈ2X¹
êÐê‡qYsAì�ÄâB¾�jƒ:´€Xù<.+c®µÜFK5*ÝU¹Ý(bjpçÏWýðäcÀé›?!}tÞøñ±'S¯“’ó¿NNnäZçOBLØŒ!›\*PðxkO ü›”(Ý¡,VhUÏT Jõ —AÍIþDêK•Á†ºTh�=ó] ïŒOîO0‚jÜ‚{Rn…±+âfÍÊ
qµ’Ž„Ùë¬/¸'¥�ˆ‰µ
Ü�:VÉwÉü
Ô*DÅÆÏÎX„`ЗW¤ƒóǧ6iè•,Œ}ôÑÃÿ«nÿSÎß{{0*ÿîÔ ¶Üé/Ê šUUˆ• (”…B0Ä·œ'VZ�ðר@�óAÈ›T^ •�ªò¸Zm¢a
~-³R‰¸ÙYok {‚"ô|—Ü“rëî;SêT´ ÷pæûª8MÈÎ 1=îNûõ]ÛÖxÆÔ©^ÂïDúñJ‰4‘OêÝÇ�kúCµ~ ø»Çäý)åÏŒ,ÞE6€pæ
nV+WªÕT,¢b‘õó±*ƒŒÇ5By D:�Ú£
aÄJØw0Ê5q1QO ¥ð;¸œÄ~¡qê� 僸¿dÌ5¿†’ž1}Åb,V§–º®G0ŠÏJØàм?¥Ýüí�ÁÆ?DÇÿÊìl7.2
çoŒt@⪺ŠZíyK�lÇJ’ó!U-�zøX«äƒBc }†¨(A@8ß |FÁÍ£¢JòKŽÅæß•:2‚Q[<6õ(€�)2 ¦NÕ8` †
sòZ¶,Ž)�e‹üq)£ç¼šôøŒÂÚï÷·Œ‘¼;“¯¤kÈ–Jãs|ËZþeÚוñ7*nëÇ2æo7.Ò’7.Õç”E-lýÏ©ÅßWÏï%þ¦aÁˆèæÅ·64
òHüµáxÓq5„ z8ÀÓ îФ–pÇ>¦átQ$ dÈÇ4«þñû7˜&DLÝ¿§x…4�?)DZëòþ˜VŽå
"ªê À¼y6X˜gÊ(
~e<Ùà@çß™ÂÈRYˆRËô‘ "ËóîH¾FZƒÜîºîùw¦qüoѲ*ót|ôÑ2 ÿþúŽä–ÖW|Cþ�i>F–?›0{vÉwézBñ7Ü~»¿µ/0*º9¦ h+0CÂõˆ Í¡ò#¦#)OhN TêæØ
>w`hšÃÊ# ã%ÔÃ
ßÂ'÷À`Œ(D ÐD! P¹Âט+ÝcÎPý(ó
'xT*½¢µa±²Ø_òZ”5”Âe
x …'èZ¼×¢Ñ;yß6D4-û°ÏªÔЮL|®¡�rÀOmiÄЂ;Ò´¼´á¢¶OM-/àð6?ôåßžzËiØB‹9P„*µíî§°04�S‘ Eóªëu›š[EùGª£'Tþ
Êøê6¼ß"Ê©€É‡•4RæBѾ@«¬¬c �Ï|¾¡¯d!,T]“CEdÞ)×ÖðõW”†Ž�>Z÷÷™/1Òû7=S.ʽ°l^pGÚˆÝc‚Ç›¨•ï