diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a4141d7356..3f995e8b5e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -201,6 +201,32 @@ jobs:
run: |
HATCH_ENV=test.py3.13-minimal just benchmark
+ examples:
+ name: Example scripts
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 0 # required for hatch version discovery when uv builds zarr
+ persist-credentials: false
+ - name: Set up Python
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
+ with:
+ python-version: '3.13'
+ cache: 'pip'
+ - name: Install uv
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
+ - 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 Examples
+ run: |
+ HATCH_ENV=test.py3.13-minimal just test-examples
+
test-complete:
name: Test complete
@@ -209,7 +235,8 @@ jobs:
test,
test-upstream-and-min-deps,
doctests,
- benchmarks
+ benchmarks,
+ examples
]
if: always()
runs-on: ubuntu-latest
diff --git a/Justfile b/Justfile
index b3f187e231..df96003eef 100644
--- a/Justfile
+++ b/Justfile
@@ -64,6 +64,10 @@ hypothesis *args:
doctest *args:
hatch run doctest:pytest tests/test_docs.py -v "$@"
+# Run each example script against the local checkout, in its PEP 723 environment
+test-examples *args:
+ hatch run {{ quote(hatch_env) }}:pytest --run-examples tests/test_examples.py -v "$@"
+
# Run the benchmark suite
benchmark *args:
hatch run {{ quote(hatch_env) }}:pytest --benchmark-enable tests/benchmarks "$@"
diff --git a/changes/4560.bugfix.md b/changes/4560.bugfix.md
new file mode 100644
index 0000000000..3dcf01a342
--- /dev/null
+++ b/changes/4560.bugfix.md
@@ -0,0 +1,8 @@
+Encoding a chunk through a numcodecs filter no longer passes the
+filter's flattened output to the next codec: `_encode_sync` reshapes the
+flattened array to the filter's output shape (from
+`resolve_metadata`), so downstream codecs (e.g. `TransposeCodec` or an `imagecodecs` serializer chained after a
+`Delta` or `FixedScaleOffset` filter) receive a chunk of the expected
+shape instead of a flat 1-D array that could make writing fail or
+silently corrupt data. Shape-changing codecs such as `PackBits` get
+their own resolved output shape.
diff --git a/examples/README.md b/examples/README.md
index a6b5fa2179..fa664d2c58 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -19,7 +19,7 @@ examples/
To add a new example:
1. Create a new subdirectory: `examples/my_example/`
-2. Add your Python code: `examples/my_example/my_example.py`
+2. Add your Python code: `examples/my_example/my_example.py`. Start it with a [PEP 723](https://peps.python.org/pep-0723/) header that lists its dependencies, including `zarr`. `just test-examples` runs every example against your local checkout, and CI runs it on every pull request.
3. Create documentation: `examples/my_example/README.md`
4. Create a documentation page at `docs/user-guide/examples/my_example.md`. The documentation page should simply link to the `README.md` and the source code, e.g.:
diff --git a/examples/codec_pipeline_performance/codec_pipeline_performance.py b/examples/codec_pipeline_performance/codec_pipeline_performance.py
index b821f3e6a7..d146c51f4a 100644
--- a/examples/codec_pipeline_performance/codec_pipeline_performance.py
+++ b/examples/codec_pipeline_performance/codec_pipeline_performance.py
@@ -1,5 +1,5 @@
# /// script
-# requires-python = ">=3.11"
+# requires-python = ">=3.12"
# dependencies = [
# "zarr @ git+https://github.com/zarr-developers/zarr-python.git@main",
# "numpy",
diff --git a/examples/sharding_coalescing/sharding_coalescing.py b/examples/sharding_coalescing/sharding_coalescing.py
index 5da62ed806..66838d2255 100644
--- a/examples/sharding_coalescing/sharding_coalescing.py
+++ b/examples/sharding_coalescing/sharding_coalescing.py
@@ -1,5 +1,5 @@
# /// script
-# requires-python = ">=3.11"
+# requires-python = ">=3.12"
# dependencies = [
# "zarr @ git+https://github.com/zarr-developers/zarr-python.git@main",
# "numpy",
diff --git a/packages/zarr-metadata/README.md b/packages/zarr-metadata/README.md
index 3c5513d1dd..12583e0c96 100644
--- a/packages/zarr-metadata/README.md
+++ b/packages/zarr-metadata/README.md
@@ -41,7 +41,7 @@ closely model the content of the Zarr specifications, such as:
`TypedDict` with its rules, and the scope that decides which definitions
read a document. `zarr_metadata.v3` and `zarr_metadata.v2` export every
definition and scope of their format, such as `BYTES_CODEC` and
- `CORE_AND_EXTENSIONS`.
+ `CORE_AND_EXTENSIONS_CONTEXT`.
- **JSON type checking** ([`zarr_metadata.typed_json`](https://zarr-metadata.readthedocs.io/en/latest/api/typed_json/)):
`check` checks a JSON value against any `TypedDict` and reports every
problem with its location.
@@ -104,7 +104,9 @@ of context-free consistency rules, e.g.:
In a v3 document, validation also checks each extension point: the data
type, the chunk grid, the chunk key encoding, each codec, and each storage
transformer. Each one is checked by the definition that claims its name in
-a scope. The scope is `CORE_AND_EXTENSIONS` unless a `context` is passed.
+a scope. The scope is `CORE_AND_EXTENSIONS_CONTEXT` unless a `context` is passed.
+`ZARR_PYTHON_CONTEXT` adds the data types that zarr-python writes but no
+specification registers.
If the definition rejects a configuration, the reader rejects it too,
and a configuration key the definition does not declare is reported as
`unknown_key`. If no definition in the scope claims a name, the reader
@@ -232,13 +234,13 @@ each field:
```python test="true" session="readme"
from zarr_metadata.model import read_array_metadata_v3
from zarr_metadata.definition import canonical_of, with_problems
-from zarr_metadata.v3.definition import CORE
+from zarr_metadata.v3.definition import CORE_CONTEXT
reading = read_array_metadata_v3(raw)
beyond_core = [
loc
for loc, field in reading.fields()
- if field.name is not None and CORE.claimant(field.read_as, field.name) is None
+ if field.name is not None and CORE_CONTEXT.claimant(field.read_as, field.name) is None
]
simplest = {
loc: canonical_of(field, problems)
@@ -283,7 +285,7 @@ scope claims is an `UnclaimedField` too, also when it sits inside another
field, such as a codec inside a shard. A model's `unclaimed_fields` lists
every field nothing in scope claims, at any depth, by its location, and
leaves out the members that waive `must_understand`: a reader can open
-the node when it is empty. `CORE` holds
+the node when it is empty. `CORE_CONTEXT` holds
`CONSOLIDATED_METADATA_EXTRA_FIELD`, which reads a group's
`consolidated_metadata` as the envelope the spec fixes. A group's
`consolidated()` reads the documents that member lists, in the group's
@@ -291,7 +293,7 @@ own scope.
A v3 model consists of its document and the scope it was read in.
`ZarrV3ArrayMetadata(document, context=None)` reads the document in the
-scope (`CORE_AND_EXTENSIONS` when none is given) and raises
+scope (`CORE_AND_EXTENSIONS_CONTEXT` when none is given) and raises
`MetadataValidationError` with every problem, so an invalid model is never
built. `to_json` returns the document as it was written, and
`to_key_value` writes that same document to store bytes.
@@ -322,7 +324,7 @@ all of them with `ZarrV3ConsolidatedMetadata`, or with
`read_consolidated_metadata_v3`, which returns problems instead of
raising them. A document with a problem then fails only when it is read.
Pass `context=group.context` to read the documents in the group's own
-scope; with no `context`, they are read in `CORE_AND_EXTENSIONS`.
+scope; with no `context`, they are read in `CORE_AND_EXTENSIONS_CONTEXT`.
To write consolidated metadata, build a `ZarrV3ConsolidatedMetadata` and
pass it to the group's `update`. The group reads the listed models in its
@@ -332,7 +334,7 @@ models as entries. Such an entry is accepted when its claims refine into
the model's scope, and refused at its path otherwise. To consolidate
children that were read in several scopes, consolidate them in the
`union` of those scopes. Every reader takes `context=None` for the
-default scope, `CORE_AND_EXTENSIONS`.
+default scope, `CORE_AND_EXTENSIONS_CONTEXT`.
Two models are equal when they represent the same document, even if each
is spelled differently. Values the package interprets (each field, and the
diff --git a/packages/zarr-metadata/changes/4575.feature.md b/packages/zarr-metadata/changes/4575.feature.md
new file mode 100644
index 0000000000..bd1d87ab40
--- /dev/null
+++ b/packages/zarr-metadata/changes/4575.feature.md
@@ -0,0 +1 @@
+Added `zarr_metadata.testing.strategies`, Hypothesis strategies that draw valid Zarr v3 metadata fields, behind a new `testing` extra (`pip install zarr-metadata[testing]`). There is one strategy for every codec, data type, chunk grid and chunk key encoding the package defines that holds no other field. `fields` draws every envelope spelling the spec allows, or only the canonical one with `spellings="canonical"`. Codecs can be drawn for the `Chunk` they are handed, chunk grids for an array shape, and fill values for a data type. The strategies are a mapping keyed by definition, so an extension author can add strategies for their own definitions.
diff --git a/packages/zarr-metadata/changes/4583.feature.md b/packages/zarr-metadata/changes/4583.feature.md
new file mode 100644
index 0000000000..cbbe44f174
--- /dev/null
+++ b/packages/zarr-metadata/changes/4583.feature.md
@@ -0,0 +1 @@
+Add the `ZARR_PYTHON_CONTEXT` scope, which reads what zarr-python writes: `CORE_AND_EXTENSIONS_CONTEXT` plus zarr-python's own data types `null_terminated_bytes` (NumPy's `S`), `raw_bytes` (NumPy's `V`) and `variable_length_bytes`, which no specification registers. `CORE_AND_EXTENSIONS_CONTEXT` now also reads the `fixed_length_utf32` data type (NumPy's `U`) and the `vlen-utf8` and `vlen-bytes` codecs that zarr-extensions registers, so the strings zarr-python writes are checked instead of read as unclaimed.
diff --git a/packages/zarr-metadata/changes/4583.removal.md b/packages/zarr-metadata/changes/4583.removal.md
new file mode 100644
index 0000000000..9faa94ee31
--- /dev/null
+++ b/packages/zarr-metadata/changes/4583.removal.md
@@ -0,0 +1 @@
+Rename the scopes `CORE`, `CORE_AND_EXTENSIONS` and `CORE_V2` to `CORE_CONTEXT`, `CORE_AND_EXTENSIONS_CONTEXT` and `CORE_V2_CONTEXT`, so that a scope's name says it is a context. The old names, imported from `zarr_metadata.v3`, `zarr_metadata.v3.definition`, `zarr_metadata.v2` or `zarr_metadata.v2.definition`, still refer to the same scopes with a `DeprecationWarning`, and will be removed in a later release.
diff --git a/packages/zarr-metadata/changes/4588.feature.md b/packages/zarr-metadata/changes/4588.feature.md
new file mode 100644
index 0000000000..e066923667
--- /dev/null
+++ b/packages/zarr-metadata/changes/4588.feature.md
@@ -0,0 +1 @@
+`zarr_metadata.v3.data_type`, `zarr_metadata.v3.codec`, `zarr_metadata.v3.chunk_grid` and `zarr_metadata.v3.chunk_key_encoding` re-export every name their submodules export, so `from zarr_metadata.v3.data_type import BoolDataTypeName, Uint8DataTypeName` works again, as it did before 0.6.0. Each name is the same object as in its submodule, which stays its documented home.
diff --git a/packages/zarr-metadata/docs/api/index.md b/packages/zarr-metadata/docs/api/index.md
index 4a23c5c2fb..e95a186284 100644
--- a/packages/zarr-metadata/docs/api/index.md
+++ b/packages/zarr-metadata/docs/api/index.md
@@ -22,7 +22,7 @@ The package is organized to mirror the structure of the Zarr specifications:
- [`zarr_metadata.v2`](v2.md) — `TypedDict` shapes for Zarr v2 documents
(`.zarray`, `.zgroup`, `.zattrs`, `.zmetadata`), and
`zarr_metadata.v2.definition`: the v2 kinds of metadata, the scope
- `CORE_V2`, and the readers of one `dtype` or codec. Each dtype family's
+ `CORE_V2_CONTEXT`, and the readers of one `dtype` or codec. Each dtype family's
definition is in `zarr_metadata.v2.data_type`, and each codec's in
`zarr_metadata.v2.codec`
- [`zarr_metadata.v3`](v3/index.md) — `TypedDict` shapes for Zarr v3
@@ -35,7 +35,7 @@ The package is organized to mirror the structure of the Zarr specifications:
scope type `Context`. Its module docstring is the guide
- [`zarr_metadata.v3.definition`](v3/definition.md) — the five v3 kinds
of metadata (`CodecDefinition`, `DataTypeDefinition`, ...), the codec
- pipeline types, and the v3 scopes `CORE` and `CORE_AND_EXTENSIONS`
+ pipeline types, and the v3 scopes `CORE_CONTEXT` and `CORE_AND_EXTENSIONS_CONTEXT`
The document types, models, and spec vocabulary — including the store keys —
are re-exported at the top level, so
diff --git a/packages/zarr-metadata/docs/api/testing.md b/packages/zarr-metadata/docs/api/testing.md
new file mode 100644
index 0000000000..a5d6145258
--- /dev/null
+++ b/packages/zarr-metadata/docs/api/testing.md
@@ -0,0 +1,8 @@
+---
+title: testing.strategies
+---
+
+::: zarr_metadata.testing.strategies
+ options:
+ inherited_members: false
+ show_if_no_docstring: false
diff --git a/packages/zarr-metadata/docs/api/v3/codec.md b/packages/zarr-metadata/docs/api/v3/codec.md
index cb96d2c7d5..ae5171acd0 100644
--- a/packages/zarr-metadata/docs/api/v3/codec.md
+++ b/packages/zarr-metadata/docs/api/v3/codec.md
@@ -22,4 +22,8 @@ title: codec
::: zarr_metadata.v3.codec.transpose
+::: zarr_metadata.v3.codec.vlen_bytes
+
+::: zarr_metadata.v3.codec.vlen_utf8
+
::: zarr_metadata.v3.codec.zstd
diff --git a/packages/zarr-metadata/docs/api/v3/data_type.md b/packages/zarr-metadata/docs/api/v3/data_type.md
index 3f8444625a..2bdfe0ad2c 100644
--- a/packages/zarr-metadata/docs/api/v3/data_type.md
+++ b/packages/zarr-metadata/docs/api/v3/data_type.md
@@ -40,8 +40,16 @@ title: data_type
::: zarr_metadata.v3.data_type.string
+::: zarr_metadata.v3.data_type.fixed_length_utf32
+
::: zarr_metadata.v3.data_type.numpy_datetime64
::: zarr_metadata.v3.data_type.numpy_timedelta64
::: zarr_metadata.v3.data_type.struct
+
+::: zarr_metadata.v3.data_type.null_terminated_bytes
+
+::: zarr_metadata.v3.data_type.variable_length_bytes
+
+::: zarr_metadata.v3.data_type.zarr_python_raw_bytes
diff --git a/packages/zarr-metadata/docs/index.md b/packages/zarr-metadata/docs/index.md
index b3936156d0..6b238b7017 100644
--- a/packages/zarr-metadata/docs/index.md
+++ b/packages/zarr-metadata/docs/index.md
@@ -39,7 +39,7 @@ closely model the content of the Zarr specifications, such as:
`TypedDict` with its rules, and the scope that decides which definitions
read a document. `zarr_metadata.v3` and `zarr_metadata.v2` export every
definition and scope of their format, such as `BYTES_CODEC` and
- `CORE_AND_EXTENSIONS`.
+ `CORE_AND_EXTENSIONS_CONTEXT`.
- **JSON type checking** ([`zarr_metadata.typed_json`](api/typed_json.md)):
`check` checks a JSON value against any `TypedDict` and reports every
problem with its location.
@@ -102,7 +102,9 @@ of context-free consistency rules, e.g.:
In a v3 document, validation also checks each extension point: the data
type, the chunk grid, the chunk key encoding, each codec, and each storage
transformer. Each one is checked by the definition that claims its name in
-a scope. The scope is `CORE_AND_EXTENSIONS` unless a `context` is passed.
+a scope. The scope is `CORE_AND_EXTENSIONS_CONTEXT` unless a `context` is passed.
+`ZARR_PYTHON_CONTEXT` adds the data types that zarr-python writes but no
+specification registers.
If the definition rejects a configuration, the reader rejects it too,
and a configuration key the definition does not declare is reported as
`unknown_key`. If no definition in the scope claims a name, the reader
@@ -234,13 +236,13 @@ each field:
```python test="true" session="index"
from zarr_metadata.model import read_array_metadata_v3
from zarr_metadata.definition import canonical_of, with_problems
-from zarr_metadata.v3.definition import CORE
+from zarr_metadata.v3.definition import CORE_CONTEXT
reading = read_array_metadata_v3(raw)
beyond_core = [
loc
for loc, field in reading.fields()
- if field.name is not None and CORE.claimant(field.read_as, field.name) is None
+ if field.name is not None and CORE_CONTEXT.claimant(field.read_as, field.name) is None
]
simplest = {
loc: canonical_of(field, problems)
@@ -285,7 +287,7 @@ scope claims is an `UnclaimedField` too, also when it sits inside another
field, such as a codec inside a shard. A model's `unclaimed_fields` lists
every field nothing in scope claims, at any depth, by its location, and
leaves out the members that waive `must_understand`: a reader can open
-the node when it is empty. `CORE` holds
+the node when it is empty. `CORE_CONTEXT` holds
`CONSOLIDATED_METADATA_EXTRA_FIELD`, which reads a group's
`consolidated_metadata` as the envelope the spec fixes. A group's
`consolidated()` reads the documents that member lists, in the group's
@@ -293,7 +295,7 @@ own scope.
A v3 model consists of its document and the scope it was read in.
`ZarrV3ArrayMetadata(document, context=None)` reads the document in the
-scope (`CORE_AND_EXTENSIONS` when none is given) and raises
+scope (`CORE_AND_EXTENSIONS_CONTEXT` when none is given) and raises
`MetadataValidationError` with every problem, so an invalid model is never
built. `to_json` returns the document as it was written, and
`to_key_value` writes that same document to store bytes.
@@ -324,7 +326,7 @@ all of them with `ZarrV3ConsolidatedMetadata`, or with
`read_consolidated_metadata_v3`, which returns problems instead of
raising them. A document with a problem then fails only when it is read.
Pass `context=group.context` to read the documents in the group's own
-scope; with no `context`, they are read in `CORE_AND_EXTENSIONS`.
+scope; with no `context`, they are read in `CORE_AND_EXTENSIONS_CONTEXT`.
To write consolidated metadata, build a `ZarrV3ConsolidatedMetadata` and
pass it to the group's `update`. The group reads the listed models in its
@@ -334,7 +336,7 @@ models as entries. Such an entry is accepted when its claims refine into
the model's scope, and refused at its path otherwise. To consolidate
children that were read in several scopes, consolidate them in the
`union` of those scopes. Every reader takes `context=None` for the
-default scope, `CORE_AND_EXTENSIONS`.
+default scope, `CORE_AND_EXTENSIONS_CONTEXT`.
Two models are equal when they represent the same document, even if each
is spelled differently. Values the package interprets (each field, and the
diff --git a/packages/zarr-metadata/docs/parsing-validation.md b/packages/zarr-metadata/docs/parsing-validation.md
index adfe8e51c2..a4122d87ea 100644
--- a/packages/zarr-metadata/docs/parsing-validation.md
+++ b/packages/zarr-metadata/docs/parsing-validation.md
@@ -161,7 +161,7 @@ This procedure yields one of two values:
`zarr-metadata` does not run each stage as a separate function. Instead, a single read of the document collects the problems from every stage, so a caller gets the full list of problems at once.
-The **registry** from the resolve-and-validate stage is called a **scope** in this library. For Zarr V3 it is a [`ZarrV3Context`](api/v3/definition.md) object, with one slot for each kind of field (`data_types`, `codecs`, `chunk_grids`, ...), and every reader takes a `context` argument. The default scope is `CORE_AND_EXTENSIONS`, which holds the data types, codecs, chunk grids, etc. defined in the Zarr V3 spec and in [zarr-extensions](https://github.com/zarr-developers/zarr-extensions/). `CORE` holds only the definitions from the Zarr V3 spec.
+The **registry** from the resolve-and-validate stage is called a **scope** in this library. For Zarr V3 it is a [`ZarrV3Context`](api/v3/definition.md) object, with one slot for each kind of field (`data_types`, `codecs`, `chunk_grids`, ...), and every reader takes a `context` argument. The default scope is `CORE_AND_EXTENSIONS_CONTEXT`, which holds the data types, codecs, chunk grids, etc. defined in the Zarr V3 spec and in [zarr-extensions](https://github.com/zarr-developers/zarr-extensions/). `CORE_CONTEXT` holds only the definitions from the Zarr V3 spec. `ZARR_PYTHON_CONTEXT` adds to `CORE_AND_EXTENSIONS_CONTEXT` the data types that zarr-python writes but no specification registers (`null_terminated_bytes` for NumPy's `S`, `raw_bytes` for NumPy's `V`, and `variable_length_bytes`), so a strict check of what zarr-python writes passes it.
`read_array_metadata_v3` reads a JSON document and returns a "reading". `reading.problems` is a tuple of the problems, and `reading.metadata` is the validated model, or `None` if there are problems. Each problem has a `loc`, which is its position in the document as a tuple, a `kind`, like `missing_key` or `invalid_value`, and a human-readable `message`:
@@ -193,10 +193,10 @@ To read one field on its own, such as a codec, use `resolve` from `zarr_metadata
```python test="true" session="parsing-validation"
from zarr_metadata.definition import resolve
-from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS, CodecDefinition
+from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS_CONTEXT, CodecDefinition
field, problems = resolve(
- {"name": "gzip", "configuration": {"level": 12}}, CodecDefinition, CORE_AND_EXTENSIONS
+ {"name": "gzip", "configuration": {"level": 12}}, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT
)
for problem in problems:
print(problem.loc, problem.kind)
@@ -214,17 +214,17 @@ To make an unknown data type or codec known, add its definition to the scope's s
```python test="true" session="parsing-validation"
from zarr_metadata.definition import EmptyConfiguration
-from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS, CodecDefinition
+from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS_CONTEXT, CodecDefinition
# The definition of the codec that was unknown: here, a codec named
# `acme.codec` that takes no configuration.
MY_CODEC = CodecDefinition(
name="acme.codec", configuration=EmptyConfiguration, kind="bytes_bytes", size="dynamic"
)
-context = CORE_AND_EXTENSIONS.update(codecs=[*CORE_AND_EXTENSIONS.codecs.values(), MY_CODEC])
+context = CORE_AND_EXTENSIONS_CONTEXT.update(codecs=[*CORE_AND_EXTENSIONS_CONTEXT.codecs.values(), MY_CODEC])
reading = read_array_metadata_v3(raw, context=context)
```
-`update` replaces each slot it is given and keeps the others. A slot can also be given as a mapping from each definition's name to it, so `CORE_AND_EXTENSIONS.update(codecs={**CORE_AND_EXTENSIONS.codecs, "gzip": MY_GZIP})` replaces the gzip definition with your own.
+`update` replaces each slot it is given and keeps the others. A slot can also be given as a mapping from each definition's name to it, so `CORE_AND_EXTENSIONS_CONTEXT.update(codecs={**CORE_AND_EXTENSIONS_CONTEXT.codecs, "gzip": MY_GZIP})` replaces the gzip definition with your own.
[^1]: The one exception is the `r*` data type, which is identified by a templated string with no `configuration` object. This mixes the identifier with the configuration in the same JSON value, which is not good design. So this library normalizes `r*` into the same form as all the other metadata objects: the field `"r16"` keeps its name, and is read by the `r*` definition with the configuration `{"bits": 16}`.
diff --git a/packages/zarr-metadata/mkdocs.yml b/packages/zarr-metadata/mkdocs.yml
index 10464e3cf0..b19c0932d2 100644
--- a/packages/zarr-metadata/mkdocs.yml
+++ b/packages/zarr-metadata/mkdocs.yml
@@ -28,6 +28,7 @@ nav:
- ' zarr_metadata.model': api/model.md
- ' zarr_metadata.problems': api/problems.md
- ' zarr_metadata.pydantic': api/pydantic.md
+ - ' zarr_metadata.testing.strategies': api/testing.md
- ' zarr_metadata.typed_json': api/typed_json.md
- ' zarr_metadata.v2': api/v2.md
- ' zarr_metadata.v3':
diff --git a/packages/zarr-metadata/pyproject.toml b/packages/zarr-metadata/pyproject.toml
index abd93bc729..25d1c7aead 100644
--- a/packages/zarr-metadata/pyproject.toml
+++ b/packages/zarr-metadata/pyproject.toml
@@ -43,6 +43,11 @@ dependencies = [
"typing_extensions>=4.16",
]
+[project.optional-dependencies]
+# `zarr_metadata.testing.strategies`. 6.85: first release with
+# `characters(exclude_categories=...)`.
+testing = ["hypothesis>=6.85"]
+
[project.urls]
Homepage = "https://github.com/zarr-developers/zarr-python"
Source = "https://github.com/zarr-developers/zarr-python/tree/main/packages/zarr-metadata"
diff --git a/packages/zarr-metadata/src/zarr_metadata/__init__.py b/packages/zarr-metadata/src/zarr_metadata/__init__.py
index bc57d90fd0..5e8b1ffcba 100644
--- a/packages/zarr-metadata/src/zarr_metadata/__init__.py
+++ b/packages/zarr-metadata/src/zarr_metadata/__init__.py
@@ -136,6 +136,16 @@
TransposeCodecMetadata,
TransposeCodecName,
)
+from zarr_metadata.v3.codec.vlen_bytes import (
+ VLEN_BYTES_CODEC_NAME,
+ VlenBytesCodecMetadata,
+ VlenBytesCodecName,
+)
+from zarr_metadata.v3.codec.vlen_utf8 import (
+ VLEN_UTF8_CODEC_NAME,
+ VlenUtf8CodecMetadata,
+ VlenUtf8CodecName,
+)
from zarr_metadata.v3.codec.zstd import ZSTD_CODEC_NAME, ZstdCodecMetadata, ZstdCodecName
from zarr_metadata.v3.consolidated import (
ZARR_V3_CONSOLIDATED_METADATA_KEY,
@@ -162,6 +172,11 @@
Complex128DataTypeName,
Complex128FillValue,
)
+from zarr_metadata.v3.data_type.fixed_length_utf32 import (
+ FIXED_LENGTH_UTF32_DATA_TYPE_NAME,
+ FixedLengthUtf32DataTypeName,
+ FixedLengthUtf32FillValue,
+)
from zarr_metadata.v3.data_type.float16 import (
FLOAT16_DATA_TYPE_NAME,
Float16DataTypeName,
@@ -265,6 +280,7 @@
"DEFAULT_CHUNK_KEY_ENCODING_NAME",
"DEFAULT_CHUNK_KEY_ENCODING_SEPARATOR",
"ENDIANNESS",
+ "FIXED_LENGTH_UTF32_DATA_TYPE_NAME",
"FLOAT16_DATA_TYPE_NAME",
"FLOAT32_DATA_TYPE_NAME",
"FLOAT64_DATA_TYPE_NAME",
@@ -291,6 +307,8 @@
"UNSET",
"V2_CHUNK_KEY_ENCODING_NAME",
"V2_CHUNK_KEY_ENCODING_SEPARATOR",
+ "VLEN_BYTES_CODEC_NAME",
+ "VLEN_UTF8_CODEC_NAME",
"ZARR_V2_ARRAY_DIMENSION_SEPARATOR",
"ZARR_V2_ARRAY_METADATA_STORE_KEY",
"ZARR_V2_ARRAY_ORDER",
@@ -325,6 +343,8 @@
"DefaultChunkKeyEncodingName",
"DefaultChunkKeyEncodingSeparator",
"Endianness",
+ "FixedLengthUtf32DataTypeName",
+ "FixedLengthUtf32FillValue",
"Float16DataTypeName",
"Float16FillValue",
"Float32DataTypeName",
@@ -378,6 +398,10 @@
"V2ChunkKeyEncodingName",
"V2ChunkKeyEncodingSeparator",
"ValidationProblem",
+ "VlenBytesCodecMetadata",
+ "VlenBytesCodecName",
+ "VlenUtf8CodecMetadata",
+ "VlenUtf8CodecName",
"ZarrV2ArrayDimensionSeparator",
"ZarrV2ArrayMetadata",
"ZarrV2ArrayMetadataJSON",
diff --git a/packages/zarr-metadata/src/zarr_metadata/_renamed.py b/packages/zarr-metadata/src/zarr_metadata/_renamed.py
new file mode 100644
index 0000000000..d0267a70db
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/_renamed.py
@@ -0,0 +1,26 @@
+"""Scopes this package renamed, which keep their old names for now with a `DeprecationWarning`."""
+
+import warnings
+from collections.abc import Mapping
+
+
+def renamed_scope(module: str, name: str, renames: Mapping[str, tuple[str, object]]) -> object:
+ """The scope that `name`, an old name in `renames`, refers to, with a `DeprecationWarning`.
+
+ `renames` maps each old name to the scope's name now and the scope. A
+ module that exported the old names calls this from its `__getattr__`,
+ so an import of an old name keeps working. Raises `AttributeError`, as
+ attribute lookup does, when `name` is not an old name.
+ """
+ if name not in renames:
+ raise AttributeError(f"module {module!r} has no attribute {name!r}")
+ new, scope = renames[name]
+ warnings.warn(
+ f"`{name}` is deprecated and will be removed; use `{new}`, the same scope",
+ DeprecationWarning,
+ stacklevel=3,
+ )
+ return scope
+
+
+__all__ = ["renamed_scope"]
diff --git a/packages/zarr-metadata/src/zarr_metadata/definition/__init__.py b/packages/zarr-metadata/src/zarr_metadata/definition/__init__.py
index 6dd9c86173..782807f4a8 100644
--- a/packages/zarr-metadata/src/zarr_metadata/definition/__init__.py
+++ b/packages/zarr-metadata/src/zarr_metadata/definition/__init__.py
@@ -40,7 +40,7 @@
configuration, checks the envelope of each nested field (an extra
member, a `must_understand` of `false`), and then runs the rules, for
one configuration: `GZIP_CODEC.read_configuration({"level": 12})`.
-3. `resolve(field, CodecDefinition, CORE_AND_EXTENSIONS)` reads a whole
+3. `resolve(field, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)` reads a whole
field in a scope. It checks the envelope, maps the name to a
definition, checks the configuration, and reads each nested field the
same way. It returns the result of the reading and every problem. The
@@ -70,17 +70,17 @@
from zarr_metadata.definition import resolve
from zarr_metadata.v3.codec.gzip import GZIP_CODEC
- from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS, CodecDefinition
+ from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS_CONTEXT, CodecDefinition
resolved, problems = resolve({"name": "gzip", "configuration": {"level": 12}},
- CodecDefinition, CORE_AND_EXTENSIONS)
+ CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)
resolved # RefusedField(..., definition=CodecDefinition(name='gzip'), ...)
problems[0].loc # ('configuration', 'level')
problems[0].input # 12
dict(problems[0].ctx) # {'ge': 0, 'le': 9}
resolved, problems = resolve({"name": "gzip", "configuration": {"level": 5}},
- CodecDefinition, CORE_AND_EXTENSIONS)
+ CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)
resolved.definition is GZIP_CODEC # True
resolved.configuration # {'level': 5}
@@ -124,7 +124,7 @@
from zarr_metadata import ValidationProblem
from zarr_metadata.definition import Nested
- from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS, CodecDefinition
+ from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS_CONTEXT, CodecDefinition
class AcmeLz4Configuration(TypedDict, closed=True):
@@ -149,7 +149,7 @@ def acme_lz4_rules(
size="dynamic",
rules=acme_lz4_rules,
)
- CONTEXT = CORE_AND_EXTENSIONS.update(codecs=[*CORE_AND_EXTENSIONS.codecs.values(), ACME_LZ4])
+ CONTEXT = CORE_AND_EXTENSIONS_CONTEXT.update(codecs=[*CORE_AND_EXTENSIONS_CONTEXT.codecs.values(), ACME_LZ4])
The TypedDict declares how keys it does not declare are handled. With
`closed=True`, such a key is a problem, as above. With `extra_items=`,
diff --git a/packages/zarr-metadata/src/zarr_metadata/definition/_definition.py b/packages/zarr-metadata/src/zarr_metadata/definition/_definition.py
index 5d50f8e605..0c30ffd16b 100644
--- a/packages/zarr-metadata/src/zarr_metadata/definition/_definition.py
+++ b/packages/zarr-metadata/src/zarr_metadata/definition/_definition.py
@@ -1374,7 +1374,7 @@ def resolve(
else raises `TypeError`.
"""
if not _is_scope(context):
- msg = f"a field is read in a scope, such as CORE_AND_EXTENSIONS, got {described(context)}"
+ msg = f"a field is read in a scope, such as CORE_AND_EXTENSIONS_CONTEXT, got {described(context)}"
raise TypeError(msg)
asked = as_kind(kind)
_check_format(asked, context)
diff --git a/packages/zarr-metadata/src/zarr_metadata/json_schema.py b/packages/zarr-metadata/src/zarr_metadata/json_schema.py
index 9b7443a8f4..ea80ac0242 100644
--- a/packages/zarr-metadata/src/zarr_metadata/json_schema.py
+++ b/packages/zarr-metadata/src/zarr_metadata/json_schema.py
@@ -30,7 +30,7 @@
**A whole `zarr.json`.** `node_metadata_json_schema_v3(context=None)`
writes a whole v3 `zarr.json`, array or group, as the scope reads it
-(`CORE_AND_EXTENSIONS` when none is given). Its fields are written
+(`CORE_AND_EXTENSIONS_CONTEXT` when none is given). Its fields are written
as the scope reads them. For each definition, the schema accepts its
field (its name, its configuration as its TypedDict declares it,
including bounds, a `must_understand` of `true`, and its bare name when
diff --git a/packages/zarr-metadata/src/zarr_metadata/model/__init__.py b/packages/zarr-metadata/src/zarr_metadata/model/__init__.py
index 98776c7c5d..ab6006ca2a 100644
--- a/packages/zarr-metadata/src/zarr_metadata/model/__init__.py
+++ b/packages/zarr-metadata/src/zarr_metadata/model/__init__.py
@@ -9,7 +9,7 @@
`read_consolidated_metadata_v2`. A reader checks a document's JSON
structure. In a v3 document it also reads each extension point (codecs,
chunk grids, data types, ...) through the definition that claims its
-name in a scope. The scope is `CORE_AND_EXTENSIONS` unless a `context`
+name in a scope. The scope is `CORE_AND_EXTENSIONS_CONTEXT` unless a `context`
is passed. A v3 reader checks the fill value against the data type the
document names, the chunk grid against the shape, and the codecs as a
pipeline, each codec against the chunk it receives. Each reader does one
diff --git a/packages/zarr-metadata/src/zarr_metadata/model/_array_v2.py b/packages/zarr-metadata/src/zarr_metadata/model/_array_v2.py
index 7768e68210..a4336ea0a5 100644
--- a/packages/zarr-metadata/src/zarr_metadata/model/_array_v2.py
+++ b/packages/zarr-metadata/src/zarr_metadata/model/_array_v2.py
@@ -70,7 +70,7 @@
from zarr_metadata.v2.attributes import ZARR_V2_ATTRIBUTES_STORE_KEY
from zarr_metadata.v2.codec import ZarrV2CodecMetadata
from zarr_metadata.v2.definition import (
- CORE_V2,
+ CORE_V2_CONTEXT,
ZarrV2CodecDefinition,
ZarrV2DataTypeDefinition,
)
@@ -437,7 +437,7 @@ def in_files(
def read_array_metadata_v2(
value: object, *, context: ZarrV2Context | None = None
) -> ZarrV2ArrayMetadataReading:
- """Read `value`, a v2 array document, in `context` (`CORE_V2` when none is given), whatever it holds.
+ """Read `value`, a v2 array document, in `context` (`CORE_V2_CONTEXT` when none is given), whatever it holds.
The result holds everything the read finds: the dtype, the compressor
and each filter as the scope read them (an `AcceptedField` read by
@@ -447,7 +447,7 @@ def read_array_metadata_v2(
problem. One that the scope does not claim is not a problem.
`fill_value` is checked against the dtype the scope read.
"""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
reading, members = read_array_v2(value, context)
if members is None:
return reading
@@ -510,7 +510,7 @@ class ZarrV2ArrayMetadata(Keyed):
node_type: Final[Literal["array"]] = "array" # noqa: PYI064 - as above
def __init__(self, document: object, context: ZarrV2Context | None = None) -> None:
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
reading, members = read_array_v2(document, context)
if members is None:
raise MetadataValidationError(reading.problems)
@@ -731,7 +731,7 @@ def with_context(self, context: ZarrV2Context | None = None) -> ZarrV2ArrayMetad
in `context`. The existing reading is reused when `context` reads
every claim identically.
"""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
if disagreements(context, claims_of(self._reading.fields())).agrees:
return self._of(self._document, context, self._reading, self._members)
return type(self)(self._document, context=context)
@@ -745,7 +745,7 @@ def refined_in(self, context: ZarrV2Context | None = None) -> ZarrV2ArrayMetadat
in the document. Raises `MetadataValidationError` when a definition
that `context` claims rejects what was written.
"""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
found = disagreements(context, claims_of(self._reading.fields()))
if len(found.conflicts) != 0:
raise ScopeConflictError(located_conflicts(self._reading.fields(), found.conflicts))
@@ -807,7 +807,9 @@ def create_default(
if lengths is not None:
given["chunks"] = lengths
if "dtype" in given and "fill_value" not in given:
- dtype, _ = resolve(given["dtype"], ZarrV2DataTypeDefinition, scoped(context, CORE_V2))
+ dtype, _ = resolve(
+ given["dtype"], ZarrV2DataTypeDefinition, scoped(context, CORE_V2_CONTEXT)
+ )
if len(fill_value_problems(dtype, 0)) != 0:
given["fill_value"] = None
merged = {key: value for key, value in {**document, **given}.items() if value is not UNSET}
@@ -836,7 +838,7 @@ def from_key_value(
valid, with every problem the read finds. Raises `TypeError` for a
scope of another format before any bytes are decoded.
"""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
files, problems = decoded_node_files_v2(
mapping, (ZARR_V2_ARRAY_METADATA_STORE_KEY, ZARR_V2_ATTRIBUTES_STORE_KEY)
)
diff --git a/packages/zarr-metadata/src/zarr_metadata/model/_array_v3.py b/packages/zarr-metadata/src/zarr_metadata/model/_array_v3.py
index 53d12254bc..58fb7e30c5 100644
--- a/packages/zarr-metadata/src/zarr_metadata/model/_array_v3.py
+++ b/packages/zarr-metadata/src/zarr_metadata/model/_array_v3.py
@@ -62,7 +62,7 @@
# A runtime import: `get_type_hints` resolves the public signatures.
from zarr_metadata.v3._context import ZarrV3Context # noqa: TC001
-from zarr_metadata.v3._core import CORE_AND_EXTENSIONS
+from zarr_metadata.v3._core import CORE_AND_EXTENSIONS_CONTEXT
from zarr_metadata.v3._definition import (
ArrayExtraFieldDefinition,
Chunk,
@@ -499,7 +499,7 @@ class ZarrV3ArrayMetadata(Keyed):
node_type: Final[Literal["array"]] = "array" # noqa: PYI064 - as above
def __init__(self, document: object, context: ZarrV3Context | None = None) -> None:
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
reading, members = read_array_v3(document, context)
if members is None:
raise MetadataValidationError(reading.problems)
@@ -696,7 +696,7 @@ def with_context(self, context: ZarrV3Context | None = None) -> ZarrV3ArrayMetad
in `context`. The existing reading is reused when `context` reads
every claim identically.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
if disagreements(context, claims_of(self._reading.fields())).agrees:
return self._of(self._document, context, self._reading, self._members)
return type(self)(self._document, context=context)
@@ -713,7 +713,7 @@ def refined_in(self, context: ZarrV3Context | None = None) -> ZarrV3ArrayMetadat
gain can expose a problem. `with_context` reads the document in
any scope.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
found = disagreements(context, claims_of(self._reading.fields()))
if len(found.conflicts) != 0:
raise ScopeConflictError(located_conflicts(self._reading.fields(), found.conflicts))
@@ -811,7 +811,7 @@ def from_key_value(
are not JSON, or the document is not valid. Raises `TypeError` for
a scope of another format before any bytes are decoded.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
return cls(load_store_json(mapping, ZARR_V3_ARRAY_METADATA_STORE_KEY), context=context)
@@ -888,7 +888,7 @@ def read_array_metadata_v3(
spec's fields), iterate over the reading's `fields()`. A value that
is not an object has no fields.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
reading, members = read_array_v3(value, context)
if members is None:
return reading
diff --git a/packages/zarr-metadata/src/zarr_metadata/model/_group_v2.py b/packages/zarr-metadata/src/zarr_metadata/model/_group_v2.py
index eb290a6cb7..1a0cdc5e48 100644
--- a/packages/zarr-metadata/src/zarr_metadata/model/_group_v2.py
+++ b/packages/zarr-metadata/src/zarr_metadata/model/_group_v2.py
@@ -63,7 +63,7 @@
from zarr_metadata.v2.array import ZARR_V2_ARRAY_METADATA_STORE_KEY
from zarr_metadata.v2.attributes import ZARR_V2_ATTRIBUTES_STORE_KEY
from zarr_metadata.v2.consolidated import ZARR_V2_CONSOLIDATED_METADATA_STORE_KEY
-from zarr_metadata.v2.definition import CORE_V2
+from zarr_metadata.v2.definition import CORE_V2_CONTEXT
from zarr_metadata.v2.group import ZARR_V2_GROUP_METADATA_STORE_KEY, ZarrV2GroupMetadataJSON
if TYPE_CHECKING:
@@ -136,8 +136,8 @@ def __reduce__(self) -> str | tuple[object, ...]:
def read_group_metadata_v2(
value: object, *, context: ZarrV2Context | None = None
) -> ZarrV2GroupMetadataReading:
- """Read `value`, a v2 group document in the merged form (`.zgroup` plus `attributes` from `.zattrs`), in `context` (`CORE_V2` when none is given), whatever it holds: every problem, and the model when there are none. A group has no fields for a scope to read; `context` is taken for consistency with every other v2 reader."""
- context = scoped(context, CORE_V2)
+ """Read `value`, a v2 group document in the merged form (`.zgroup` plus `attributes` from `.zattrs`), in `context` (`CORE_V2_CONTEXT` when none is given), whatever it holds: every problem, and the model when there are none. A group has no fields for a scope to read; `context` is taken for consistency with every other v2 reader."""
+ context = scoped(context, CORE_V2_CONTEXT)
problems = _group_problems_v2(value)
if len(problems) != 0:
return ZarrV2GroupMetadataReading(problems=problems)
@@ -173,7 +173,7 @@ def fields(self) -> Iterator[tuple[Loc, ResolvedField[Any]]]:
def read_node_metadata_v2(
files: Mapping[str, object], *, context: ZarrV2Context | None = None
) -> ZarrV2NodeMetadataReading:
- """Read the v2 node described by `files`, in `context` (`CORE_V2` when none is given), as the node type its files declare.
+ """Read the v2 node described by `files`, in `context` (`CORE_V2_CONTEXT` when none is given), as the node type its files declare.
`files` maps each of the node's files to its decoded JSON: `.zarray`
or `.zgroup`, and `.zattrs` when there is one. They can come from a
@@ -194,7 +194,7 @@ def read_node_metadata_v2(
v2 counterpart of `read_node_metadata_v3`, which reads one `zarr.json`
and its `node_type`.
"""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
is_array = ZARR_V2_ARRAY_METADATA_STORE_KEY in files
is_group = ZARR_V2_GROUP_METADATA_STORE_KEY in files
if is_array and is_group:
@@ -234,7 +234,7 @@ def node_metadata_from_key_value_v2(
mapping: Mapping[StoreKey, bytes], *, context: ZarrV2Context | None = None
) -> ZarrV2NodeMetadata:
"""Return the model of the v2 node whose files are in `mapping`, a store's keys and bytes: each file is decoded, then read as `read_node_metadata_v2` reads it. Raises `MetadataValidationError` with every problem, including a file whose bytes are not JSON, and `TypeError` for a scope of another format before any bytes are decoded."""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
files, problems = decoded_node_files_v2(mapping)
if len(problems) != 0:
raise MetadataValidationError(problems)
@@ -278,7 +278,7 @@ class ZarrV2GroupMetadata(Keyed):
node_type: Final[Literal["group"]] = "group" # noqa: PYI064 - as above
def __init__(self, document: object, context: ZarrV2Context | None = None) -> None:
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
problems = _group_problems_v2(document)
if len(problems) != 0:
raise MetadataValidationError(problems)
@@ -368,7 +368,7 @@ def update(self, **members: Unpack[ZarrV2GroupMetadataUpdate]) -> ZarrV2GroupMet
def with_context(self, context: ZarrV2Context | None = None) -> ZarrV2GroupMetadata:
"""Return this document read in `context`: the same group, holding that scope."""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
return self._of(self._document, context)
def refined_in(self, context: ZarrV2Context | None = None) -> ZarrV2GroupMetadata:
@@ -406,7 +406,7 @@ def from_key_value(
valid, with every problem the read finds. Raises `TypeError` for a
scope of another format before any bytes are decoded.
"""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
files, problems = decoded_node_files_v2(
mapping, (ZARR_V2_GROUP_METADATA_STORE_KEY, ZARR_V2_ATTRIBUTES_STORE_KEY)
)
@@ -449,8 +449,8 @@ def __reduce__(self) -> str | tuple[object, ...]:
def read_consolidated_metadata_v2(
value: object, *, context: ZarrV2Context | None = None
) -> ZarrV2ConsolidatedMetadataReading:
- """Read `value`, a v2 `.zmetadata` document, in `context` (`CORE_V2` when none is given), whatever it holds: every problem the read finds, and the model when there are none. This is the form of `ZarrV2ConsolidatedMetadata(value, context)` that returns problems instead of raising them."""
- context = scoped(context, CORE_V2)
+ """Read `value`, a v2 `.zmetadata` document, in `context` (`CORE_V2_CONTEXT` when none is given), whatever it holds: every problem the read finds, and the model when there are none. This is the form of `ZarrV2ConsolidatedMetadata(value, context)` that returns problems instead of raising them."""
+ context = scoped(context, CORE_V2_CONTEXT)
document, nodes, files, problems = _read_consolidated_v2(value, context)
if len(problems) != 0:
return ZarrV2ConsolidatedMetadataReading(problems=problems)
@@ -487,7 +487,7 @@ class ZarrV2ConsolidatedMetadata(Keyed):
zarr_consolidated_format: Final = 1
def __init__(self, document: object, context: ZarrV2Context | None = None) -> None:
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
read, nodes, files, problems = _read_consolidated_v2(document, context)
if len(problems) != 0:
raise MetadataValidationError(problems)
@@ -576,7 +576,7 @@ def __reduce__(self) -> tuple[type[ZarrV2ConsolidatedMetadata], tuple[object, Za
def with_context(self, context: ZarrV2Context | None = None) -> ZarrV2ConsolidatedMetadata:
"""Return this document with every node read in `context`, whatever that changes. Raises `MetadataValidationError` when a node has a problem in `context`."""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
return type(self)(self._document, context=context)
def refined_in(self, context: ZarrV2Context | None = None) -> ZarrV2ConsolidatedMetadata:
@@ -586,7 +586,7 @@ def refined_in(self, context: ZarrV2Context | None = None) -> ZarrV2Consolidated
node's entry. Raises `MetadataValidationError` when a gain exposes
a problem.
"""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
conflicts: list[Conflict] = []
for path, node in self._metadata.items():
if not isinstance(node, ZarrV2ArrayMetadata):
@@ -633,7 +633,7 @@ def from_key_value(
are not JSON, or the document is not valid. Raises `TypeError` for
a scope of another format before the bytes are decoded.
"""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
return cls(
load_store_json(mapping, ZARR_V2_CONSOLIDATED_METADATA_STORE_KEY), context=context
)
diff --git a/packages/zarr-metadata/src/zarr_metadata/model/_group_v3.py b/packages/zarr-metadata/src/zarr_metadata/model/_group_v3.py
index 155d8e8019..a189856678 100644
--- a/packages/zarr-metadata/src/zarr_metadata/model/_group_v3.py
+++ b/packages/zarr-metadata/src/zarr_metadata/model/_group_v3.py
@@ -80,7 +80,7 @@
# A runtime import: `get_type_hints` resolves the public signatures.
from zarr_metadata.v3._context import ZarrV3Context # noqa: TC001
-from zarr_metadata.v3._core import CORE_AND_EXTENSIONS
+from zarr_metadata.v3._core import CORE_AND_EXTENSIONS_CONTEXT
from zarr_metadata.v3._definition import GroupExtraFieldDefinition
from zarr_metadata.v3._hierarchy import hierarchy_problems, path_faults
from zarr_metadata.v3.consolidated import (
@@ -169,7 +169,7 @@ class ZarrV3GroupMetadata(Keyed):
the document as written, refined, and `context` is the scope.
`attributes` and `consolidated_metadata` are views of the values the
read refined. Each member the spec does not define is an extra field,
- in `extra_fields`, as the scope read it. `CORE` reads a
+ in `extra_fields`, as the scope read it. `CORE_CONTEXT` reads a
`consolidated_metadata` member as the envelope the core spec fixes,
and no definition reads the documents the member lists: they follow a
convention of the reference implementation, not the core spec. To
@@ -186,7 +186,7 @@ class ZarrV3GroupMetadata(Keyed):
node_type: Final[Literal["group"]] = "group" # noqa: PYI064 - as above
def __init__(self, document: object, context: ZarrV3Context | None = None) -> None:
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
reading, members = read_group_v3(document, context)
if members is None or len(reading.problems) != 0:
raise MetadataValidationError(reading.problems)
@@ -281,7 +281,7 @@ def attributes(self) -> Mapping[str, JSONValue]:
def extra_fields(
self,
) -> Mapping[str, AcceptedField[GroupExtraFieldDefinition[Any]] | UnclaimedField]:
- """Each member the spec does not define, by key, as the scope read it: an `AcceptedField` when a definition in scope claims the key (as `CORE` claims `consolidated_metadata`), and an `UnclaimedField` otherwise."""
+ """Each member the spec does not define, by key, as the scope read it: an `AcceptedField` when a definition in scope claims the key (as `CORE_CONTEXT` claims `consolidated_metadata`), and an `UnclaimedField` otherwise."""
return self._shown[3]
@property
@@ -365,7 +365,7 @@ def update(self, **members: Unpack[ZarrV3GroupMetadataUpdate]) -> ZarrV3GroupMet
def with_context(self, context: ZarrV3Context | None = None) -> ZarrV3GroupMetadata:
"""Return this document read in `context`, whatever that changes. Raises `MetadataValidationError` when the document has a problem in `context`. The existing reading is reused when `context` reads every claim identically."""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
if disagreements(context, claims_of(self._reading.fields())).agrees:
return self._of(self._document, context, self._reading, self._members)
return type(self)(self._document, context=context)
@@ -382,7 +382,7 @@ def refined_in(self, context: ZarrV3Context | None = None) -> ZarrV3GroupMetadat
in `context`, and `ZarrV3ConsolidatedMetadata.refined_in` checks
that they read the same there.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
found = disagreements(context, claims_of(self._reading.fields()))
if len(found.conflicts) != 0:
raise ScopeConflictError(located_conflicts(self._reading.fields(), found.conflicts))
@@ -409,7 +409,7 @@ def create_default(
**overrides: Unpack[ZarrV3GroupMetadataUpdate],
) -> ZarrV3GroupMetadata:
"""Return a group with no attributes, with any document members in `overrides` applied, read in `context`. `overrides` takes what `update` takes, including `UNSET` to drop a member and a `ZarrV3ConsolidatedMetadata` as `consolidated_metadata`. Raises `MetadataValidationError` when the resulting document has a problem."""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
changes = member_changes(
overrides, ZarrV3GroupMetadataUpdate, GroupExtraFieldDefinition.spec_members
)
@@ -426,7 +426,7 @@ def from_json(
Raises `MetadataValidationError` with every problem the read finds.
A `consolidated_metadata` of `null`, which zarr-python 3.0 and 3.1
wrote, is a value the document wrote that is not an object. In a
- scope that claims the member, as `CORE` does, this is a problem,
+ scope that claims the member, as `CORE_CONTEXT` does, this is a problem,
because the spec requires an object. Use
`read_repaired_node_metadata_v3` to read such a store. A member the
spec does not define is kept in `extra_fields`.
@@ -443,7 +443,7 @@ def from_key_value(
are not JSON, or the document is not valid. Raises `TypeError` for
a scope of another format before any bytes are decoded.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
return cls(load_store_json(mapping, ZARR_V3_GROUP_METADATA_STORE_KEY), context=context)
@@ -495,7 +495,7 @@ class ZarrV3ConsolidatedMetadata(Keyed):
must_understand: Final = False
def __init__(self, member: object, context: ZarrV3Context | None = None) -> None:
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
# The member is always under a group's key, even when read on its
# own, so nesting depth is counted from there, as in the group.
readings, members, document, problems = _read_consolidated_v3(
@@ -561,7 +561,7 @@ def __reduce__(self) -> tuple[type[ZarrV3ConsolidatedMetadata], tuple[object, Za
def with_context(self, context: ZarrV3Context | None = None) -> ZarrV3ConsolidatedMetadata:
"""Return this member with every document read in `context`, whatever that changes. Raises `MetadataValidationError` when a document has a problem in `context`. The existing readings are reused when `context` reads every claim of every document identically."""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
if all(
disagreements(context, claims_of(node.reading.fields())).agrees
for node in self._metadata.values()
@@ -577,7 +577,7 @@ def refined_in(self, context: ZarrV3Context | None = None) -> ZarrV3Consolidated
document's path. Raises `MetadataValidationError` when a gain
exposes a problem.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
conflicts = [
dataclasses.replace(
conflict, loc=("metadata", path, *(() if conflict.loc is None else conflict.loc))
@@ -681,8 +681,8 @@ def __reduce__(self) -> str | tuple[object, ...]:
def read_consolidated_metadata_v3(
value: object, *, context: ZarrV3Context | None = None
) -> ZarrV3ConsolidatedMetadataReading:
- """Read `value`, a group's `consolidated_metadata` member, in `context` (`CORE_AND_EXTENSIONS` when none is given), whatever it holds: every problem the read finds, and the model when there are none. This is the form of `ZarrV3ConsolidatedMetadata(value, context)` that returns problems instead of raising them."""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ """Read `value`, a group's `consolidated_metadata` member, in `context` (`CORE_AND_EXTENSIONS_CONTEXT` when none is given), whatever it holds: every problem the read finds, and the model when there are none. This is the form of `ZarrV3ConsolidatedMetadata(value, context)` that returns problems instead of raising them."""
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
readings, members, document, problems = _read_consolidated_v3(
value, context, (ZARR_V3_CONSOLIDATED_METADATA_KEY,)
)
@@ -737,7 +737,7 @@ def read_node_metadata_v3(
read `node_type` from unvalidated JSON, and a document in another
format is reported as not v3.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
node_type, problems = _node_type(value)
if node_type == "array":
return read_array_metadata_v3(value, context=context)
@@ -761,7 +761,7 @@ def node_metadata_from_json_v3(
`MetadataValidationError` with every problem `read_node_metadata_v3`
finds, including a `node_type` that is neither array nor group.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
reading = read_node_metadata_v3(data, context=context)
if reading.metadata is None:
raise MetadataValidationError(reading.problems)
@@ -776,7 +776,7 @@ def node_metadata_from_key_value_v3(
Raises `MetadataValidationError` when the key is missing, its bytes
are not JSON, or the document is not a valid array or group.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
# Array documents and group documents are both stored at `zarr.json`.
document = load_store_json(mapping, ZARR_V3_GROUP_METADATA_STORE_KEY)
return node_metadata_from_json_v3(document, context=context)
@@ -853,13 +853,13 @@ def read_group_metadata_v3(
reader must understand each one that does not declare
`must_understand: false`; the model reports these as
`must_understand_fields`. A `consolidated_metadata` member, if
- present and claimed by the scope, as `CORE` claims it, is checked
+ present and claimed by the scope, as `CORE_CONTEXT` claims it, is checked
against the envelope the core spec fixes. The documents it lists are
not read; `read_consolidated_metadata_v3` reads them. This is the
form of `ZarrV3GroupMetadata(value, context)` that returns problems
instead of raising them.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
reading, members = read_group_v3(value, context)
if members is None or members.document is None:
return reading
diff --git a/packages/zarr-metadata/src/zarr_metadata/model/_repair.py b/packages/zarr-metadata/src/zarr_metadata/model/_repair.py
index d16663b1fe..679a584fc8 100644
--- a/packages/zarr-metadata/src/zarr_metadata/model/_repair.py
+++ b/packages/zarr-metadata/src/zarr_metadata/model/_repair.py
@@ -45,9 +45,9 @@
)
from zarr_metadata.model._group_v3 import ZarrV3NodeMetadataReading, read_node_metadata_v3
from zarr_metadata.v2.array import ZARR_V2_ARRAY_METADATA_STORE_KEY
-from zarr_metadata.v2.definition import CORE_V2
+from zarr_metadata.v2.definition import CORE_V2_CONTEXT
from zarr_metadata.v2.group import ZARR_V2_GROUP_METADATA_STORE_KEY
-from zarr_metadata.v3._core import CORE_AND_EXTENSIONS
+from zarr_metadata.v3._core import CORE_AND_EXTENSIONS_CONTEXT
from zarr_metadata.v3.consolidated import ZARR_V3_CONSOLIDATED_METADATA_KEY
if TYPE_CHECKING:
@@ -278,7 +278,7 @@ def read_repaired_node_metadata_v3(
Content that no repair applies to is read as it is, and reported the
same way `read_node_metadata_v3` reports it.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
repaired, repairs = repair_node_metadata_v3(value)
return ZarrV3RepairedNodeMetadataReading(
read_node_metadata_v3(repaired, context=context), repairs
@@ -482,7 +482,7 @@ def read_repaired_node_metadata_v2(
Content that no repair applies to is read as it is, and reported the
same way `read_node_metadata_v2` reports it.
"""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
repaired, repairs = repair_node_metadata_v2(files)
return ZarrV2RepairedNodeMetadataReading(
read_node_metadata_v2(repaired, context=context), repairs
@@ -510,7 +510,7 @@ def read_repaired_consolidated_metadata_v2(
read as it is, and reported the same way
`read_consolidated_metadata_v2` reports it.
"""
- context = scoped(context, CORE_V2)
+ context = scoped(context, CORE_V2_CONTEXT)
repaired, repairs = repair_consolidated_metadata_v2(value)
return ZarrV2RepairedConsolidatedMetadataReading(
read_consolidated_metadata_v2(repaired, context=context), repairs
diff --git a/packages/zarr-metadata/src/zarr_metadata/pydantic.py b/packages/zarr-metadata/src/zarr_metadata/pydantic.py
index f369f2beec..84ded7390e 100644
--- a/packages/zarr-metadata/src/zarr_metadata/pydantic.py
+++ b/packages/zarr-metadata/src/zarr_metadata/pydantic.py
@@ -20,7 +20,7 @@
`"zarr_metadata_context_v2"` item. This lets a model with fields of
both formats give a scope for each format.
- If no scope is given for a format, its field types read in the default
- scope for that format, `CORE_AND_EXTENSIONS` or `CORE_V2`.
+ scope for that format, `CORE_AND_EXTENSIONS_CONTEXT` or `CORE_V2_CONTEXT`.
A mapping key that starts like these keys but is not one of them raises
`TypeError`, so a misspelled key is not read as no scope. To fix a field's
@@ -128,8 +128,8 @@ class ArrayManifest(BaseModel):
from zarr_metadata.definition._context import scoped
from zarr_metadata.definition._definition import Context
from zarr_metadata.definition._scope import ScopeConflictError
-from zarr_metadata.v2.definition import CORE_V2, ZarrV2Context
-from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS, ZarrV3Context
+from zarr_metadata.v2.definition import CORE_V2_CONTEXT, ZarrV2Context
+from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS_CONTEXT, ZarrV3Context
if TYPE_CHECKING:
from collections.abc import Callable, Mapping
@@ -321,7 +321,7 @@ def _context_of(validation_context: object, default: _C, key: str) -> _C | None:
_model.ZarrV3ArrayMetadata,
_model.ZarrV3ArrayMetadata.from_json,
_model.ZarrV3ArrayMetadata.refined_in,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
CONTEXT_KEY,
),
json_schema_input_type=_ZarrV3ArrayMetadataSchema,
@@ -337,7 +337,7 @@ def _context_of(validation_context: object, default: _C, key: str) -> _C | None:
_model.ZarrV2ArrayMetadata,
_model.ZarrV2ArrayMetadata.from_json,
_model.ZarrV2ArrayMetadata.refined_in,
- CORE_V2,
+ CORE_V2_CONTEXT,
CONTEXT_KEY_V2,
),
json_schema_input_type=_ZarrV2ArrayMetadataSchema,
@@ -353,7 +353,7 @@ def _context_of(validation_context: object, default: _C, key: str) -> _C | None:
_model.ZarrV3GroupMetadata,
_model.ZarrV3GroupMetadata.from_json,
_model.ZarrV3GroupMetadata.refined_in,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
CONTEXT_KEY,
),
json_schema_input_type=_ZarrV3GroupMetadataSchema,
@@ -369,7 +369,7 @@ def _context_of(validation_context: object, default: _C, key: str) -> _C | None:
_model.ZarrV2GroupMetadata,
_model.ZarrV2GroupMetadata.from_json,
_model.ZarrV2GroupMetadata.refined_in,
- CORE_V2,
+ CORE_V2_CONTEXT,
CONTEXT_KEY_V2,
),
json_schema_input_type=_ZarrV2GroupMetadataSchema,
@@ -385,7 +385,7 @@ def _context_of(validation_context: object, default: _C, key: str) -> _C | None:
_model.ZarrV3ConsolidatedMetadata,
_model.ZarrV3ConsolidatedMetadata.from_json,
_model.ZarrV3ConsolidatedMetadata.refined_in,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
CONTEXT_KEY,
),
json_schema_input_type=_ZarrV3ConsolidatedMetadataSchema,
@@ -404,7 +404,7 @@ def _context_of(validation_context: object, default: _C, key: str) -> _C | None:
_model.ZarrV2ConsolidatedMetadata,
_model.ZarrV2ConsolidatedMetadata.from_json,
_model.ZarrV2ConsolidatedMetadata.refined_in,
- CORE_V2,
+ CORE_V2_CONTEXT,
CONTEXT_KEY_V2,
),
json_schema_input_type=_ZarrV2ConsolidatedMetadataSchema,
@@ -419,15 +419,15 @@ def _context_of(validation_context: object, default: _C, key: str) -> _C | None:
_FIELD_PARTS: Final[Mapping[type[Any], tuple[ZarrV3Context | ZarrV2Context, object]]] = (
MappingProxyType(
{
- _model.ZarrV3ArrayMetadata: (CORE_AND_EXTENSIONS, _ZarrV3ArrayMetadataSchema),
- _model.ZarrV2ArrayMetadata: (CORE_V2, _ZarrV2ArrayMetadataSchema),
- _model.ZarrV3GroupMetadata: (CORE_AND_EXTENSIONS, _ZarrV3GroupMetadataSchema),
- _model.ZarrV2GroupMetadata: (CORE_V2, _ZarrV2GroupMetadataSchema),
+ _model.ZarrV3ArrayMetadata: (CORE_AND_EXTENSIONS_CONTEXT, _ZarrV3ArrayMetadataSchema),
+ _model.ZarrV2ArrayMetadata: (CORE_V2_CONTEXT, _ZarrV2ArrayMetadataSchema),
+ _model.ZarrV3GroupMetadata: (CORE_AND_EXTENSIONS_CONTEXT, _ZarrV3GroupMetadataSchema),
+ _model.ZarrV2GroupMetadata: (CORE_V2_CONTEXT, _ZarrV2GroupMetadataSchema),
_model.ZarrV3ConsolidatedMetadata: (
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
_ZarrV3ConsolidatedMetadataSchema,
),
- _model.ZarrV2ConsolidatedMetadata: (CORE_V2, _ZarrV2ConsolidatedMetadataSchema),
+ _model.ZarrV2ConsolidatedMetadata: (CORE_V2_CONTEXT, _ZarrV2ConsolidatedMetadataSchema),
}
)
)
diff --git a/packages/zarr-metadata/src/zarr_metadata/testing/__init__.py b/packages/zarr-metadata/src/zarr_metadata/testing/__init__.py
new file mode 100644
index 0000000000..271a4dfe51
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/testing/__init__.py
@@ -0,0 +1,6 @@
+"""Testing utilities for code that reads or writes Zarr metadata.
+
+`zarr_metadata.testing.strategies` holds Hypothesis strategies that draw
+valid metadata. It needs the `testing` extra:
+`pip install zarr-metadata[testing]`.
+"""
diff --git a/packages/zarr-metadata/src/zarr_metadata/testing/_chunk_grid.py b/packages/zarr-metadata/src/zarr_metadata/testing/_chunk_grid.py
new file mode 100644
index 0000000000..b7528a3907
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/testing/_chunk_grid.py
@@ -0,0 +1,105 @@
+"""Strategies for each chunk grid this package defines.
+
+Over an array of a known shape, a grid is drawn to fit it: a chunk
+length for each dimension, never zero ("Chunk sizes must be greater
+than zero", https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v3/chunk-grids/regular-grid/index.rst#L40),
+and rectilinear edges that cover each dimension. Over no shape, what is
+drawn is what the grid's rules accept.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Final
+
+from hypothesis import strategies as st
+
+from zarr_metadata.testing._strategy import MAX_RANK, ChunkGridStrategy
+from zarr_metadata.v3.chunk_grid.rectilinear import RECTILINEAR_CHUNK_GRID
+from zarr_metadata.v3.chunk_grid.regular import REGULAR_CHUNK_GRID
+
+if TYPE_CHECKING:
+ from collections.abc import Mapping
+
+ from zarr_metadata._common import JSONValue
+ from zarr_metadata.v3._definition import ChunkGridDefinition
+
+_MAX_LENGTH: Final = 16
+"""The longest chunk drawn along a dimension whose length is unknown."""
+
+_MAX_RUNS: Final = 5
+"""The most runs of equal lengths a rectilinear dimension is drawn with."""
+
+_MAX_COUNT: Final = 4
+"""The most chunks a run is drawn with."""
+
+
+def _length(extent: int | None) -> st.SearchStrategy[int]:
+ """A chunk length along a dimension of `extent`: at least 1, and up to twice the extent when it is known and not 0."""
+ if extent is None or extent == 0:
+ return st.integers(1, _MAX_LENGTH)
+ return st.integers(1, 2 * extent)
+
+
+def _regular(shape: tuple[int, ...] | None) -> st.SearchStrategy[Mapping[str, JSONValue]]:
+ """A chunk length for each dimension."""
+ if shape is None:
+ lengths = st.lists(_length(None), max_size=MAX_RANK).map(tuple)
+ else:
+ lengths = st.tuples(*(_length(extent) for extent in shape))
+ return lengths.map(lambda chunk_shape: {"chunk_shape": chunk_shape})
+
+
+def _spelled_run(draw: st.DrawFn, size: int, count: int) -> list[JSONValue]:
+ """`count` chunks of `size`: one `[size, count]` pair, or the size written once for each."""
+ if draw(st.booleans()):
+ return [(size, count)]
+ return [size] * count
+
+
+@st.composite
+def _edges(draw: st.DrawFn, extent: int | None) -> JSONValue:
+ """Edge lengths along a dimension, covering `extent`, in runs spelled as pairs or written out.
+
+ Runs are drawn apart, so two of one size can sit side by side and a
+ pair can count one chunk: spellings the canonical form merges. A
+ dimension of length zero, or of a length not known, may be covered by
+ no edges at all.
+ """
+ min_runs = 0 if extent is None or extent == 0 else 1
+ runs = draw(
+ st.lists(
+ st.tuples(st.integers(1, _MAX_LENGTH), st.integers(1, _MAX_COUNT)),
+ min_size=min_runs,
+ max_size=_MAX_RUNS,
+ )
+ )
+ covered = sum(size * count for size, count in runs)
+ if extent is not None and covered < extent:
+ runs.append((extent - covered, 1))
+ spelled: list[JSONValue] = []
+ for size, count in runs:
+ spelled.extend(_spelled_run(draw, size, count))
+ return tuple(spelled)
+
+
+def _rectilinear_dimension(extent: int | None) -> st.SearchStrategy[JSONValue]:
+ """A bare length, which repeats until it covers the dimension, or edges that cover it."""
+ return _length(extent) | _edges(extent)
+
+
+def _rectilinear(shape: tuple[int, ...] | None) -> st.SearchStrategy[Mapping[str, JSONValue]]:
+ """Inline chunk shapes, one entry for each dimension."""
+ if shape is None:
+ dimensions = st.lists(_rectilinear_dimension(None), max_size=MAX_RANK).map(tuple)
+ else:
+ dimensions = st.tuples(*(_rectilinear_dimension(extent) for extent in shape))
+ return dimensions.map(lambda chunk_shapes: {"kind": "inline", "chunk_shapes": chunk_shapes})
+
+
+CHUNK_GRID_STRATEGIES: Final[Mapping[ChunkGridDefinition[Any], ChunkGridStrategy]] = {
+ REGULAR_CHUNK_GRID: ChunkGridStrategy(configurations=_regular),
+ RECTILINEAR_CHUNK_GRID: ChunkGridStrategy(configurations=_rectilinear),
+}
+"""A strategy for each chunk grid."""
+
+__all__ = ["CHUNK_GRID_STRATEGIES"]
diff --git a/packages/zarr-metadata/src/zarr_metadata/testing/_chunk_key_encoding.py b/packages/zarr-metadata/src/zarr_metadata/testing/_chunk_key_encoding.py
new file mode 100644
index 0000000000..9c525118c9
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/testing/_chunk_key_encoding.py
@@ -0,0 +1,40 @@
+"""Strategies for each chunk key encoding this package defines."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Final
+
+from hypothesis import strategies as st
+
+from zarr_metadata.testing._strategy import ChunkKeyEncodingStrategy
+from zarr_metadata.v3.chunk_key_encoding.default import (
+ DEFAULT_CHUNK_KEY_ENCODING,
+ DEFAULT_CHUNK_KEY_ENCODING_SEPARATOR,
+)
+from zarr_metadata.v3.chunk_key_encoding.v2 import (
+ V2_CHUNK_KEY_ENCODING,
+ V2_CHUNK_KEY_ENCODING_SEPARATOR,
+)
+
+if TYPE_CHECKING:
+ from collections.abc import Mapping
+
+ from zarr_metadata.v3._definition import ChunkKeyEncodingDefinition
+
+CHUNK_KEY_ENCODING_STRATEGIES: Final[
+ Mapping[ChunkKeyEncodingDefinition[Any], ChunkKeyEncodingStrategy]
+] = {
+ DEFAULT_CHUNK_KEY_ENCODING: ChunkKeyEncodingStrategy(
+ configurations=st.fixed_dictionaries(
+ {}, optional={"separator": st.sampled_from(DEFAULT_CHUNK_KEY_ENCODING_SEPARATOR)}
+ )
+ ),
+ V2_CHUNK_KEY_ENCODING: ChunkKeyEncodingStrategy(
+ configurations=st.fixed_dictionaries(
+ {}, optional={"separator": st.sampled_from(V2_CHUNK_KEY_ENCODING_SEPARATOR)}
+ )
+ ),
+}
+"""A strategy for each chunk key encoding: a `separator`, or none, which means the encoding's default."""
+
+__all__ = ["CHUNK_KEY_ENCODING_STRATEGIES"]
diff --git a/packages/zarr-metadata/src/zarr_metadata/testing/_codec.py b/packages/zarr-metadata/src/zarr_metadata/testing/_codec.py
new file mode 100644
index 0000000000..e966cd9087
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/testing/_codec.py
@@ -0,0 +1,194 @@
+"""Strategies for each codec this package defines that holds no other field.
+
+A codec handed an array draws what fits the chunk: a `bytes` codec an
+`endian` for a type whose values take several bytes, a `transpose` an
+`order` with an entry for each axis. Handed `Chunk()`, nothing is known
+of the chunk, and what is drawn is what the codec's rules accept.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Final
+
+from hypothesis import strategies as st
+
+from zarr_metadata.definition._definition import AcceptedField, EmptyConfiguration
+from zarr_metadata.testing._data_type import (
+ COMPLEX_WIDTHS,
+ FLOAT_WIDTHS,
+ INTEGER_RANGES,
+ FloatWidth,
+ finite_floats,
+ float_spellings,
+)
+from zarr_metadata.testing._strategy import EMPTY_CONFIGURATIONS, MAX_RANK, CodecStrategy
+from zarr_metadata.v3._definition import Chunk, storage_of
+from zarr_metadata.v3.codec._arithmetic import NOT_NUMBERS
+from zarr_metadata.v3.codec.blosc import BLOSC_CNAME, BLOSC_CODEC, BLOSC_NO_SHUFFLE, BLOSC_SHUFFLE
+from zarr_metadata.v3.codec.bytes import BYTES_CODEC, ENDIANNESS
+from zarr_metadata.v3.codec.crc32c import CRC32C_CODEC
+from zarr_metadata.v3.codec.gzip import GZIP_CODEC
+from zarr_metadata.v3.codec.scale_offset import SCALE_OFFSET_CODEC
+from zarr_metadata.v3.codec.transpose import TRANSPOSE_CODEC
+from zarr_metadata.v3.codec.vlen_bytes import VLEN_BYTES_CODEC
+from zarr_metadata.v3.codec.vlen_utf8 import VLEN_UTF8_CODEC
+from zarr_metadata.v3.codec.zstd import ZSTD_CODEC, ZSTD_MAX_LEVEL, ZSTD_MIN_LEVEL
+from zarr_metadata.v3.data_type.numpy_datetime64 import NUMPY_DATETIME64_DATA_TYPE
+from zarr_metadata.v3.data_type.numpy_timedelta64 import NUMPY_TIMEDELTA64_DATA_TYPE
+
+if TYPE_CHECKING:
+ from collections.abc import Mapping
+
+ from zarr_metadata._common import JSONValue
+ from zarr_metadata.v3._definition import CodecDefinition
+
+_BLOSC_MAX_TYPESIZE: Final = 255
+"""The widest element c-blosc takes: `BLOSC_MAX_TYPESIZE`."""
+
+_BLOSC_MAX_BLOCKSIZE: Final = 2**31 - 1
+"""The largest block c-blosc takes, a signed 32-bit size; 0 lets it choose."""
+
+_ENDIAN: Final = st.sampled_from(ENDIANNESS)
+
+_NUMPY_TIMES: Final = frozenset({NUMPY_DATETIME64_DATA_TYPE, NUMPY_TIMEDELTA64_DATA_TYPE})
+"""The numpy time types, each stored as a signed 64-bit integer."""
+
+_NUMPY_TIME_RANGE: Final = (-(2**63) + 1, 2**63 - 1)
+"""The signed 64-bit integers but the least, which numpy reads as `NaT`: the finite numpy times."""
+
+
+def _bytes(chunk: Chunk) -> st.SearchStrategy[Mapping[str, JSONValue]] | None:
+ """An `endian`, required for a data type whose values take several bytes; None for one whose values vary in size."""
+ storage = None if chunk.data_type is None else storage_of(chunk.data_type)
+ if storage == "variable_length":
+ return None
+ if storage == "multi_byte":
+ return st.fixed_dictionaries({"endian": _ENDIAN})
+ return st.fixed_dictionaries({}, optional={"endian": _ENDIAN})
+
+
+@st.composite
+def _blosc(draw: st.DrawFn) -> Mapping[str, JSONValue]:
+ """A compressor, a level, a shuffle and a block size; and a `typesize`, which only `noshuffle` may leave out.
+
+ Under `noshuffle` the spec says `typesize` "is ignored", so it is
+ drawn there too, a spelling that is not the simplest.
+ """
+ shuffle = draw(st.sampled_from(BLOSC_SHUFFLE))
+ typesize = st.integers(1, _BLOSC_MAX_TYPESIZE)
+ required: dict[str, st.SearchStrategy[JSONValue]] = {
+ "cname": st.sampled_from(BLOSC_CNAME),
+ "clevel": st.integers(0, 9),
+ "shuffle": st.just(shuffle),
+ "blocksize": st.integers(0, _BLOSC_MAX_BLOCKSIZE),
+ }
+ if shuffle == BLOSC_NO_SHUFFLE:
+ return draw(st.fixed_dictionaries(required, optional={"typesize": typesize}))
+ return draw(st.fixed_dictionaries({**required, "typesize": typesize}))
+
+
+def _order(rank: int) -> st.SearchStrategy[Mapping[str, JSONValue]]:
+ return st.permutations(range(rank)).map(lambda order: {"order": tuple(order)})
+
+
+def _transpose(chunk: Chunk) -> st.SearchStrategy[Mapping[str, JSONValue]]:
+ """A permutation of the chunk's axes; of some number of axes, when the chunk does not say."""
+ if chunk.rank is not None:
+ return _order(chunk.rank)
+ return st.integers(0, MAX_RANK).flatmap(_order)
+
+
+def _finite_float_values(width: FloatWidth) -> st.SearchStrategy[JSONValue]:
+ """A finite float of `width` bits, as a number or the hex string of its bits."""
+ return finite_floats(width).flatmap(lambda value: float_spellings(value, width))
+
+
+def _non_zero_float_values(width: FloatWidth) -> st.SearchStrategy[JSONValue]:
+ """A finite, non-zero float of `width` bits, as a number or the hex string of its bits."""
+ return (
+ finite_floats(width)
+ .filter(lambda value: value != 0)
+ .flatmap(lambda value: float_spellings(value, width))
+ )
+
+
+def _scale_offset(chunk: Chunk) -> st.SearchStrategy[Mapping[str, JSONValue]] | None:
+ """An `offset` and a `scale`, each a finite value of the data type the codec is handed, the scale not zero; None for a data type whose values are not numbers.
+
+ Each is a fill value of the chunk's data type
+ (https://github.com/zarr-developers/zarr-extensions/blob/4da7b37a84f76e660902f6d3de3eaef0e0febae6/codecs/scale_offset/README.md?plain=1#L39-L43),
+ a float's as a number or the hex string of its bits, a complex
+ number's as a pair of them. Each is drawn finite, and the scale
+ non-zero, so that decoding never divides by zero; a scale small
+ enough that decoding overflows is still drawn. A numpy time is drawn
+ as the signed 64-bit integer it is stored as, never `NaT`, since it
+ "is compatible with any codec that supports arrays of signed 64-bit
+ integers"
+ (https://github.com/zarr-developers/zarr-extensions/blob/4da7b37a84f76e660902f6d3de3eaef0e0febae6/data-types/numpy.datetime64/README.md?plain=1#L120).
+ A data type this package does not define, whose values it cannot
+ draw, takes only the configuration with neither, the identity.
+ Handed a chunk whose data type is unknown, the values are small
+ non-negative integers, which every integer and float type holds.
+ """
+ data_type = chunk.data_type
+ definition = data_type.definition if isinstance(data_type, AcceptedField) else None
+ if definition is None:
+ offset: st.SearchStrategy[JSONValue] = st.integers(0, 127)
+ scale: st.SearchStrategy[JSONValue] = st.integers(1, 127)
+ elif definition.name in NOT_NUMBERS:
+ return None
+ elif definition in INTEGER_RANGES or definition in _NUMPY_TIMES:
+ low, high = INTEGER_RANGES.get(definition, _NUMPY_TIME_RANGE)
+ offset = st.integers(low, high)
+ scale = st.integers(low, high).filter(lambda value: value != 0)
+ elif definition in FLOAT_WIDTHS:
+ offset = _finite_float_values(FLOAT_WIDTHS[definition])
+ scale = _non_zero_float_values(FLOAT_WIDTHS[definition])
+ elif definition in COMPLEX_WIDTHS:
+ width = COMPLEX_WIDTHS[definition]
+ offset = st.tuples(_finite_float_values(width), _finite_float_values(width))
+ # Non-zero in either part is a non-zero complex number.
+ scale = st.one_of(
+ st.tuples(_non_zero_float_values(width), _finite_float_values(width)),
+ st.tuples(_finite_float_values(width), _non_zero_float_values(width)),
+ )
+ else:
+ return EMPTY_CONFIGURATIONS
+ return st.fixed_dictionaries({}, optional={"offset": offset, "scale": scale})
+
+
+def _vlen(definition: CodecDefinition[Any]) -> CodecStrategy:
+ """A codec with nothing to configure that takes only some data types: none for a chunk of another, which its own chunk rules say."""
+ return CodecStrategy(
+ configurations=lambda chunk: (
+ None
+ if any(definition.chunk_rules(EmptyConfiguration(), {}, chunk))
+ else EMPTY_CONFIGURATIONS
+ )
+ )
+
+
+def _unchunked(configurations: st.SearchStrategy[Mapping[str, JSONValue]]) -> CodecStrategy:
+ """A codec whose configurations do not depend on the chunk it is handed."""
+ return CodecStrategy(configurations=lambda _chunk: configurations)
+
+
+CODEC_STRATEGIES: Final[Mapping[CodecDefinition[Any], CodecStrategy]] = {
+ BLOSC_CODEC: _unchunked(_blosc()),
+ BYTES_CODEC: CodecStrategy(configurations=_bytes),
+ CRC32C_CODEC: _unchunked(EMPTY_CONFIGURATIONS),
+ GZIP_CODEC: _unchunked(st.fixed_dictionaries({"level": st.integers(0, 9)})),
+ SCALE_OFFSET_CODEC: CodecStrategy(configurations=_scale_offset),
+ TRANSPOSE_CODEC: CodecStrategy(configurations=_transpose),
+ VLEN_BYTES_CODEC: _vlen(VLEN_BYTES_CODEC),
+ VLEN_UTF8_CODEC: _vlen(VLEN_UTF8_CODEC),
+ ZSTD_CODEC: _unchunked(
+ st.fixed_dictionaries(
+ {"level": st.integers(ZSTD_MIN_LEVEL, ZSTD_MAX_LEVEL)},
+ optional={"checksum": st.booleans()},
+ )
+ ),
+}
+"""A strategy for each codec that holds no other field."""
+
+__all__ = ["CODEC_STRATEGIES"]
diff --git a/packages/zarr-metadata/src/zarr_metadata/testing/_data_type.py b/packages/zarr-metadata/src/zarr_metadata/testing/_data_type.py
new file mode 100644
index 0000000000..b592b2d979
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/testing/_data_type.py
@@ -0,0 +1,256 @@
+"""Strategies for each data type this package defines that holds no other field.
+
+A fill value is drawn in every spelling the data type's spec gives it:
+a float's as a number, a named non-finite value or a hex string of its
+bits, a `bytes` value's as an array of byte values or base64.
+"""
+
+from __future__ import annotations
+
+import base64
+import struct
+from typing import TYPE_CHECKING, Any, Final, Literal, cast, get_args
+
+from hypothesis import strategies as st
+
+from zarr_metadata._numpy import NUMPY_TIME_MAX_SCALE_FACTOR
+from zarr_metadata.testing._strategy import EMPTY_CONFIGURATIONS, DataTypeStrategy
+from zarr_metadata.v3.data_type._float import FloatSpecialFillValue
+from zarr_metadata.v3.data_type._numpy_time import NUMPY_TIME_UNIT
+from zarr_metadata.v3.data_type.bool import BOOL_DATA_TYPE
+from zarr_metadata.v3.data_type.bytes import BYTES_DATA_TYPE
+from zarr_metadata.v3.data_type.complex64 import COMPLEX64_DATA_TYPE
+from zarr_metadata.v3.data_type.complex128 import COMPLEX128_DATA_TYPE
+from zarr_metadata.v3.data_type.fixed_length_utf32 import (
+ FIXED_LENGTH_UTF32_DATA_TYPE,
+ FixedLengthUtf32Configuration,
+)
+from zarr_metadata.v3.data_type.float16 import FLOAT16_DATA_TYPE
+from zarr_metadata.v3.data_type.float32 import FLOAT32_DATA_TYPE
+from zarr_metadata.v3.data_type.float64 import FLOAT64_DATA_TYPE
+from zarr_metadata.v3.data_type.int8 import INT8_DATA_TYPE
+from zarr_metadata.v3.data_type.int16 import INT16_DATA_TYPE
+from zarr_metadata.v3.data_type.int32 import INT32_DATA_TYPE
+from zarr_metadata.v3.data_type.int64 import INT64_DATA_TYPE
+from zarr_metadata.v3.data_type.null_terminated_bytes import (
+ NULL_TERMINATED_BYTES_DATA_TYPE,
+ NullTerminatedBytesConfiguration,
+)
+from zarr_metadata.v3.data_type.numpy_datetime64 import NUMPY_DATETIME64_DATA_TYPE
+from zarr_metadata.v3.data_type.numpy_timedelta64 import NUMPY_TIMEDELTA64_DATA_TYPE
+from zarr_metadata.v3.data_type.raw import RAW_BYTES_DATA_TYPE, RawBytesConfiguration
+from zarr_metadata.v3.data_type.string import STRING_DATA_TYPE
+from zarr_metadata.v3.data_type.uint8 import UINT8_DATA_TYPE
+from zarr_metadata.v3.data_type.uint16 import UINT16_DATA_TYPE
+from zarr_metadata.v3.data_type.uint32 import UINT32_DATA_TYPE
+from zarr_metadata.v3.data_type.uint64 import UINT64_DATA_TYPE
+from zarr_metadata.v3.data_type.variable_length_bytes import VARIABLE_LENGTH_BYTES_DATA_TYPE
+from zarr_metadata.v3.data_type.zarr_python_raw_bytes import (
+ ZARR_PYTHON_RAW_BYTES_DATA_TYPE,
+ ZarrPythonRawBytesConfiguration,
+)
+
+if TYPE_CHECKING:
+ from collections.abc import Mapping
+
+ from zarr_metadata._common import JSONValue
+ from zarr_metadata.v3._definition import DataTypeDefinition
+
+FloatWidth = Literal[16, 32, 64]
+
+INTEGER_RANGES: Final[Mapping[DataTypeDefinition[Any], tuple[int, int]]] = {
+ INT8_DATA_TYPE: (-(2**7), 2**7 - 1),
+ INT16_DATA_TYPE: (-(2**15), 2**15 - 1),
+ INT32_DATA_TYPE: (-(2**31), 2**31 - 1),
+ INT64_DATA_TYPE: (-(2**63), 2**63 - 1),
+ UINT8_DATA_TYPE: (0, 2**8 - 1),
+ UINT16_DATA_TYPE: (0, 2**16 - 1),
+ UINT32_DATA_TYPE: (0, 2**32 - 1),
+ UINT64_DATA_TYPE: (0, 2**64 - 1),
+}
+"""The values each integer data type holds."""
+
+FLOAT_WIDTHS: Final[Mapping[DataTypeDefinition[Any], FloatWidth]] = {
+ FLOAT16_DATA_TYPE: 16,
+ FLOAT32_DATA_TYPE: 32,
+ FLOAT64_DATA_TYPE: 64,
+}
+"""The width in bits of each floating-point data type."""
+
+COMPLEX_WIDTHS: Final[Mapping[DataTypeDefinition[Any], FloatWidth]] = {
+ COMPLEX64_DATA_TYPE: 32,
+ COMPLEX128_DATA_TYPE: 64,
+}
+"""The width in bits of each part of each complex data type."""
+
+_MAX_RAW_BYTES: Final = 32
+"""The widest raw bits drawn, in bytes: `r256`."""
+
+_MAX_BYTES: Final = 16
+"""The longest `bytes` fill value drawn."""
+
+
+def finite_floats(width: FloatWidth) -> st.SearchStrategy[float]:
+ """Finite numbers a float of `width` bits holds exactly."""
+ return st.floats(width=width, allow_nan=False, allow_infinity=False)
+
+
+_PACKED: Final[Mapping[FloatWidth, str]] = {16: ">e", 32: ">f", 64: ">d"}
+"""How `struct` packs a float of each width, most significant byte first."""
+
+
+def _hex_spellings(digits: str) -> st.SearchStrategy[str]:
+ """`0x` and the digits, the letters in either case."""
+ return st.sampled_from((f"0x{digits}", f"0x{digits.upper()}"))
+
+
+def float_spellings(value: float, width: FloatWidth) -> st.SearchStrategy[JSONValue]:
+ """`value`, a float of `width` bits, as a number or as the hex string of its bits."""
+ return st.just(value) | _hex_spellings(struct.pack(_PACKED[width], value).hex())
+
+
+def _float_fill_values(width: FloatWidth) -> st.SearchStrategy[JSONValue]:
+ """A number, an integer the type holds exactly, a named non-finite value, or the bits in hex.
+
+ A float's fill value is any of these
+ (https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v3/data-types/index.rst#L63-L79).
+ Numbers are drawn from those the type holds, so none is rounded: a
+ `float16` takes `1e300` by the letter of the spec, and a reader is
+ free to make of it what it will.
+ """
+ # Every integer of magnitude up to 2**11 is a float16, the narrowest type.
+ numbers = finite_floats(width) | st.integers(-(2**11), 2**11)
+ named = st.sampled_from(get_args(FloatSpecialFillValue))
+ bits = st.integers(0, 2**width - 1).flatmap(
+ lambda value: _hex_spellings(f"{value:0{width // 4}x}")
+ )
+ return st.one_of(numbers, named, bits)
+
+
+def _fill_values_of(values: st.SearchStrategy[JSONValue]) -> DataTypeStrategy:
+ """A data type with nothing to configure, whose fill values are `values`."""
+ return DataTypeStrategy(
+ configurations=EMPTY_CONFIGURATIONS, fill_values=lambda _configuration: values
+ )
+
+
+def _byte_values(size: int) -> st.SearchStrategy[JSONValue]:
+ """`size` byte values, each an integer in `[0, 255]`, as a JSON array."""
+ return st.lists(st.integers(0, 255), min_size=size, max_size=size).map(
+ lambda values: cast("JSONValue", tuple(values))
+ )
+
+
+def _raw_bytes_fill_values(configuration: Mapping[str, JSONValue]) -> st.SearchStrategy[JSONValue]:
+ """One byte value for each 8 bits of the size."""
+ return _byte_values(cast("RawBytesConfiguration", configuration)["bits"] // 8)
+
+
+def _base64(value: bytes) -> JSONValue:
+ return base64.b64encode(value).decode("ascii")
+
+
+def _complex_fill_values(width: FloatWidth) -> st.SearchStrategy[JSONValue]:
+ """A pair of float fill values of `width` bits, real then imaginary."""
+ component = _float_fill_values(width)
+ return st.tuples(component, component).map(lambda pair: cast("JSONValue", pair))
+
+
+_BYTES_FILL_VALUES: Final = st.integers(0, _MAX_BYTES).flatmap(_byte_values) | st.binary(
+ max_size=_MAX_BYTES
+).map(_base64)
+"""A `bytes` fill value: byte values, or the bytes in standard-alphabet base64."""
+
+_LENGTH_BYTES: Final = st.integers(1, _MAX_BYTES)
+"""The `length_bytes` of zarr-python's fixed-length byte types."""
+
+
+def _base64_fill_values(
+ configuration: Mapping[str, JSONValue],
+) -> st.SearchStrategy[JSONValue]:
+ """At most `length_bytes` bytes in standard-alphabet base64, as zarr-python's fixed-length byte types take them."""
+ length_bytes = cast("ZarrPythonRawBytesConfiguration", configuration)["length_bytes"]
+ return st.binary(max_size=length_bytes).map(_base64)
+
+
+_TEXT: Final = st.text(st.characters(exclude_categories=("Cs",)), max_size=16)
+"""Text a JSON document can hold: no lone surrogate, which is no Unicode scalar value."""
+
+_MAX_CODE_POINTS: Final = 16
+"""The most code points a drawn `fixed_length_utf32` value holds."""
+
+
+def _fixed_length_utf32_fill_values(
+ configuration: Mapping[str, JSONValue],
+) -> st.SearchStrategy[JSONValue]:
+ """Text of at most one code point for each 4 bytes of the size, some of them trailing `U+0000` padding."""
+ capacity = cast("FixedLengthUtf32Configuration", configuration)["length_bytes"] // 4
+ return st.text(st.characters(exclude_categories=("Cs",)) | st.just("\x00"), max_size=capacity)
+
+
+_NUMPY_TIME: Final = DataTypeStrategy(
+ configurations=st.fixed_dictionaries(
+ {
+ "unit": st.sampled_from(NUMPY_TIME_UNIT),
+ "scale_factor": st.integers(1, NUMPY_TIME_MAX_SCALE_FACTOR),
+ }
+ ),
+ fill_values=lambda _configuration: st.integers(-(2**63), 2**63 - 1) | st.just("NaT"),
+)
+"""Both numpy time types: a unit, and how many of it one tick is; a signed 64-bit count of ticks, or `"NaT"`."""
+
+DATA_TYPE_STRATEGIES: Final[Mapping[DataTypeDefinition[Any], DataTypeStrategy]] = {
+ BOOL_DATA_TYPE: _fill_values_of(st.booleans()),
+ **{
+ definition: _fill_values_of(st.integers(low, high))
+ for definition, (low, high) in INTEGER_RANGES.items()
+ },
+ **{
+ definition: _fill_values_of(_float_fill_values(width))
+ for definition, width in FLOAT_WIDTHS.items()
+ },
+ **{
+ definition: _fill_values_of(_complex_fill_values(width))
+ for definition, width in COMPLEX_WIDTHS.items()
+ },
+ RAW_BYTES_DATA_TYPE: DataTypeStrategy(
+ configurations=st.integers(1, _MAX_RAW_BYTES).map(
+ lambda size: RawBytesConfiguration(bits=8 * size)
+ ),
+ fill_values=_raw_bytes_fill_values,
+ ),
+ BYTES_DATA_TYPE: _fill_values_of(_BYTES_FILL_VALUES),
+ STRING_DATA_TYPE: _fill_values_of(_TEXT),
+ NULL_TERMINATED_BYTES_DATA_TYPE: DataTypeStrategy(
+ configurations=_LENGTH_BYTES.map(
+ lambda length_bytes: NullTerminatedBytesConfiguration(length_bytes=length_bytes)
+ ),
+ fill_values=_base64_fill_values,
+ ),
+ VARIABLE_LENGTH_BYTES_DATA_TYPE: _fill_values_of(st.binary(max_size=_MAX_BYTES).map(_base64)),
+ ZARR_PYTHON_RAW_BYTES_DATA_TYPE: DataTypeStrategy(
+ configurations=_LENGTH_BYTES.map(
+ lambda length_bytes: ZarrPythonRawBytesConfiguration(length_bytes=length_bytes)
+ ),
+ fill_values=_base64_fill_values,
+ ),
+ FIXED_LENGTH_UTF32_DATA_TYPE: DataTypeStrategy(
+ configurations=st.integers(1, _MAX_CODE_POINTS).map(
+ lambda count: FixedLengthUtf32Configuration(length_bytes=4 * count)
+ ),
+ fill_values=_fixed_length_utf32_fill_values,
+ ),
+ NUMPY_DATETIME64_DATA_TYPE: _NUMPY_TIME,
+ NUMPY_TIMEDELTA64_DATA_TYPE: _NUMPY_TIME,
+}
+"""A strategy for each data type that holds no other field."""
+
+__all__ = [
+ "COMPLEX_WIDTHS",
+ "DATA_TYPE_STRATEGIES",
+ "FLOAT_WIDTHS",
+ "INTEGER_RANGES",
+ "FloatWidth",
+ "finite_floats",
+ "float_spellings",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/testing/_strategy.py b/packages/zarr-metadata/src/zarr_metadata/testing/_strategy.py
new file mode 100644
index 0000000000..73de0f8f5f
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/testing/_strategy.py
@@ -0,0 +1,147 @@
+"""What draws one definition's configurations: a record for each kind of definition.
+
+Each record holds what its kind's configurations depend on, the way
+each kind of definition does: a data type's fill values depend on its
+configuration, a chunk grid's on the shape of the array, a codec's on
+the chunk it is handed. A chunk key encoding's depend on nothing.
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections.abc import Callable, Mapping
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any, ClassVar, Final, TypeAlias, cast
+
+from hypothesis import strategies as st
+
+from zarr_metadata.definition._definition import EmptyConfiguration
+from zarr_metadata.v3._definition import (
+ Chunk,
+ ChunkGridDefinition,
+ ChunkKeyEncodingDefinition,
+ CodecDefinition,
+ DataTypeDefinition,
+)
+
+if TYPE_CHECKING:
+ from hypothesis.strategies import SearchStrategy
+
+ from zarr_metadata._common import JSONValue
+ from zarr_metadata.definition._definition import Definition
+
+Configurations: TypeAlias = "SearchStrategy[Mapping[str, JSONValue]]"
+"""Configurations of one definition, each the mapping every configuration is."""
+
+NOTHING_KNOWN: Final = Chunk()
+"""A chunk nothing is known of, which every codec takes."""
+
+MAX_RANK: Final = 5
+"""The most axes drawn where the number of axes is not known."""
+
+EMPTY_CONFIGURATIONS: Final = st.builds(EmptyConfiguration)
+"""A fresh empty configuration for each draw, so a test that changes one changes no other."""
+
+
+class DefinitionStrategy(ABC):
+ """How to draw the configurations of one kind of definition."""
+
+ __slots__ = ()
+
+ kind: ClassVar[type[Definition[Any]]]
+ """The kind of definition whose configurations it draws."""
+
+ @abstractmethod
+ def unconstrained(self) -> Configurations:
+ """Configurations when nothing is known of what they depend on: what the definition's rules accept."""
+
+
+@dataclass(frozen=True, kw_only=True, slots=True)
+class DataTypeStrategy(DefinitionStrategy):
+ """How to draw a data type's configurations, and the fill values an array of one takes."""
+
+ kind: ClassVar[type[Definition[Any]]] = DataTypeDefinition
+
+ configurations: Configurations
+ """Configurations its definition accepts."""
+ fill_values: Callable[[Mapping[str, JSONValue]], SearchStrategy[JSONValue]]
+ """Fill values of the data type a configuration describes."""
+
+ def unconstrained(self) -> Configurations:
+ return self.configurations
+
+
+@dataclass(frozen=True, kw_only=True, slots=True)
+class ChunkGridStrategy(DefinitionStrategy):
+ """How to draw a chunk grid's configurations over an array of a shape."""
+
+ kind: ClassVar[type[Definition[Any]]] = ChunkGridDefinition
+
+ configurations: Callable[[tuple[int, ...] | None], Configurations]
+ """Configurations of grids that fit an array of the shape.
+
+ Handed None, no shape is known, and what is drawn is what the grid's
+ rules accept, whatever shape it is put over.
+ """
+
+ def unconstrained(self) -> Configurations:
+ return self.configurations(None)
+
+
+@dataclass(frozen=True, kw_only=True, slots=True)
+class ChunkKeyEncodingStrategy(DefinitionStrategy):
+ """How to draw a chunk key encoding's configurations."""
+
+ kind: ClassVar[type[Definition[Any]]] = ChunkKeyEncodingDefinition
+
+ configurations: Configurations
+ """Configurations its definition accepts."""
+
+ def unconstrained(self) -> Configurations:
+ return self.configurations
+
+
+@dataclass(frozen=True, kw_only=True, slots=True)
+class CodecStrategy(DefinitionStrategy):
+ """How to draw a codec's configurations, handed a chunk.
+
+ Every codec takes `Chunk()`, a chunk nothing is known of, so a
+ strategy that draws nothing for it is refused when it is built.
+ """
+
+ kind: ClassVar[type[Definition[Any]]] = CodecDefinition
+
+ configurations: Callable[[Chunk], Configurations | None]
+ """Configurations of the codec handed the chunk; None when no configuration takes it.
+
+ A `bytes` codec handed a `string` chunk is None, since the spec gives
+ that data type a codec of its own.
+ """
+
+ def __post_init__(self) -> None:
+ if self.configurations(NOTHING_KNOWN) is None:
+ msg = "a codec strategy draws configurations for Chunk(), a chunk nothing is known of; got None"
+ raise ValueError(msg)
+
+ def unconstrained(self) -> Configurations:
+ return cast("Configurations", self.configurations(NOTHING_KNOWN))
+
+
+Strategies: TypeAlias = Mapping[Any, DefinitionStrategy]
+"""How to draw each definition's configurations, by the definition.
+
+The key is `Any`, not `Definition[Any]`: a mapping's key type is
+invariant, so `{MY_CODEC: CodecStrategy(...)}`, a mapping keyed by a
+`CodecDefinition`, would not be a `Mapping[Definition[Any], ...]`.
+"""
+
+__all__ = [
+ "NOTHING_KNOWN",
+ "ChunkGridStrategy",
+ "ChunkKeyEncodingStrategy",
+ "CodecStrategy",
+ "Configurations",
+ "DataTypeStrategy",
+ "DefinitionStrategy",
+ "Strategies",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/testing/strategies.py b/packages/zarr-metadata/src/zarr_metadata/testing/strategies.py
new file mode 100644
index 0000000000..8491c9c171
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/testing/strategies.py
@@ -0,0 +1,299 @@
+"""Hypothesis strategies for valid Zarr v3 metadata fields.
+
+Every strategy here draws metadata its definition accepts, and that the
+spec allows: a reader, writer or converter can test itself against the
+whole space of a codec's, data type's, chunk grid's or chunk key
+encoding's configurations instead of the handful its authors thought to
+write down.
+
+```python
+from hypothesis import given
+from zarr_metadata.testing.strategies import fields
+from zarr_metadata.v3.codec.blosc import BLOSC_CODEC
+
+@given(fields(BLOSC_CODEC))
+def test_my_reader_reads_every_blosc_codec(field):
+ my_reader.read_codec(field)
+```
+
+What a configuration depends on, each kind's function takes:
+`fill_values` a data type's configuration, `chunk_grid_configurations`
+the shape of the array, `codec_configurations` the `Chunk` the codec is
+handed. Left out, nothing is known of it, and what is drawn is what the
+definition's rules accept. `fields` wraps configurations in the
+envelopes a document writes them in.
+
+How each definition's configurations are drawn is a value,
+`Strategies`, keyed by the definition: `DEFAULT_STRATEGIES` holds one
+for each definition this package defines that holds no other field. An
+extension author adds their own with
+`DEFAULT_STRATEGIES | {MY_CODEC: CodecStrategy(...)}`.
+
+Configurations are drawn at the data level the TypedDicts declare, JSON
+arrays as tuples; `json.dumps` takes them as they are.
+
+Requires the `testing` extra (`pip install zarr-metadata[testing]`).
+"""
+
+from __future__ import annotations
+
+from functools import partial
+from types import MappingProxyType
+from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar, cast
+
+from hypothesis import strategies as st
+
+from zarr_metadata._sentinel import UNSET
+from zarr_metadata.definition._definition import Definition, as_kind, canonicalize, kind_of
+from zarr_metadata.testing._chunk_grid import CHUNK_GRID_STRATEGIES
+from zarr_metadata.testing._chunk_key_encoding import CHUNK_KEY_ENCODING_STRATEGIES
+from zarr_metadata.testing._codec import CODEC_STRATEGIES
+from zarr_metadata.testing._data_type import DATA_TYPE_STRATEGIES
+from zarr_metadata.testing._strategy import (
+ NOTHING_KNOWN,
+ ChunkGridStrategy,
+ ChunkKeyEncodingStrategy,
+ CodecStrategy,
+ Configurations,
+ DataTypeStrategy,
+ DefinitionStrategy,
+ Strategies,
+)
+from zarr_metadata.v3._context import ZarrV3Context
+from zarr_metadata.v3._core import CORE_CONTEXT
+from zarr_metadata.v3._definition import Chunk, DataTypeDefinition
+
+if TYPE_CHECKING:
+ from collections.abc import Mapping
+
+ from hypothesis.strategies import SearchStrategy
+
+ from zarr_metadata._common import JSONValue
+ from zarr_metadata.v3 import ZarrV3MetadataFieldJSON
+ from zarr_metadata.v3._context import ZarrV3ContextSlots
+ from zarr_metadata.v3._definition import (
+ ChunkGridDefinition,
+ ChunkKeyEncodingDefinition,
+ CodecDefinition,
+ )
+
+C = TypeVar("C")
+
+Spellings = Literal["all", "canonical"]
+"""How fields are written: every spelling the spec allows, or only the simplest.
+
+`"all"` draws each envelope the spec gives a field -- a bare name, an
+object without a configuration, an empty configuration, a
+`must_understand` of `true` -- and configurations that say what a
+simpler one says, as a `blosc` `typesize` under `noshuffle` does.
+`"canonical"` draws each in the spelling `canonicalize` gives it, which
+is what a writer that writes the fewest words writes.
+"""
+
+DEFAULT_STRATEGIES: Final[MappingProxyType[Definition[Any], DefinitionStrategy]] = MappingProxyType(
+ {
+ **DATA_TYPE_STRATEGIES,
+ **CHUNK_GRID_STRATEGIES,
+ **CHUNK_KEY_ENCODING_STRATEGIES,
+ **CODEC_STRATEGIES,
+ }
+)
+"""A strategy for each definition this package defines that holds no other field."""
+
+
+def _strategy_for(definition: Definition[Any], strategies: Strategies) -> DefinitionStrategy:
+ """The strategy `strategies` holds for `definition`, which draws configurations of its kind."""
+ found = strategies.get(definition)
+ if found is None:
+ msg = f"no strategy for {definition.name!r}; pass one in `strategies`"
+ raise LookupError(msg)
+ if not isinstance(definition, found.kind):
+ msg = (
+ f"the strategy for {definition.name!r}, a {type(definition).__name__}, draws "
+ f"configurations of a {found.kind.__name__}: {found!r}"
+ )
+ raise TypeError(msg)
+ return found
+
+
+def data_type_configurations(
+ definition: DataTypeDefinition[C], *, strategies: Strategies = DEFAULT_STRATEGIES
+) -> SearchStrategy[C]:
+ """Configurations of the data type `definition` defines."""
+ found = cast("DataTypeStrategy", _strategy_for(definition, strategies))
+ return cast("SearchStrategy[C]", found.configurations)
+
+
+def fill_values(
+ definition: DataTypeDefinition[C],
+ configuration: C,
+ *,
+ strategies: Strategies = DEFAULT_STRATEGIES,
+) -> SearchStrategy[JSONValue]:
+ """Fill values of the data type `definition` and `configuration` describe, in every spelling the spec gives them."""
+ found = cast("DataTypeStrategy", _strategy_for(definition, strategies))
+ return found.fill_values(cast("Mapping[str, JSONValue]", configuration))
+
+
+def chunk_grid_configurations(
+ definition: ChunkGridDefinition[C],
+ shape: tuple[int, ...] | None = None,
+ *,
+ strategies: Strategies = DEFAULT_STRATEGIES,
+) -> SearchStrategy[C]:
+ """Configurations of the chunk grid `definition` defines, that fit an array of `shape`.
+
+ With no shape, what is drawn is what the grid's rules accept.
+ """
+ found = cast("ChunkGridStrategy", _strategy_for(definition, strategies))
+ return cast("SearchStrategy[C]", found.configurations(shape))
+
+
+def chunk_key_encoding_configurations(
+ definition: ChunkKeyEncodingDefinition[C], *, strategies: Strategies = DEFAULT_STRATEGIES
+) -> SearchStrategy[C]:
+ """Configurations of the chunk key encoding `definition` defines."""
+ found = cast("ChunkKeyEncodingStrategy", _strategy_for(definition, strategies))
+ return cast("SearchStrategy[C]", found.configurations)
+
+
+def codec_configurations(
+ definition: CodecDefinition[C],
+ chunk: Chunk = NOTHING_KNOWN,
+ *,
+ strategies: Strategies = DEFAULT_STRATEGIES,
+) -> SearchStrategy[C] | None:
+ """Configurations of the codec `definition` defines, handed `chunk`; None when none takes it.
+
+ With no chunk, nothing is known of it, and what is drawn is what the
+ codec's rules accept.
+ """
+ found = cast("CodecStrategy", _strategy_for(definition, strategies))
+ return cast("SearchStrategy[C] | None", found.configurations(chunk))
+
+
+def _written_bare(definition: Definition[Any]) -> bool:
+ """Whether the spec has the field written as its bare name: a data type it defines itself.
+
+ "If the data type is defined in this specification, then the value
+ must be the data type identifier provided as a string"
+ (https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v3/core/index.rst#L509-L512).
+ """
+ if not isinstance(definition, DataTypeDefinition):
+ return False
+ return CORE_CONTEXT.claimant(DataTypeDefinition, definition.name) is definition
+
+
+@st.composite
+def _any_spelling(
+ draw: st.DrawFn, definition: Definition[Any], configurations: SearchStrategy[Any]
+) -> ZarrV3MetadataFieldJSON:
+ """A configuration of `definition` in one of the envelopes the spec allows it.
+
+ A configuration with nothing in it is written as the bare name, an
+ object without one, or an object with an empty one; any object may
+ say `must_understand` is `true`, which is what absence means. A name
+ that carries the configuration, as `r16` does, and a data type the
+ spec defines itself, are written as the bare name.
+ """
+ configuration = cast("Mapping[str, JSONValue]", draw(configurations))
+ carried = definition._carrying_name(configuration) # pyright: ignore[reportPrivateUsage]
+ if carried is not None:
+ return carried
+ if _written_bare(definition):
+ return definition.name
+ empty = len(configuration) == 0
+ if empty and draw(st.booleans()):
+ return definition.name
+ field: dict[str, JSONValue] = {"name": definition.name}
+ if not empty or draw(st.booleans()):
+ field["configuration"] = configuration
+ if draw(st.booleans()):
+ field["must_understand"] = True
+ return cast("ZarrV3MetadataFieldJSON", field)
+
+
+def _scope_of(definition: Definition[Any]) -> ZarrV3Context:
+ """The scope that claims `definition` and nothing else; a `TypeError` for a definition no slot of a Zarr v3 scope holds."""
+ for slot, kind in ZarrV3Context.kinds.items():
+ if isinstance(definition, kind):
+ return ZarrV3Context(**cast("ZarrV3ContextSlots", {slot: (definition,)}))
+ msg = (
+ f"{definition.name!r} is a {type(definition).__name__}, "
+ "which no slot of a Zarr v3 scope holds"
+ )
+ raise TypeError(msg)
+
+
+def _checked(
+ field: ZarrV3MetadataFieldJSON,
+ *,
+ definition: Definition[Any],
+ kind: type[Definition[Any]],
+ scope: ZarrV3Context,
+ spellings: Spellings,
+) -> ZarrV3MetadataFieldJSON:
+ """`field`, or its canonical spelling, once it reads without a problem in `scope`; a `ValueError` when it does not."""
+ canonical, problems = canonicalize(field, kind, scope)
+ if len(problems) != 0:
+ msg = f"{field!r}, drawn for {definition.name!r}, which is refused: {problems!r}"
+ raise ValueError(msg)
+ if spellings == "canonical":
+ return cast("ZarrV3MetadataFieldJSON", canonical)
+ return field
+
+
+def fields(
+ definition: Definition[C],
+ configurations: SearchStrategy[C] | UNSET = UNSET,
+ *,
+ spellings: Spellings = "all",
+ strategies: Strategies = DEFAULT_STRATEGIES,
+) -> SearchStrategy[ZarrV3MetadataFieldJSON]:
+ """Metadata fields of `definition`: its name and a configuration, in an envelope a document writes.
+
+ `configurations` is what to draw the configuration from, by default
+ the definition's own with nothing known of what they depend on: pass
+ `codec_configurations(definition, chunk)` for a codec handed a chunk,
+ once it is known not to be None. `spellings` says whether every
+ spelling the spec allows is drawn, or only the simplest.
+
+ Each field is read in a scope of its definition alone, with no chunk
+ known, and one its definition's rules refuse, as they refuse one
+ drawn from `configurations` they do not accept, raises a
+ `ValueError`. A codec's chunk rules are not checked: what fits the
+ chunk is what `codec_configurations` draws.
+ """
+ drawn: Configurations | SearchStrategy[C] = (
+ _strategy_for(definition, strategies).unconstrained()
+ if configurations is UNSET
+ else configurations
+ )
+ checked = partial(
+ _checked,
+ definition=definition,
+ kind=as_kind(kind_of(definition)),
+ scope=_scope_of(definition),
+ spellings=spellings,
+ )
+ return _any_spelling(definition, drawn).map(checked)
+
+
+__all__ = [
+ "DEFAULT_STRATEGIES",
+ "NOTHING_KNOWN",
+ "ChunkGridStrategy",
+ "ChunkKeyEncodingStrategy",
+ "CodecStrategy",
+ "Configurations",
+ "DataTypeStrategy",
+ "DefinitionStrategy",
+ "Spellings",
+ "Strategies",
+ "chunk_grid_configurations",
+ "chunk_key_encoding_configurations",
+ "codec_configurations",
+ "data_type_configurations",
+ "fields",
+ "fill_values",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v2/__init__.py b/packages/zarr-metadata/src/zarr_metadata/v2/__init__.py
index 741ff5cfb5..af6dda6c22 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v2/__init__.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v2/__init__.py
@@ -6,12 +6,16 @@
and `data_type`, and the v2 kinds and scope are in `definition`.
This module also exports every v2 definition (`GZIP_CODEC_V2`,
-`FLOAT_DATA_TYPE_V2`, ...), the scope `CORE_V2`, and the scope class
+`FLOAT_DATA_TYPE_V2`, ...), the scope `CORE_V2_CONTEXT`, and the scope class
`ZarrV2Context`, so that one import builds or updates a scope. Each is
the same object as in its own submodule, where a definition's
configuration types and constants are.
"""
+from typing import TYPE_CHECKING
+
+from zarr_metadata._renamed import renamed_scope
+from zarr_metadata.v2._core import RENAMED_SCOPES_V2
from zarr_metadata.v2.codec.checksum import (
ADLER32_CODEC_V2,
CRC32_CODEC_V2,
@@ -59,7 +63,7 @@
DATETIME64_DATA_TYPE_V2,
TIMEDELTA64_DATA_TYPE_V2,
)
-from zarr_metadata.v2.definition import CORE_V2, ZarrV2Context
+from zarr_metadata.v2.definition import CORE_V2_CONTEXT, ZarrV2Context
__all__ = [
"ADLER32_CODEC_V2",
@@ -70,7 +74,7 @@
"BYTES_DATA_TYPE_V2",
"BZ2_CODEC_V2",
"COMPLEX_DATA_TYPE_V2",
- "CORE_V2",
+ "CORE_V2_CONTEXT",
"CRC32C_CODEC_V2",
"CRC32_CODEC_V2",
"DATETIME64_DATA_TYPE_V2",
@@ -98,3 +102,11 @@
"ZSTD_CODEC_V2",
"ZarrV2Context",
]
+
+
+if not TYPE_CHECKING:
+ # Hidden from type checkers, which would otherwise read every missing
+ # name of this module as a scope.
+ def __getattr__(name: str) -> object:
+ """`CORE_V2`, the scope's name before it ended in `_CONTEXT`, with a `DeprecationWarning`."""
+ return renamed_scope(__name__, name, RENAMED_SCOPES_V2)
diff --git a/packages/zarr-metadata/src/zarr_metadata/v2/_core.py b/packages/zarr-metadata/src/zarr_metadata/v2/_core.py
new file mode 100644
index 0000000000..0daaa6c344
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/v2/_core.py
@@ -0,0 +1,17 @@
+"""The scope this package provides for Zarr v2."""
+
+from __future__ import annotations
+
+from typing import Final
+
+from zarr_metadata.v2._context import ZarrV2Context
+from zarr_metadata.v2.codec import CODECS_V2
+from zarr_metadata.v2.data_type import DATA_TYPES_V2
+
+CORE_V2_CONTEXT: Final = ZarrV2Context(data_types=DATA_TYPES_V2, codecs=CODECS_V2)
+"""The data types that zarr-python 2.x writes and the codecs that numcodecs 0.16 configures."""
+
+RENAMED_SCOPES_V2: Final = {"CORE_V2": ("CORE_V2_CONTEXT", CORE_V2_CONTEXT)}
+"""The v2 scope's name before it ended in `_CONTEXT`, with the scope's name now and the scope."""
+
+__all__ = ["CORE_V2_CONTEXT", "RENAMED_SCOPES_V2"]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v2/definition.py b/packages/zarr-metadata/src/zarr_metadata/v2/definition.py
index 8cf4990992..d0da681c58 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v2/definition.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v2/definition.py
@@ -4,10 +4,10 @@
in `compressor` and `filters`. Zarr v2 has no extension registry, but it
still has a scope: the data types that zarr-python 2.x writes (one
definition per NumPy family), and the codecs that numcodecs 0.16
-configures (one per id that this package models). `CORE_V2` is that
+configures (one per id that this package models). `CORE_V2_CONTEXT` is that
scope. To read one field in it, call `zarr_metadata.definition.resolve`
with the field's kind, as for a v3 field:
-`resolve(" object:
+ """`CORE_V2`, the scope's name before it ended in `_CONTEXT`, with a `DeprecationWarning`."""
+ return renamed_scope(__name__, name, RENAMED_SCOPES_V2)
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/__init__.py b/packages/zarr-metadata/src/zarr_metadata/v3/__init__.py
index c9b1fc425b..3953cf5317 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v3/__init__.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/__init__.py
@@ -8,14 +8,18 @@
`definition`.
This module also exports every v3 definition (`BYTES_CODEC`,
-`FLOAT64_DATA_TYPE`, `REGULAR_CHUNK_GRID`, ...), the scopes `CORE` and
-`CORE_AND_EXTENSIONS`, and the scope class `ZarrV3Context`, so that one
+`FLOAT64_DATA_TYPE`, `REGULAR_CHUNK_GRID`, ...), the scopes `CORE_CONTEXT`,
+`CORE_AND_EXTENSIONS_CONTEXT` and `ZARR_PYTHON_CONTEXT`, and the scope class `ZarrV3Context`, so that one
import builds or updates a scope. Each is the same object as in its own
submodule, where a definition's configuration types and constants are.
"""
+from typing import TYPE_CHECKING
+
from zarr_metadata._common import ZarrV3NamedConfigJSON
+from zarr_metadata._renamed import renamed_scope
from zarr_metadata.v3._common import ZarrV3ExtensionField, ZarrV3MetadataFieldJSON
+from zarr_metadata.v3._core import RENAMED_SCOPES
from zarr_metadata.v3._hierarchy import NodeName, NodePath
from zarr_metadata.v3.chunk_grid.rectilinear import RECTILINEAR_CHUNK_GRID
from zarr_metadata.v3.chunk_grid.regular import REGULAR_CHUNK_GRID
@@ -29,12 +33,15 @@
from zarr_metadata.v3.codec.scale_offset import SCALE_OFFSET_CODEC
from zarr_metadata.v3.codec.sharding_indexed import SHARDING_INDEXED_CODEC
from zarr_metadata.v3.codec.transpose import TRANSPOSE_CODEC
+from zarr_metadata.v3.codec.vlen_bytes import VLEN_BYTES_CODEC
+from zarr_metadata.v3.codec.vlen_utf8 import VLEN_UTF8_CODEC
from zarr_metadata.v3.codec.zstd import ZSTD_CODEC
from zarr_metadata.v3.consolidated import CONSOLIDATED_METADATA_EXTRA_FIELD
from zarr_metadata.v3.data_type.bool import BOOL_DATA_TYPE
from zarr_metadata.v3.data_type.bytes import BYTES_DATA_TYPE
from zarr_metadata.v3.data_type.complex64 import COMPLEX64_DATA_TYPE
from zarr_metadata.v3.data_type.complex128 import COMPLEX128_DATA_TYPE
+from zarr_metadata.v3.data_type.fixed_length_utf32 import FIXED_LENGTH_UTF32_DATA_TYPE
from zarr_metadata.v3.data_type.float16 import FLOAT16_DATA_TYPE
from zarr_metadata.v3.data_type.float32 import FLOAT32_DATA_TYPE
from zarr_metadata.v3.data_type.float64 import FLOAT64_DATA_TYPE
@@ -42,6 +49,7 @@
from zarr_metadata.v3.data_type.int16 import INT16_DATA_TYPE
from zarr_metadata.v3.data_type.int32 import INT32_DATA_TYPE
from zarr_metadata.v3.data_type.int64 import INT64_DATA_TYPE
+from zarr_metadata.v3.data_type.null_terminated_bytes import NULL_TERMINATED_BYTES_DATA_TYPE
from zarr_metadata.v3.data_type.numpy_datetime64 import NUMPY_DATETIME64_DATA_TYPE
from zarr_metadata.v3.data_type.numpy_timedelta64 import NUMPY_TIMEDELTA64_DATA_TYPE
from zarr_metadata.v3.data_type.raw import RAW_BYTES_DATA_TYPE
@@ -51,9 +59,12 @@
from zarr_metadata.v3.data_type.uint16 import UINT16_DATA_TYPE
from zarr_metadata.v3.data_type.uint32 import UINT32_DATA_TYPE
from zarr_metadata.v3.data_type.uint64 import UINT64_DATA_TYPE
+from zarr_metadata.v3.data_type.variable_length_bytes import VARIABLE_LENGTH_BYTES_DATA_TYPE
+from zarr_metadata.v3.data_type.zarr_python_raw_bytes import ZARR_PYTHON_RAW_BYTES_DATA_TYPE
from zarr_metadata.v3.definition import (
- CORE,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
+ ZARR_PYTHON_CONTEXT,
ZarrV3Context,
)
@@ -66,10 +77,11 @@
"COMPLEX64_DATA_TYPE",
"COMPLEX128_DATA_TYPE",
"CONSOLIDATED_METADATA_EXTRA_FIELD",
- "CORE",
- "CORE_AND_EXTENSIONS",
+ "CORE_AND_EXTENSIONS_CONTEXT",
+ "CORE_CONTEXT",
"CRC32C_CODEC",
"DEFAULT_CHUNK_KEY_ENCODING",
+ "FIXED_LENGTH_UTF32_DATA_TYPE",
"FLOAT16_DATA_TYPE",
"FLOAT32_DATA_TYPE",
"FLOAT64_DATA_TYPE",
@@ -78,6 +90,7 @@
"INT16_DATA_TYPE",
"INT32_DATA_TYPE",
"INT64_DATA_TYPE",
+ "NULL_TERMINATED_BYTES_DATA_TYPE",
"NUMPY_DATETIME64_DATA_TYPE",
"NUMPY_TIMEDELTA64_DATA_TYPE",
"RAW_BYTES_DATA_TYPE",
@@ -93,6 +106,11 @@
"UINT32_DATA_TYPE",
"UINT64_DATA_TYPE",
"V2_CHUNK_KEY_ENCODING",
+ "VARIABLE_LENGTH_BYTES_DATA_TYPE",
+ "VLEN_BYTES_CODEC",
+ "VLEN_UTF8_CODEC",
+ "ZARR_PYTHON_CONTEXT",
+ "ZARR_PYTHON_RAW_BYTES_DATA_TYPE",
"ZSTD_CODEC",
"NodeName",
"NodePath",
@@ -101,3 +119,11 @@
"ZarrV3MetadataFieldJSON",
"ZarrV3NamedConfigJSON",
]
+
+
+if not TYPE_CHECKING:
+ # Hidden from type checkers, which would otherwise read every missing
+ # name of this module as a scope.
+ def __getattr__(name: str) -> object:
+ """`CORE` and `CORE_AND_EXTENSIONS`, the scopes' names before they ended in `_CONTEXT`, with a `DeprecationWarning`."""
+ return renamed_scope(__name__, name, RENAMED_SCOPES)
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/_core.py b/packages/zarr-metadata/src/zarr_metadata/v3/_core.py
index 5a3f52f4e4..f15d0b0ddf 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v3/_core.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/_core.py
@@ -1,9 +1,11 @@
"""The scopes this package provides for Zarr v3, because "is this valid?" has two useful answers.
-`CORE` contains what the Zarr v3 specification itself defines, so a
+`CORE_CONTEXT` contains what the Zarr v3 specification itself defines, so a
document read in it uses nothing that an implementation could reject as
-optional. `CORE_AND_EXTENSIONS` adds the extensions that `zarr-extensions`
-registers and this package defines. A name in neither scope is not
+optional. `CORE_AND_EXTENSIONS_CONTEXT` adds the extensions that `zarr-extensions`
+registers and this package defines. `ZARR_PYTHON_CONTEXT` adds the data types that
+zarr-python writes but no specification registers, whose spelling
+zarr-python may still change. A name in none of these scopes is not
rejected, because that is what keeps the format open. Instead it is read
as an `UnclaimedField` and not checked.
"""
@@ -25,12 +27,15 @@
from zarr_metadata.v3.codec.scale_offset import SCALE_OFFSET_CODEC
from zarr_metadata.v3.codec.sharding_indexed import SHARDING_INDEXED_CODEC
from zarr_metadata.v3.codec.transpose import TRANSPOSE_CODEC
+from zarr_metadata.v3.codec.vlen_bytes import VLEN_BYTES_CODEC
+from zarr_metadata.v3.codec.vlen_utf8 import VLEN_UTF8_CODEC
from zarr_metadata.v3.codec.zstd import ZSTD_CODEC
from zarr_metadata.v3.consolidated import CONSOLIDATED_METADATA_EXTRA_FIELD
from zarr_metadata.v3.data_type.bool import BOOL_DATA_TYPE
from zarr_metadata.v3.data_type.bytes import BYTES_DATA_TYPE
from zarr_metadata.v3.data_type.complex64 import COMPLEX64_DATA_TYPE
from zarr_metadata.v3.data_type.complex128 import COMPLEX128_DATA_TYPE
+from zarr_metadata.v3.data_type.fixed_length_utf32 import FIXED_LENGTH_UTF32_DATA_TYPE
from zarr_metadata.v3.data_type.float16 import FLOAT16_DATA_TYPE
from zarr_metadata.v3.data_type.float32 import FLOAT32_DATA_TYPE
from zarr_metadata.v3.data_type.float64 import FLOAT64_DATA_TYPE
@@ -38,6 +43,7 @@
from zarr_metadata.v3.data_type.int16 import INT16_DATA_TYPE
from zarr_metadata.v3.data_type.int32 import INT32_DATA_TYPE
from zarr_metadata.v3.data_type.int64 import INT64_DATA_TYPE
+from zarr_metadata.v3.data_type.null_terminated_bytes import NULL_TERMINATED_BYTES_DATA_TYPE
from zarr_metadata.v3.data_type.numpy_datetime64 import NUMPY_DATETIME64_DATA_TYPE
from zarr_metadata.v3.data_type.numpy_timedelta64 import NUMPY_TIMEDELTA64_DATA_TYPE
from zarr_metadata.v3.data_type.raw import RAW_BYTES_DATA_TYPE
@@ -47,8 +53,10 @@
from zarr_metadata.v3.data_type.uint16 import UINT16_DATA_TYPE
from zarr_metadata.v3.data_type.uint32 import UINT32_DATA_TYPE
from zarr_metadata.v3.data_type.uint64 import UINT64_DATA_TYPE
+from zarr_metadata.v3.data_type.variable_length_bytes import VARIABLE_LENGTH_BYTES_DATA_TYPE
+from zarr_metadata.v3.data_type.zarr_python_raw_bytes import ZARR_PYTHON_RAW_BYTES_DATA_TYPE
-CORE: Final = ZarrV3Context(
+CORE_CONTEXT: Final = ZarrV3Context(
data_types=(
BOOL_DATA_TYPE,
INT8_DATA_TYPE,
@@ -80,21 +88,53 @@
)
"""A scope with only what the Zarr v3 specification defines, including the envelope of a group's `consolidated_metadata`, which the spec fixes."""
-CORE_AND_EXTENSIONS: Final = CORE.update(
+CORE_AND_EXTENSIONS_CONTEXT: Final = CORE_CONTEXT.update(
data_types=[
- *CORE.data_types.values(),
+ *CORE_CONTEXT.data_types.values(),
BYTES_DATA_TYPE,
STRING_DATA_TYPE,
+ FIXED_LENGTH_UTF32_DATA_TYPE,
NUMPY_DATETIME64_DATA_TYPE,
NUMPY_TIMEDELTA64_DATA_TYPE,
STRUCT_DATA_TYPE,
],
- chunk_grids=[*CORE.chunk_grids.values(), RECTILINEAR_CHUNK_GRID],
- codecs=[*CORE.codecs.values(), CAST_VALUE_CODEC, SCALE_OFFSET_CODEC, ZSTD_CODEC],
+ chunk_grids=[*CORE_CONTEXT.chunk_grids.values(), RECTILINEAR_CHUNK_GRID],
+ codecs=[
+ *CORE_CONTEXT.codecs.values(),
+ CAST_VALUE_CODEC,
+ SCALE_OFFSET_CODEC,
+ VLEN_BYTES_CODEC,
+ VLEN_UTF8_CODEC,
+ ZSTD_CODEC,
+ ],
)
"""A scope with what the specification defines, plus the extensions that `zarr-extensions` registers and this package defines."""
+ZARR_PYTHON_CONTEXT: Final = CORE_AND_EXTENSIONS_CONTEXT.update(
+ data_types=[
+ *CORE_AND_EXTENSIONS_CONTEXT.data_types.values(),
+ NULL_TERMINATED_BYTES_DATA_TYPE,
+ VARIABLE_LENGTH_BYTES_DATA_TYPE,
+ ZARR_PYTHON_RAW_BYTES_DATA_TYPE,
+ ],
+)
+"""A scope with `CORE_AND_EXTENSIONS_CONTEXT`, plus the data types that zarr-python writes but no specification registers: `null_terminated_bytes`, `variable_length_bytes` and `raw_bytes`.
+
+zarr-python warns when it writes these that their spelling may change, so
+they are kept out of `CORE_AND_EXTENSIONS_CONTEXT`. Read in this scope to check
+strictly what zarr-python writes.
+"""
+
+RENAMED_SCOPES: Final = {
+ "CORE": ("CORE_CONTEXT", CORE_CONTEXT),
+ "CORE_AND_EXTENSIONS": ("CORE_AND_EXTENSIONS_CONTEXT", CORE_AND_EXTENSIONS_CONTEXT),
+}
+"""The v3 scopes' names before they ended in `_CONTEXT`, with each scope's name now and the scope."""
+
+
__all__ = [
- "CORE",
- "CORE_AND_EXTENSIONS",
+ "CORE_AND_EXTENSIONS_CONTEXT",
+ "CORE_CONTEXT",
+ "RENAMED_SCOPES",
+ "ZARR_PYTHON_CONTEXT",
]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/_json_schema.py b/packages/zarr-metadata/src/zarr_metadata/v3/_json_schema.py
index 420266eec4..3640e92abd 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v3/_json_schema.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/_json_schema.py
@@ -15,7 +15,7 @@
)
from zarr_metadata.definition._envelope import EXTENSION_NAME_SCHEMA_PATTERN
from zarr_metadata.v3._context import ZarrV3Context, ZarrV3Kind, is_v3_kind
-from zarr_metadata.v3._core import CORE_AND_EXTENSIONS
+from zarr_metadata.v3._core import CORE_AND_EXTENSIONS_CONTEXT
from zarr_metadata.v3._definition import (
RAW_BYTES_NAME,
RAW_BYTES_NAME_PATTERN,
@@ -52,7 +52,7 @@ def kind_field(kind: type[Definition[Any]]) -> object:
def field_json_schema_v3(
kind: type[ZarrV3Kind], *, context: ZarrV3Context | None = None
) -> JSONSchema:
- """The JSON Schema of one metadata field read as `kind` in `context` (`CORE_AND_EXTENSIONS` when none is given): everything `resolve` checks, except the rules. A scope of another format raises `TypeError`, as it does for every v3 reader. An extra field is written as the member alone, `{key: value}`.
+ """The JSON Schema of one metadata field read as `kind` in `context` (`CORE_AND_EXTENSIONS_CONTEXT` when none is given): everything `resolve` checks, except the rules. A scope of another format raises `TypeError`, as it does for every v3 reader. An extra field is written as the member alone, `{key: value}`.
The schema accepts a field that one of the definitions in scope reads
(its name, its configuration as the TypedDict declares it, an optional
@@ -75,7 +75,7 @@ def field_json_schema_v3(
"Zarr v3 fields only"
)
raise TypeError(msg)
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
schemas = Schemas(field_schemas(context))
if issubclass(asked, (ArrayExtraFieldDefinition, GroupExtraFieldDefinition)):
return schemas.document(_extra_field_schema(asked, context, schemas))
@@ -207,7 +207,7 @@ def node_metadata_json_schema_v3(*, context: ZarrV3Context | None = None) -> JSO
definition declares, or a name that no definition claims. When the
document names a data type in scope, the fill value must have the
JSON shape that the data type's definition declares (for `int8`, an
- integer in [-128, 127]). In a scope that claims it, as `CORE` does,
+ integer in [-128, 127]). In a scope that claims it, as `CORE_CONTEXT` does,
a group's `consolidated_metadata` is the envelope the core spec
fixes, with any JSON as the documents it lists, because the reader
does not read them, and a `null` value, which zarr-python 3.0 and 3.1
@@ -226,7 +226,7 @@ def node_metadata_json_schema_v3(*, context: ZarrV3Context | None = None) -> JSO
arrays as lists. A model's `to_json` returns tuples, and a Python
validator does not accept tuples as arrays.
"""
- context = scoped(context, CORE_AND_EXTENSIONS)
+ context = scoped(context, CORE_AND_EXTENSIONS_CONTEXT)
schemas = Schemas(_documents(context))
array = schemas.of(ZarrV3ArrayMetadataJSON)
group = schemas.of(ZarrV3GroupMetadataJSON)
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/chunk_grid/__init__.py b/packages/zarr-metadata/src/zarr_metadata/v3/chunk_grid/__init__.py
index cc10287e8a..71d1f2c115 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v3/chunk_grid/__init__.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/chunk_grid/__init__.py
@@ -5,7 +5,8 @@
- `regular` -- core v3 spec
- `rectilinear` -- zarr-extensions
-Import a grid's types and its definition from its submodule:
+Import a grid's types and its definition from its submodule, or from this
+package, which re-exports every name its submodules export:
`ChunkGridMetadata`, the canonical type for the grid's permitted JSON
shapes, `ChunkGridObject`, `ChunkGridConfiguration`, and
`_CHUNK_GRID`.
@@ -13,4 +14,40 @@
See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-grids
"""
-__all__: list[str] = []
+from zarr_metadata.v3.chunk_grid.rectilinear import (
+ RECTILINEAR_CHUNK_GRID,
+ RECTILINEAR_CHUNK_GRID_NAME,
+ RectilinearChunkGridConfiguration,
+ RectilinearChunkGridMetadata,
+ RectilinearChunkGridName,
+ RectilinearChunkGridObject,
+ RectilinearDimSpec,
+ canonical_chunk_shapes,
+ canonical_dim_spec,
+)
+from zarr_metadata.v3.chunk_grid.regular import (
+ REGULAR_CHUNK_GRID,
+ REGULAR_CHUNK_GRID_NAME,
+ RegularChunkGridConfiguration,
+ RegularChunkGridMetadata,
+ RegularChunkGridName,
+ RegularChunkGridObject,
+)
+
+__all__ = [
+ "RECTILINEAR_CHUNK_GRID",
+ "RECTILINEAR_CHUNK_GRID_NAME",
+ "REGULAR_CHUNK_GRID",
+ "REGULAR_CHUNK_GRID_NAME",
+ "RectilinearChunkGridConfiguration",
+ "RectilinearChunkGridMetadata",
+ "RectilinearChunkGridName",
+ "RectilinearChunkGridObject",
+ "RectilinearDimSpec",
+ "RegularChunkGridConfiguration",
+ "RegularChunkGridMetadata",
+ "RegularChunkGridName",
+ "RegularChunkGridObject",
+ "canonical_chunk_shapes",
+ "canonical_dim_spec",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/chunk_key_encoding/__init__.py b/packages/zarr-metadata/src/zarr_metadata/v3/chunk_key_encoding/__init__.py
index bd40726986..7162840872 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v3/chunk_key_encoding/__init__.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/chunk_key_encoding/__init__.py
@@ -9,7 +9,8 @@
https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v3/chunk-key-encodings/default/index.rst
https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v3/chunk-key-encodings/v2/index.rst
-Import an encoding's types and its definition from its submodule:
+Import an encoding's types and its definition from its submodule, or from
+this package, which re-exports every name its submodules export:
`ChunkKeyEncodingMetadata`, the canonical type for the encoding's
permitted JSON shapes, `ChunkKeyEncodingObject`,
`ChunkKeyEncodingConfiguration`, and `_CHUNK_KEY_ENCODING`.
@@ -17,4 +18,42 @@
See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-key-encoding
"""
-__all__: list[str] = []
+from zarr_metadata.v3.chunk_key_encoding.default import (
+ DEFAULT_CHUNK_KEY_ENCODING,
+ DEFAULT_CHUNK_KEY_ENCODING_NAME,
+ DEFAULT_CHUNK_KEY_ENCODING_SEPARATOR,
+ DefaultChunkKeyEncodingConfiguration,
+ DefaultChunkKeyEncodingMetadata,
+ DefaultChunkKeyEncodingName,
+ DefaultChunkKeyEncodingObject,
+ DefaultChunkKeyEncodingSeparator,
+)
+from zarr_metadata.v3.chunk_key_encoding.v2 import (
+ V2_CHUNK_KEY_ENCODING,
+ V2_CHUNK_KEY_ENCODING_NAME,
+ V2_CHUNK_KEY_ENCODING_SEPARATOR,
+ V2ChunkKeyEncodingConfiguration,
+ V2ChunkKeyEncodingMetadata,
+ V2ChunkKeyEncodingName,
+ V2ChunkKeyEncodingObject,
+ V2ChunkKeyEncodingSeparator,
+)
+
+__all__ = [
+ "DEFAULT_CHUNK_KEY_ENCODING",
+ "DEFAULT_CHUNK_KEY_ENCODING_NAME",
+ "DEFAULT_CHUNK_KEY_ENCODING_SEPARATOR",
+ "V2_CHUNK_KEY_ENCODING",
+ "V2_CHUNK_KEY_ENCODING_NAME",
+ "V2_CHUNK_KEY_ENCODING_SEPARATOR",
+ "DefaultChunkKeyEncodingConfiguration",
+ "DefaultChunkKeyEncodingMetadata",
+ "DefaultChunkKeyEncodingName",
+ "DefaultChunkKeyEncodingObject",
+ "DefaultChunkKeyEncodingSeparator",
+ "V2ChunkKeyEncodingConfiguration",
+ "V2ChunkKeyEncodingMetadata",
+ "V2ChunkKeyEncodingName",
+ "V2ChunkKeyEncodingObject",
+ "V2ChunkKeyEncodingSeparator",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/codec/__init__.py b/packages/zarr-metadata/src/zarr_metadata/v3/codec/__init__.py
index 569a298d7b..f84aedf743 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v3/codec/__init__.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/codec/__init__.py
@@ -2,11 +2,13 @@
Each codec defined by the spec or by zarr-extensions has its own submodule
(`blosc`, `bytes`, `cast_value`, `crc32c`, `gzip`, `scale_offset`,
-`sharding_indexed`, `transpose`, `zstd`). Import a codec's types and its
-definition from its submodule: `CodecMetadata`, the canonical type for
-the codec's permitted JSON shapes (object form plus, where the spec allows,
-a bare-string short-hand form), `CodecObject`, `_CODEC`, and, for a
-codec that has a configuration, `CodecConfiguration`.
+`sharding_indexed`, `transpose`, `vlen_bytes`, `vlen_utf8`, `zstd`). Import a
+codec's types and its definition from its submodule, or from this package,
+which re-exports every name its submodules export: `CodecMetadata`, the
+canonical type for the codec's permitted JSON shapes (object form plus,
+where the spec allows, a bare-string short-hand form), `CodecObject`,
+`_CODEC`, and, for a codec that has a configuration,
+`CodecConfiguration`.
For the JSON of any codec entry (one of the entries in array metadata's
`codecs`), import `ZarrV3MetadataFieldJSON` from `zarr_metadata.v3`. A
@@ -22,4 +24,188 @@
See https://zarr-specs.readthedocs.io/en/latest/v3/codecs/index.html
"""
-__all__: list[str] = []
+from zarr_metadata.v3.codec.blosc import (
+ BLOSC_CNAME,
+ BLOSC_CODEC,
+ BLOSC_CODEC_NAME,
+ BLOSC_NO_SHUFFLE,
+ BLOSC_SHUFFLE,
+ BloscCName,
+ BloscCodecConfiguration,
+ BloscCodecMetadata,
+ BloscCodecName,
+ BloscCodecObject,
+ BloscShuffle,
+)
+from zarr_metadata.v3.codec.bytes import (
+ BYTES_CODEC,
+ BYTES_CODEC_NAME,
+ ENDIANNESS,
+ BytesCodecConfiguration,
+ BytesCodecMetadata,
+ BytesCodecName,
+ BytesCodecObject,
+ Endianness,
+)
+from zarr_metadata.v3.codec.cast_value import (
+ CAST_OUT_OF_RANGE_MODE,
+ CAST_ROUNDING_MODE,
+ CAST_VALUE_CODEC,
+ CAST_VALUE_CODEC_NAME,
+ CastOutOfRangeMode,
+ CastRoundingMode,
+ CastValueCodecConfiguration,
+ CastValueCodecMetadata,
+ CastValueCodecName,
+ CastValueCodecObject,
+ ScalarMap,
+ ScalarMapEntry,
+)
+from zarr_metadata.v3.codec.crc32c import (
+ CRC32C_CODEC,
+ CRC32C_CODEC_NAME,
+ Crc32cCodecMetadata,
+ Crc32cCodecName,
+ Crc32cCodecObject,
+)
+from zarr_metadata.v3.codec.gzip import (
+ GZIP_CODEC,
+ GZIP_CODEC_NAME,
+ GzipCodecConfiguration,
+ GzipCodecMetadata,
+ GzipCodecName,
+ GzipCodecObject,
+)
+from zarr_metadata.v3.codec.scale_offset import (
+ SCALE_OFFSET_CODEC,
+ SCALE_OFFSET_CODEC_NAME,
+ ScaleOffsetCodecConfiguration,
+ ScaleOffsetCodecMetadata,
+ ScaleOffsetCodecName,
+ ScaleOffsetCodecObject,
+)
+from zarr_metadata.v3.codec.sharding_indexed import (
+ SHARDING_INDEX_LOCATION,
+ SHARDING_INDEXED_CODEC,
+ SHARDING_INDEXED_CODEC_NAME,
+ ShardingIndexedCodecConfiguration,
+ ShardingIndexedCodecMetadata,
+ ShardingIndexedCodecName,
+ ShardingIndexedCodecObject,
+ ShardingIndexLocation,
+)
+from zarr_metadata.v3.codec.transpose import (
+ TRANSPOSE_CODEC,
+ TRANSPOSE_CODEC_NAME,
+ TransposeCodecConfiguration,
+ TransposeCodecMetadata,
+ TransposeCodecName,
+ TransposeCodecObject,
+)
+from zarr_metadata.v3.codec.vlen_bytes import (
+ VLEN_BYTES_CODEC,
+ VLEN_BYTES_CODEC_NAME,
+ VlenBytesCodecMetadata,
+ VlenBytesCodecName,
+ VlenBytesCodecObject,
+)
+from zarr_metadata.v3.codec.vlen_utf8 import (
+ VLEN_UTF8_CODEC,
+ VLEN_UTF8_CODEC_NAME,
+ VlenUtf8CodecMetadata,
+ VlenUtf8CodecName,
+ VlenUtf8CodecObject,
+)
+from zarr_metadata.v3.codec.zstd import (
+ ZSTD_CODEC,
+ ZSTD_CODEC_NAME,
+ ZSTD_MAX_LEVEL,
+ ZSTD_MIN_LEVEL,
+ ZstdCodecConfiguration,
+ ZstdCodecMetadata,
+ ZstdCodecName,
+ ZstdCodecObject,
+)
+
+__all__ = [
+ "BLOSC_CNAME",
+ "BLOSC_CODEC",
+ "BLOSC_CODEC_NAME",
+ "BLOSC_NO_SHUFFLE",
+ "BLOSC_SHUFFLE",
+ "BYTES_CODEC",
+ "BYTES_CODEC_NAME",
+ "CAST_OUT_OF_RANGE_MODE",
+ "CAST_ROUNDING_MODE",
+ "CAST_VALUE_CODEC",
+ "CAST_VALUE_CODEC_NAME",
+ "CRC32C_CODEC",
+ "CRC32C_CODEC_NAME",
+ "ENDIANNESS",
+ "GZIP_CODEC",
+ "GZIP_CODEC_NAME",
+ "SCALE_OFFSET_CODEC",
+ "SCALE_OFFSET_CODEC_NAME",
+ "SHARDING_INDEXED_CODEC",
+ "SHARDING_INDEXED_CODEC_NAME",
+ "SHARDING_INDEX_LOCATION",
+ "TRANSPOSE_CODEC",
+ "TRANSPOSE_CODEC_NAME",
+ "VLEN_BYTES_CODEC",
+ "VLEN_BYTES_CODEC_NAME",
+ "VLEN_UTF8_CODEC",
+ "VLEN_UTF8_CODEC_NAME",
+ "ZSTD_CODEC",
+ "ZSTD_CODEC_NAME",
+ "ZSTD_MAX_LEVEL",
+ "ZSTD_MIN_LEVEL",
+ "BloscCName",
+ "BloscCodecConfiguration",
+ "BloscCodecMetadata",
+ "BloscCodecName",
+ "BloscCodecObject",
+ "BloscShuffle",
+ "BytesCodecConfiguration",
+ "BytesCodecMetadata",
+ "BytesCodecName",
+ "BytesCodecObject",
+ "CastOutOfRangeMode",
+ "CastRoundingMode",
+ "CastValueCodecConfiguration",
+ "CastValueCodecMetadata",
+ "CastValueCodecName",
+ "CastValueCodecObject",
+ "Crc32cCodecMetadata",
+ "Crc32cCodecName",
+ "Crc32cCodecObject",
+ "Endianness",
+ "GzipCodecConfiguration",
+ "GzipCodecMetadata",
+ "GzipCodecName",
+ "GzipCodecObject",
+ "ScalarMap",
+ "ScalarMapEntry",
+ "ScaleOffsetCodecConfiguration",
+ "ScaleOffsetCodecMetadata",
+ "ScaleOffsetCodecName",
+ "ScaleOffsetCodecObject",
+ "ShardingIndexLocation",
+ "ShardingIndexedCodecConfiguration",
+ "ShardingIndexedCodecMetadata",
+ "ShardingIndexedCodecName",
+ "ShardingIndexedCodecObject",
+ "TransposeCodecConfiguration",
+ "TransposeCodecMetadata",
+ "TransposeCodecName",
+ "TransposeCodecObject",
+ "VlenBytesCodecMetadata",
+ "VlenBytesCodecName",
+ "VlenBytesCodecObject",
+ "VlenUtf8CodecMetadata",
+ "VlenUtf8CodecName",
+ "VlenUtf8CodecObject",
+ "ZstdCodecConfiguration",
+ "ZstdCodecMetadata",
+ "ZstdCodecName",
+ "ZstdCodecObject",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/codec/_vlen.py b/packages/zarr-metadata/src/zarr_metadata/v3/codec/_vlen.py
new file mode 100644
index 0000000000..7952bea056
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/codec/_vlen.py
@@ -0,0 +1,27 @@
+"""The rule that the variable-length codecs `vlen-utf8` and `vlen-bytes` share."""
+
+from collections.abc import Iterator
+
+from zarr_metadata._json import ValidationProblem, shown
+from zarr_metadata.definition._definition import AcceptedField
+from zarr_metadata.v3._definition import Chunk
+
+
+def vlen_problems(chunk: Chunk, compatible: frozenset[str]) -> Iterator[ValidationProblem]:
+ """Check that the chunk's data type is one of the `compatible` data types, named as their definitions name them.
+
+ Each value is written as its length followed by its bytes, and each
+ codec reads those bytes as the values of particular data types, so
+ `vlen-utf8` and `vlen-bytes` are not compatible with each other's data
+ type even though both vary in size. If the scope did not read the data
+ type, nothing is checked.
+ """
+ data_type = chunk.data_type
+ if not isinstance(data_type, AcceptedField) or data_type.definition.name in compatible:
+ return
+ expected = " or ".join(shown(name) for name in sorted(compatible))
+ yield ValidationProblem(
+ (),
+ f"expected a chunk of data type {expected}, got {shown(data_type.name)}",
+ "invalid_value",
+ )
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/codec/vlen_bytes.py b/packages/zarr-metadata/src/zarr_metadata/v3/codec/vlen_bytes.py
new file mode 100644
index 0000000000..0def142fdb
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/codec/vlen_bytes.py
@@ -0,0 +1,77 @@
+"""
+`vlen-bytes` codec types (zarr-extensions).
+
+See https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/codecs/vlen-bytes/README.md
+
+The codec has no configuration fields, so the `configuration` key is
+optional. The spec's example leaves it out, and zarr-python writes an
+empty object, `"configuration": {}`.
+"""
+
+from collections.abc import Iterator
+from typing import Final, Literal, NotRequired
+
+from typing_extensions import TypedDict
+
+from zarr_metadata._json import ValidationProblem
+from zarr_metadata.definition._definition import EmptyConfiguration, Nested
+from zarr_metadata.v3._definition import Chunk, CodecDefinition
+from zarr_metadata.v3.codec._vlen import vlen_problems
+from zarr_metadata.v3.data_type.bytes import BYTES_DATA_TYPE_NAME
+from zarr_metadata.v3.data_type.variable_length_bytes import VARIABLE_LENGTH_BYTES_DATA_TYPE_NAME
+
+VLEN_BYTES_CODEC_NAME: Final = "vlen-bytes"
+"""The `name` field value of the `vlen-bytes` codec."""
+
+VlenBytesCodecName = Literal["vlen-bytes"]
+"""Literal type of the `name` field of the `vlen-bytes` codec."""
+
+
+class VlenBytesCodecObject(TypedDict, closed=True):
+ """`vlen-bytes` codec metadata in object form. The codec has no configuration fields."""
+
+ name: VlenBytesCodecName
+ configuration: NotRequired[EmptyConfiguration]
+ must_understand: NotRequired[bool]
+
+
+VlenBytesCodecMetadata = VlenBytesCodecObject | VlenBytesCodecName
+"""Permitted JSON shapes for `vlen-bytes` codec metadata: the object form, or the bare name, since the codec has no configuration."""
+
+
+_COMPATIBLE: Final = frozenset({BYTES_DATA_TYPE_NAME, VARIABLE_LENGTH_BYTES_DATA_TYPE_NAME})
+
+
+def _chunk_rules(
+ configuration: EmptyConfiguration, nested: Nested, chunk: Chunk
+) -> Iterator[ValidationProblem]:
+ """Check that the data type is `bytes`, as the codec "is only compatible with the `"bytes"` data type" (https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/codecs/vlen-bytes/README.md?plain=1#L30).
+
+ zarr-python's `variable_length_bytes` holds the same values under
+ another name, so it is compatible too. Only `ZARR_PYTHON_CONTEXT` reads
+ that data type.
+ """
+ yield from vlen_problems(chunk, _COMPATIBLE)
+
+
+VLEN_BYTES_CODEC: Final = CodecDefinition(
+ name=VLEN_BYTES_CODEC_NAME,
+ configuration=EmptyConfiguration,
+ kind="array_bytes",
+ size="dynamic",
+ chunk_rules=_chunk_rules,
+)
+"""The `vlen-bytes` codec, which serializes variable-length byte strings.
+
+It is "only compatible with the `"bytes"` data type"
+(https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/codecs/vlen-bytes/README.md?plain=1#L30).
+The size of its output depends on the values, so it is dynamic.
+"""
+
+__all__ = [
+ "VLEN_BYTES_CODEC",
+ "VLEN_BYTES_CODEC_NAME",
+ "VlenBytesCodecMetadata",
+ "VlenBytesCodecName",
+ "VlenBytesCodecObject",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/codec/vlen_utf8.py b/packages/zarr-metadata/src/zarr_metadata/v3/codec/vlen_utf8.py
new file mode 100644
index 0000000000..1cd8ae69c0
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/codec/vlen_utf8.py
@@ -0,0 +1,71 @@
+"""
+`vlen-utf8` codec types (zarr-extensions).
+
+See https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/codecs/vlen-utf8/README.md
+
+The codec has no configuration fields, so the `configuration` key is
+optional. The spec's example leaves it out, and zarr-python writes an
+empty object, `"configuration": {}`.
+"""
+
+from collections.abc import Iterator
+from typing import Final, Literal, NotRequired
+
+from typing_extensions import TypedDict
+
+from zarr_metadata._json import ValidationProblem
+from zarr_metadata.definition._definition import EmptyConfiguration, Nested
+from zarr_metadata.v3._definition import Chunk, CodecDefinition
+from zarr_metadata.v3.codec._vlen import vlen_problems
+from zarr_metadata.v3.data_type.string import STRING_DATA_TYPE_NAME
+
+VLEN_UTF8_CODEC_NAME: Final = "vlen-utf8"
+"""The `name` field value of the `vlen-utf8` codec."""
+
+VlenUtf8CodecName = Literal["vlen-utf8"]
+"""Literal type of the `name` field of the `vlen-utf8` codec."""
+
+
+class VlenUtf8CodecObject(TypedDict, closed=True):
+ """`vlen-utf8` codec metadata in object form. The codec has no configuration fields."""
+
+ name: VlenUtf8CodecName
+ configuration: NotRequired[EmptyConfiguration]
+ must_understand: NotRequired[bool]
+
+
+VlenUtf8CodecMetadata = VlenUtf8CodecObject | VlenUtf8CodecName
+"""Permitted JSON shapes for `vlen-utf8` codec metadata: the object form, or the bare name, since the codec has no configuration."""
+
+
+_COMPATIBLE: Final = frozenset({STRING_DATA_TYPE_NAME})
+
+
+def _chunk_rules(
+ configuration: EmptyConfiguration, nested: Nested, chunk: Chunk
+) -> Iterator[ValidationProblem]:
+ """Check that the data type is `string`, as the codec "is only compatible with the `"string"` data type" (https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/codecs/vlen-utf8/README.md?plain=1#L30)."""
+ yield from vlen_problems(chunk, _COMPATIBLE)
+
+
+VLEN_UTF8_CODEC: Final = CodecDefinition(
+ name=VLEN_UTF8_CODEC_NAME,
+ configuration=EmptyConfiguration,
+ kind="array_bytes",
+ size="dynamic",
+ chunk_rules=_chunk_rules,
+)
+"""The `vlen-utf8` codec, which serializes variable-length UTF-8 strings.
+
+It is "only compatible with the `"string"` data type"
+(https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/codecs/vlen-utf8/README.md?plain=1#L30).
+The size of its output depends on the values, so it is dynamic.
+"""
+
+__all__ = [
+ "VLEN_UTF8_CODEC",
+ "VLEN_UTF8_CODEC_NAME",
+ "VlenUtf8CodecMetadata",
+ "VlenUtf8CodecName",
+ "VlenUtf8CodecObject",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/consolidated.py b/packages/zarr-metadata/src/zarr_metadata/v3/consolidated.py
index a3aa01afb9..d96aff7146 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v3/consolidated.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/consolidated.py
@@ -9,7 +9,7 @@
only its `{kind, must_understand, metadata}` envelope, which
`ZarrV3ConsolidatedMetadataEnvelopeJSON` types with any JSON as the
entries. `CONSOLIDATED_METADATA_EXTRA_FIELD` reads a group's member as
-that envelope, and `CORE` holds it. `ZarrV3ConsolidatedMetadataJSON` types
+that envelope, and `CORE_CONTEXT` holds it. `ZarrV3ConsolidatedMetadataJSON` types
the entries as the reference implementation writes them.
"""
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/data_type/__init__.py b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/__init__.py
index d1a193b2c4..528215df03 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v3/data_type/__init__.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/__init__.py
@@ -4,20 +4,342 @@
- Core primitives: `bool`, `int8`/`16`/`32`/`64`, `uint8`/`16`/`32`/`64`,
`float16`/`32`/`64`, `complex64`/`128`, `raw` (for `r`)
-- zarr-extensions: `bytes`, `string`, `numpy_datetime64`, `numpy_timedelta64`,
- `struct`
+- zarr-extensions: `bytes`, `string`, `fixed_length_utf32`,
+ `numpy_datetime64`, `numpy_timedelta64`, `struct`
+- zarr-python, registered by no specification: `null_terminated_bytes`,
+ `variable_length_bytes`, `zarr_python_raw_bytes` (for `raw_bytes`)
-Import a data type's types and its definition from its submodule:
+Import a data type's types and its definition from its submodule, or from
+this package, which re-exports every name its submodules export:
- `DataTypeName` -- the literal type of the dtype's `data_type` string
(or, for named-config dtypes, the literal value of their `name` field)
- `FillValue` -- the permitted JSON shape of the `fill_value` field
- the envelope and configuration TypedDicts of named-config dtypes
- (`numpy_datetime64`, `numpy_timedelta64`, `struct`), the branded
+ (`fixed_length_utf32`, `null_terminated_bytes`, `numpy_datetime64`,
+ `numpy_timedelta64`, `struct`, `raw_bytes`), the branded
`HexFloat` / `Base64Bytes` types, and their validator functions
- `_DATA_TYPE`, the definition
See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html
"""
-__all__: list[str] = []
+from zarr_metadata.v3.data_type.bool import (
+ BOOL_DATA_TYPE,
+ BOOL_DATA_TYPE_NAME,
+ BoolDataTypeName,
+ BoolFillValue,
+)
+from zarr_metadata.v3.data_type.bytes import (
+ BYTES_DATA_TYPE,
+ BYTES_DATA_TYPE_NAME,
+ Base64Bytes,
+ BytesDataTypeName,
+ BytesFillValue,
+ base64_bytes,
+)
+from zarr_metadata.v3.data_type.complex64 import (
+ COMPLEX64_DATA_TYPE,
+ COMPLEX64_DATA_TYPE_NAME,
+ Complex64Component,
+ Complex64DataTypeName,
+ Complex64FillValue,
+)
+from zarr_metadata.v3.data_type.complex128 import (
+ COMPLEX128_DATA_TYPE,
+ COMPLEX128_DATA_TYPE_NAME,
+ Complex128Component,
+ Complex128DataTypeName,
+ Complex128FillValue,
+)
+from zarr_metadata.v3.data_type.fixed_length_utf32 import (
+ FIXED_LENGTH_UTF32_DATA_TYPE,
+ FIXED_LENGTH_UTF32_DATA_TYPE_NAME,
+ FixedLengthUtf32,
+ FixedLengthUtf32Configuration,
+ FixedLengthUtf32DataTypeName,
+ FixedLengthUtf32FillValue,
+)
+from zarr_metadata.v3.data_type.float16 import (
+ CANONICAL_NAN_HEX_FLOAT16,
+ CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT16,
+ CANONICAL_POSITIVE_INFINITY_HEX_FLOAT16,
+ FLOAT16_DATA_TYPE,
+ FLOAT16_DATA_TYPE_NAME,
+ Float16DataTypeName,
+ Float16FillValue,
+ Float16SpecialFillValue,
+ HexFloat16,
+ hex_float16,
+)
+from zarr_metadata.v3.data_type.float32 import (
+ CANONICAL_NAN_HEX_FLOAT32,
+ CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT32,
+ CANONICAL_POSITIVE_INFINITY_HEX_FLOAT32,
+ FLOAT32_DATA_TYPE,
+ FLOAT32_DATA_TYPE_NAME,
+ Float32DataTypeName,
+ Float32FillValue,
+ Float32SpecialFillValue,
+ HexFloat32,
+ hex_float32,
+)
+from zarr_metadata.v3.data_type.float64 import (
+ CANONICAL_NAN_HEX_FLOAT64,
+ CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT64,
+ CANONICAL_POSITIVE_INFINITY_HEX_FLOAT64,
+ FLOAT64_DATA_TYPE,
+ FLOAT64_DATA_TYPE_NAME,
+ Float64DataTypeName,
+ Float64FillValue,
+ Float64SpecialFillValue,
+ HexFloat64,
+ hex_float64,
+)
+from zarr_metadata.v3.data_type.int8 import (
+ INT8_DATA_TYPE,
+ INT8_DATA_TYPE_NAME,
+ Int8DataTypeName,
+ Int8FillValue,
+)
+from zarr_metadata.v3.data_type.int16 import (
+ INT16_DATA_TYPE,
+ INT16_DATA_TYPE_NAME,
+ Int16DataTypeName,
+ Int16FillValue,
+)
+from zarr_metadata.v3.data_type.int32 import (
+ INT32_DATA_TYPE,
+ INT32_DATA_TYPE_NAME,
+ Int32DataTypeName,
+ Int32FillValue,
+)
+from zarr_metadata.v3.data_type.int64 import (
+ INT64_DATA_TYPE,
+ INT64_DATA_TYPE_NAME,
+ Int64DataTypeName,
+ Int64FillValue,
+)
+from zarr_metadata.v3.data_type.null_terminated_bytes import (
+ NULL_TERMINATED_BYTES_DATA_TYPE,
+ NULL_TERMINATED_BYTES_DATA_TYPE_NAME,
+ NullTerminatedBytes,
+ NullTerminatedBytesConfiguration,
+ NullTerminatedBytesDataTypeName,
+ NullTerminatedBytesFillValue,
+)
+from zarr_metadata.v3.data_type.numpy_datetime64 import (
+ NUMPY_DATETIME64_DATA_TYPE,
+ NUMPY_DATETIME64_DATA_TYPE_NAME,
+ NUMPY_TIME_UNIT,
+ NumpyDatetime64,
+ NumpyDatetime64Configuration,
+ NumpyDatetime64DataTypeName,
+ NumpyDatetime64FillValue,
+ NumpyTimeUnit,
+)
+from zarr_metadata.v3.data_type.numpy_timedelta64 import (
+ NUMPY_TIMEDELTA64_DATA_TYPE,
+ NUMPY_TIMEDELTA64_DATA_TYPE_NAME,
+ NumpyTimedelta64,
+ NumpyTimedelta64Configuration,
+ NumpyTimedelta64DataTypeName,
+ NumpyTimedelta64FillValue,
+)
+from zarr_metadata.v3.data_type.raw import (
+ RAW_BYTES_DATA_TYPE,
+ RawBytesConfiguration,
+ RawBytesDataTypeName,
+ RawBytesFillValue,
+ raw_bytes_dtype_name,
+)
+from zarr_metadata.v3.data_type.string import (
+ STRING_DATA_TYPE,
+ STRING_DATA_TYPE_NAME,
+ StringDataTypeName,
+ StringFillValue,
+)
+from zarr_metadata.v3.data_type.struct import (
+ STRUCT_DATA_TYPE,
+ STRUCT_DATA_TYPE_NAME,
+ Struct,
+ StructConfiguration,
+ StructDataTypeName,
+ StructField,
+ StructFillValue,
+)
+from zarr_metadata.v3.data_type.uint8 import (
+ UINT8_DATA_TYPE,
+ UINT8_DATA_TYPE_NAME,
+ Uint8DataTypeName,
+ Uint8FillValue,
+)
+from zarr_metadata.v3.data_type.uint16 import (
+ UINT16_DATA_TYPE,
+ UINT16_DATA_TYPE_NAME,
+ Uint16DataTypeName,
+ Uint16FillValue,
+)
+from zarr_metadata.v3.data_type.uint32 import (
+ UINT32_DATA_TYPE,
+ UINT32_DATA_TYPE_NAME,
+ Uint32DataTypeName,
+ Uint32FillValue,
+)
+from zarr_metadata.v3.data_type.uint64 import (
+ UINT64_DATA_TYPE,
+ UINT64_DATA_TYPE_NAME,
+ Uint64DataTypeName,
+ Uint64FillValue,
+)
+from zarr_metadata.v3.data_type.variable_length_bytes import (
+ VARIABLE_LENGTH_BYTES_DATA_TYPE,
+ VARIABLE_LENGTH_BYTES_DATA_TYPE_NAME,
+ VariableLengthBytesDataTypeName,
+ VariableLengthBytesFillValue,
+)
+from zarr_metadata.v3.data_type.zarr_python_raw_bytes import (
+ ZARR_PYTHON_RAW_BYTES_DATA_TYPE,
+ ZARR_PYTHON_RAW_BYTES_DATA_TYPE_NAME,
+ ZarrPythonRawBytes,
+ ZarrPythonRawBytesConfiguration,
+ ZarrPythonRawBytesDataTypeName,
+ ZarrPythonRawBytesFillValue,
+)
+
+__all__ = [
+ "BOOL_DATA_TYPE",
+ "BOOL_DATA_TYPE_NAME",
+ "BYTES_DATA_TYPE",
+ "BYTES_DATA_TYPE_NAME",
+ "CANONICAL_NAN_HEX_FLOAT16",
+ "CANONICAL_NAN_HEX_FLOAT32",
+ "CANONICAL_NAN_HEX_FLOAT64",
+ "CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT16",
+ "CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT32",
+ "CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT64",
+ "CANONICAL_POSITIVE_INFINITY_HEX_FLOAT16",
+ "CANONICAL_POSITIVE_INFINITY_HEX_FLOAT32",
+ "CANONICAL_POSITIVE_INFINITY_HEX_FLOAT64",
+ "COMPLEX64_DATA_TYPE",
+ "COMPLEX64_DATA_TYPE_NAME",
+ "COMPLEX128_DATA_TYPE",
+ "COMPLEX128_DATA_TYPE_NAME",
+ "FIXED_LENGTH_UTF32_DATA_TYPE",
+ "FIXED_LENGTH_UTF32_DATA_TYPE_NAME",
+ "FLOAT16_DATA_TYPE",
+ "FLOAT16_DATA_TYPE_NAME",
+ "FLOAT32_DATA_TYPE",
+ "FLOAT32_DATA_TYPE_NAME",
+ "FLOAT64_DATA_TYPE",
+ "FLOAT64_DATA_TYPE_NAME",
+ "INT8_DATA_TYPE",
+ "INT8_DATA_TYPE_NAME",
+ "INT16_DATA_TYPE",
+ "INT16_DATA_TYPE_NAME",
+ "INT32_DATA_TYPE",
+ "INT32_DATA_TYPE_NAME",
+ "INT64_DATA_TYPE",
+ "INT64_DATA_TYPE_NAME",
+ "NULL_TERMINATED_BYTES_DATA_TYPE",
+ "NULL_TERMINATED_BYTES_DATA_TYPE_NAME",
+ "NUMPY_DATETIME64_DATA_TYPE",
+ "NUMPY_DATETIME64_DATA_TYPE_NAME",
+ "NUMPY_TIMEDELTA64_DATA_TYPE",
+ "NUMPY_TIMEDELTA64_DATA_TYPE_NAME",
+ "NUMPY_TIME_UNIT",
+ "RAW_BYTES_DATA_TYPE",
+ "STRING_DATA_TYPE",
+ "STRING_DATA_TYPE_NAME",
+ "STRUCT_DATA_TYPE",
+ "STRUCT_DATA_TYPE_NAME",
+ "UINT8_DATA_TYPE",
+ "UINT8_DATA_TYPE_NAME",
+ "UINT16_DATA_TYPE",
+ "UINT16_DATA_TYPE_NAME",
+ "UINT32_DATA_TYPE",
+ "UINT32_DATA_TYPE_NAME",
+ "UINT64_DATA_TYPE",
+ "UINT64_DATA_TYPE_NAME",
+ "VARIABLE_LENGTH_BYTES_DATA_TYPE",
+ "VARIABLE_LENGTH_BYTES_DATA_TYPE_NAME",
+ "ZARR_PYTHON_RAW_BYTES_DATA_TYPE",
+ "ZARR_PYTHON_RAW_BYTES_DATA_TYPE_NAME",
+ "Base64Bytes",
+ "BoolDataTypeName",
+ "BoolFillValue",
+ "BytesDataTypeName",
+ "BytesFillValue",
+ "Complex64Component",
+ "Complex64DataTypeName",
+ "Complex64FillValue",
+ "Complex128Component",
+ "Complex128DataTypeName",
+ "Complex128FillValue",
+ "FixedLengthUtf32",
+ "FixedLengthUtf32Configuration",
+ "FixedLengthUtf32DataTypeName",
+ "FixedLengthUtf32FillValue",
+ "Float16DataTypeName",
+ "Float16FillValue",
+ "Float16SpecialFillValue",
+ "Float32DataTypeName",
+ "Float32FillValue",
+ "Float32SpecialFillValue",
+ "Float64DataTypeName",
+ "Float64FillValue",
+ "Float64SpecialFillValue",
+ "HexFloat16",
+ "HexFloat32",
+ "HexFloat64",
+ "Int8DataTypeName",
+ "Int8FillValue",
+ "Int16DataTypeName",
+ "Int16FillValue",
+ "Int32DataTypeName",
+ "Int32FillValue",
+ "Int64DataTypeName",
+ "Int64FillValue",
+ "NullTerminatedBytes",
+ "NullTerminatedBytesConfiguration",
+ "NullTerminatedBytesDataTypeName",
+ "NullTerminatedBytesFillValue",
+ "NumpyDatetime64",
+ "NumpyDatetime64Configuration",
+ "NumpyDatetime64DataTypeName",
+ "NumpyDatetime64FillValue",
+ "NumpyTimeUnit",
+ "NumpyTimedelta64",
+ "NumpyTimedelta64Configuration",
+ "NumpyTimedelta64DataTypeName",
+ "NumpyTimedelta64FillValue",
+ "RawBytesConfiguration",
+ "RawBytesDataTypeName",
+ "RawBytesFillValue",
+ "StringDataTypeName",
+ "StringFillValue",
+ "Struct",
+ "StructConfiguration",
+ "StructDataTypeName",
+ "StructField",
+ "StructFillValue",
+ "Uint8DataTypeName",
+ "Uint8FillValue",
+ "Uint16DataTypeName",
+ "Uint16FillValue",
+ "Uint32DataTypeName",
+ "Uint32FillValue",
+ "Uint64DataTypeName",
+ "Uint64FillValue",
+ "VariableLengthBytesDataTypeName",
+ "VariableLengthBytesFillValue",
+ "ZarrPythonRawBytes",
+ "ZarrPythonRawBytesConfiguration",
+ "ZarrPythonRawBytesDataTypeName",
+ "ZarrPythonRawBytesFillValue",
+ "base64_bytes",
+ "hex_float16",
+ "hex_float32",
+ "hex_float64",
+ "raw_bytes_dtype_name",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/data_type/_byte.py b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/_byte.py
index 07de456e22..cfc7adddec 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v3/data_type/_byte.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/_byte.py
@@ -1,15 +1,44 @@
-"""A byte value, as used in the fill values of raw bits and of `bytes`.
+"""A byte value, as used in the fill values of raw bits and of `bytes`, and the rules of a fill value written in base64.
-Each is an integer in `[0, 255]`
+Each byte value is an integer in `[0, 255]`
(https://github.com/zarr-developers/zarr-specs/blob/fc7dd9c9beb5a50b87f9b08b00bf50fc0048482f/docs/v3/data-types/index.rst#L97-L99;
the reference for the `bytes` type is https://github.com/zarr-developers/zarr-extensions/blob/4da7b37a84f76e660902f6d3de3eaef0e0febae6/data-types/bytes/README.md?plain=1#L8).
"""
+import base64
+from collections.abc import Iterator
from typing import Annotated
from annotated_types import Interval
+from zarr_metadata._json import ValidationProblem, shown
+from zarr_metadata._numpy import base64_bytes
+
ByteValue = Annotated[int, Interval(ge=0, le=255)]
"""One byte of a fill value: a JSON integer in `[0, 255]`."""
-__all__ = ["ByteValue"]
+
+def base64_problems(value: str) -> Iterator[ValidationProblem]:
+ """Check that a fill value written as a string is standard-alphabet base64."""
+ try:
+ base64_bytes(value)
+ except ValueError:
+ yield ValidationProblem(
+ (), f"expected standard-alphabet base64, got {shown(value)}", "invalid_value"
+ )
+
+
+def base64_decoded(value: str) -> bytes | None:
+ """The bytes that `value` encodes in standard-alphabet base64; None when it is not such base64."""
+ try:
+ return base64.b64decode(base64_bytes(value))
+ except ValueError:
+ return None
+
+
+def base64_spelling(data: bytes) -> str:
+ """`data` in the single spelling of a base64 fill value, standard-alphabet base64: `"QR=="` decodes to the byte that `"QQ=="` spells."""
+ return base64.b64encode(data).decode("ascii")
+
+
+__all__ = ["ByteValue", "base64_decoded", "base64_problems", "base64_spelling"]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/data_type/bytes.py b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/bytes.py
index 1f6dc1b7f0..b92facd4af 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v3/data_type/bytes.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/bytes.py
@@ -8,7 +8,7 @@
from collections.abc import Iterator
from typing import Final, Literal
-from zarr_metadata._json import ValidationProblem, shown
+from zarr_metadata._json import ValidationProblem
from zarr_metadata._numpy import Base64Bytes, base64_bytes
from zarr_metadata.definition._definition import (
EmptyConfiguration,
@@ -18,7 +18,7 @@
DataTypeDefinition,
variable_length,
)
-from zarr_metadata.v3.data_type._byte import ByteValue
+from zarr_metadata.v3.data_type._byte import ByteValue, base64_problems, base64_spelling
BYTES_DATA_TYPE_NAME: Final = "bytes"
"""The `data_type` value for the variable-length `bytes` type."""
@@ -38,14 +38,8 @@ def _fill_value_rules(
configuration: EmptyConfiguration, nested: Nested, value: BytesFillValue
) -> Iterator[ValidationProblem]:
"""Check that a string fill value is standard-alphabet base64. A fill value written as byte values is not checked here."""
- if not isinstance(value, str):
- return
- try:
- base64_bytes(value)
- except ValueError:
- yield ValidationProblem(
- (), f"expected standard-alphabet base64, got {shown(value)}", "invalid_value"
- )
+ if isinstance(value, str):
+ yield from base64_problems(value)
def _fill_value_canonical(
@@ -59,7 +53,7 @@ def _fill_value_canonical(
to the same byte as `"QQ=="`.
"""
data = base64.b64decode(value) if isinstance(value, str) else bytes(value)
- return base64.b64encode(data).decode("ascii")
+ return base64_spelling(data)
BYTES_DATA_TYPE: Final = DataTypeDefinition(
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/data_type/fixed_length_utf32.py b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/fixed_length_utf32.py
new file mode 100644
index 0000000000..234cb792b8
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/fixed_length_utf32.py
@@ -0,0 +1,124 @@
+"""
+Zarr `fixed_length_utf32` data type (fixed-length UTF-32 strings, zarr-extensions).
+
+See https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/data-types/fixed_length_utf32/README.md
+"""
+
+from collections.abc import Iterator
+from typing import Final, Literal, NotRequired
+
+from typing_extensions import ReadOnly, TypedDict
+
+from zarr_metadata._json import ValidationProblem
+from zarr_metadata.definition._definition import Nested
+from zarr_metadata.v3._definition import DataTypeDefinition, multi_byte
+
+FIXED_LENGTH_UTF32_DATA_TYPE_NAME: Final = "fixed_length_utf32"
+"""The `name` field value of the `fixed_length_utf32` data type."""
+
+FixedLengthUtf32DataTypeName = Literal["fixed_length_utf32"]
+"""Literal type of the `name` field of the `fixed_length_utf32` data type."""
+
+_CODE_POINT_BYTES: Final = 4
+
+
+class FixedLengthUtf32Configuration(TypedDict, closed=True):
+ """
+ Configuration for the `fixed_length_utf32` data type.
+
+ Attributes
+ ----------
+ length_bytes
+ The size in bytes of every value: a positive multiple of 4, since
+ each code point takes 4 bytes.
+ """
+
+ length_bytes: ReadOnly[int]
+
+
+class FixedLengthUtf32(TypedDict, closed=True):
+ """`fixed_length_utf32` data type metadata."""
+
+ name: FixedLengthUtf32DataTypeName
+ configuration: FixedLengthUtf32Configuration
+ must_understand: NotRequired[bool]
+
+
+FixedLengthUtf32FillValue = str
+"""Permitted JSON shape of the `fill_value` field for `fixed_length_utf32`: a JSON string of at most `length_bytes / 4` code points."""
+
+
+def _rules(
+ configuration: FixedLengthUtf32Configuration, nested: Nested
+) -> Iterator[ValidationProblem]:
+ """Check that `length_bytes` is a "positive integer, multiple of `4`" (https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/data-types/fixed_length_utf32/README.md?plain=1#L41)."""
+ length_bytes = configuration["length_bytes"]
+ if length_bytes <= 0 or length_bytes % _CODE_POINT_BYTES != 0:
+ yield ValidationProblem(
+ ("length_bytes",),
+ f"expected a positive multiple of {_CODE_POINT_BYTES}, got {length_bytes}",
+ "invalid_value",
+ )
+
+
+def _fill_value_rules(
+ configuration: FixedLengthUtf32Configuration, nested: Nested, value: FixedLengthUtf32FillValue
+) -> Iterator[ValidationProblem]:
+ """Check that the fill value fits: "a JSON string of at most `length_bytes / 4` code points" (https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/data-types/fixed_length_utf32/README.md?plain=1#L124-L126).
+
+ Trailing `U+0000` code points count, although `_fill_value_canonical`
+ removes them: a string "with more than `length_bytes / 4` code points
+ is invalid and MUST be rejected"
+ (https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/data-types/fixed_length_utf32/README.md?plain=1#L112-L113).
+ `null_terminated_bytes`, which no specification defines, follows
+ zarr-python instead, which drops trailing null bytes before it checks
+ the size.
+ """
+ capacity = configuration["length_bytes"] // _CODE_POINT_BYTES
+ if len(value) > capacity:
+ yield ValidationProblem(
+ (),
+ f"expected at most {capacity} code points, got {len(value)}",
+ "invalid_value",
+ )
+
+
+def _fill_value_canonical(
+ configuration: FixedLengthUtf32Configuration, nested: Nested, value: FixedLengthUtf32FillValue
+) -> str:
+ """Return the string without its trailing `U+0000` code points.
+
+ A shorter string "decodes to the scalar whose remaining code points are
+ `U+0000` padding", and a scalar is encoded "with any trailing `U+0000`
+ padding code points removed"
+ (https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/data-types/fixed_length_utf32/README.md?plain=1#L107-L116),
+ so `"a"` and `"a\\u0000"` are the same value.
+ """
+ return value.rstrip("\x00")
+
+
+FIXED_LENGTH_UTF32_DATA_TYPE: Final = DataTypeDefinition(
+ name=FIXED_LENGTH_UTF32_DATA_TYPE_NAME,
+ configuration=FixedLengthUtf32Configuration,
+ rules=_rules,
+ fill_value=FixedLengthUtf32FillValue,
+ fill_value_rules=_fill_value_rules,
+ fill_value_canonical=_fill_value_canonical,
+ storage=multi_byte,
+)
+"""The `fixed_length_utf32` data type: NumPy's `U`.
+
+Each code point is a 4-byte UTF-32 code unit, so the `bytes` codec "MUST
+be configured with an explicit `endian` setting"
+(https://github.com/zarr-developers/zarr-extensions/blob/cadd1dab6c9aebafb66080b78b583f6507466b3d/data-types/fixed_length_utf32/README.md?plain=1#L100-L103).
+"""
+
+
+__all__ = [
+ "FIXED_LENGTH_UTF32_DATA_TYPE",
+ "FIXED_LENGTH_UTF32_DATA_TYPE_NAME",
+ "FixedLengthUtf32",
+ "FixedLengthUtf32Configuration",
+ "FixedLengthUtf32DataTypeName",
+ "FixedLengthUtf32FillValue",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/data_type/null_terminated_bytes.py b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/null_terminated_bytes.py
new file mode 100644
index 0000000000..8dc9d34fa9
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/null_terminated_bytes.py
@@ -0,0 +1,121 @@
+"""
+zarr-python's `null_terminated_bytes` data type (fixed-length, null-padded bytes).
+
+No specification registers this data type. zarr-python writes it for
+NumPy's `S` data types, and warns that its spelling may change
+(https://github.com/zarr-developers/zarr-python/blob/a2f2415c86f7d35212ae8c0d5dc5e4a22a6c6191/src/zarr/core/dtype/npy/bytes.py#L385-L390).
+This module describes what zarr-python writes. Its definition is in the
+`ZARR_PYTHON_CONTEXT` scope, not in `CORE_AND_EXTENSIONS_CONTEXT`.
+"""
+
+import base64
+from collections.abc import Iterator
+from typing import Final, Literal, NotRequired
+
+from typing_extensions import ReadOnly, TypedDict
+
+from zarr_metadata._json import ValidationProblem
+from zarr_metadata._numpy import Base64Bytes
+from zarr_metadata.definition._definition import Nested
+from zarr_metadata.v3._definition import DataTypeDefinition, single_byte
+from zarr_metadata.v3.data_type._byte import base64_decoded, base64_problems, base64_spelling
+
+NULL_TERMINATED_BYTES_DATA_TYPE_NAME: Final = "null_terminated_bytes"
+"""The `name` field value of the `null_terminated_bytes` data type."""
+
+NullTerminatedBytesDataTypeName = Literal["null_terminated_bytes"]
+"""Literal type of the `name` field of the `null_terminated_bytes` data type."""
+
+
+class NullTerminatedBytesConfiguration(TypedDict, closed=True):
+ """
+ Configuration for the `null_terminated_bytes` data type.
+
+ Attributes
+ ----------
+ length_bytes
+ The size in bytes of every value, at least 1.
+ """
+
+ length_bytes: ReadOnly[int]
+
+
+class NullTerminatedBytes(TypedDict, closed=True):
+ """`null_terminated_bytes` data type metadata."""
+
+ name: NullTerminatedBytesDataTypeName
+ configuration: NullTerminatedBytesConfiguration
+ must_understand: NotRequired[bool]
+
+
+NullTerminatedBytesFillValue = Base64Bytes
+"""Permitted JSON shape of the `fill_value` field for `null_terminated_bytes`: the bytes, without their trailing null bytes, as a `Base64Bytes` string."""
+
+
+def _rules(
+ configuration: NullTerminatedBytesConfiguration, nested: Nested
+) -> Iterator[ValidationProblem]:
+ """Check that `length_bytes` is positive. zarr-python refuses a length below 1, since such a type holds no data."""
+ length_bytes = configuration["length_bytes"]
+ if length_bytes < 1:
+ yield ValidationProblem(
+ ("length_bytes",), f"expected a positive size, got {length_bytes}", "invalid_value"
+ )
+
+
+def _fill_value_rules(
+ configuration: NullTerminatedBytesConfiguration,
+ nested: Nested,
+ value: NullTerminatedBytesFillValue,
+) -> Iterator[ValidationProblem]:
+ """Check that the fill value is base64, and that the bytes it encodes fit in `length_bytes`.
+
+ Trailing null bytes are padding, as in `_fill_value_canonical`, so they
+ do not count: `"QQAA"` fits in one byte, as `"QQ=="` does.
+ """
+ yield from base64_problems(value)
+ data = base64_decoded(value)
+ if data is None:
+ return
+ size = len(data.rstrip(b"\x00"))
+ if size > configuration["length_bytes"]:
+ yield ValidationProblem(
+ (),
+ f"expected at most {configuration['length_bytes']} bytes, got {size}",
+ "invalid_value",
+ )
+
+
+def _fill_value_canonical(
+ configuration: NullTerminatedBytesConfiguration,
+ nested: Nested,
+ value: NullTerminatedBytesFillValue,
+) -> str:
+ """Return the bytes without their trailing null bytes, encoded in base64 again.
+
+ A value is padded with null bytes to `length_bytes`, so `"QQ=="` and
+ `"QQAA"` are the same value, as are `"QR=="` and `"QQ=="`.
+ """
+ return base64_spelling(base64.b64decode(value).rstrip(b"\x00"))
+
+
+NULL_TERMINATED_BYTES_DATA_TYPE: Final = DataTypeDefinition(
+ name=NULL_TERMINATED_BYTES_DATA_TYPE_NAME,
+ configuration=NullTerminatedBytesConfiguration,
+ rules=_rules,
+ fill_value=NullTerminatedBytesFillValue,
+ fill_value_rules=_fill_value_rules,
+ fill_value_canonical=_fill_value_canonical,
+ storage=single_byte,
+)
+"""zarr-python's `null_terminated_bytes` data type: NumPy's `S`. Each value is `length_bytes` single bytes."""
+
+
+__all__ = [
+ "NULL_TERMINATED_BYTES_DATA_TYPE",
+ "NULL_TERMINATED_BYTES_DATA_TYPE_NAME",
+ "NullTerminatedBytes",
+ "NullTerminatedBytesConfiguration",
+ "NullTerminatedBytesDataTypeName",
+ "NullTerminatedBytesFillValue",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/data_type/variable_length_bytes.py b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/variable_length_bytes.py
new file mode 100644
index 0000000000..28803e9d80
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/variable_length_bytes.py
@@ -0,0 +1,62 @@
+"""
+zarr-python's `variable_length_bytes` data type (variable-length raw bytes).
+
+No specification registers this name. zarr-python writes it for arrays of
+Python `bytes` objects, and warns that its spelling may change
+(https://github.com/zarr-developers/zarr-python/blob/a2f2415c86f7d35212ae8c0d5dc5e4a22a6c6191/src/zarr/core/dtype/npy/bytes.py#L1141-L1143).
+zarr-extensions registers the same values as `bytes`. This module describes
+what zarr-python writes. Its definition is in the `ZARR_PYTHON_CONTEXT` scope, not
+in `CORE_AND_EXTENSIONS_CONTEXT`.
+"""
+
+import base64
+from collections.abc import Iterator
+from typing import Final, Literal
+
+from zarr_metadata._json import ValidationProblem
+from zarr_metadata._numpy import Base64Bytes
+from zarr_metadata.definition._definition import EmptyConfiguration, Nested
+from zarr_metadata.v3._definition import DataTypeDefinition, variable_length
+from zarr_metadata.v3.data_type._byte import base64_problems, base64_spelling
+
+VARIABLE_LENGTH_BYTES_DATA_TYPE_NAME: Final = "variable_length_bytes"
+"""The `data_type` value for the `variable_length_bytes` type."""
+
+VariableLengthBytesDataTypeName = Literal["variable_length_bytes"]
+"""Literal type of the `data_type` field for `variable_length_bytes`."""
+
+VariableLengthBytesFillValue = Base64Bytes
+"""Permitted JSON shape of the `fill_value` field for `variable_length_bytes`: a `Base64Bytes` string, which is what zarr-python writes and reads."""
+
+
+def _fill_value_rules(
+ configuration: EmptyConfiguration, nested: Nested, value: VariableLengthBytesFillValue
+) -> Iterator[ValidationProblem]:
+ """Check that the fill value is standard-alphabet base64."""
+ yield from base64_problems(value)
+
+
+def _fill_value_canonical(
+ configuration: EmptyConfiguration, nested: Nested, value: VariableLengthBytesFillValue
+) -> str:
+ """Return the bytes encoded in base64 again, so `"QR=="` becomes `"QQ=="`."""
+ return base64_spelling(base64.b64decode(value))
+
+
+VARIABLE_LENGTH_BYTES_DATA_TYPE: Final = DataTypeDefinition(
+ name=VARIABLE_LENGTH_BYTES_DATA_TYPE_NAME,
+ configuration=EmptyConfiguration,
+ fill_value=VariableLengthBytesFillValue,
+ fill_value_rules=_fill_value_rules,
+ fill_value_canonical=_fill_value_canonical,
+ storage=variable_length,
+)
+"""zarr-python's `variable_length_bytes` data type. It is written as a bare name and has no configuration, and each value is stored in as many bytes as it needs."""
+
+
+__all__ = [
+ "VARIABLE_LENGTH_BYTES_DATA_TYPE",
+ "VARIABLE_LENGTH_BYTES_DATA_TYPE_NAME",
+ "VariableLengthBytesDataTypeName",
+ "VariableLengthBytesFillValue",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/data_type/zarr_python_raw_bytes.py b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/zarr_python_raw_bytes.py
new file mode 100644
index 0000000000..7f8a7e6c63
--- /dev/null
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/data_type/zarr_python_raw_bytes.py
@@ -0,0 +1,123 @@
+"""
+zarr-python's `raw_bytes` data type (fixed-length raw bytes).
+
+No specification registers this data type. zarr-python writes it for
+NumPy's `V` data types without fields, and warns that its spelling may
+change
+(https://github.com/zarr-developers/zarr-python/blob/a2f2415c86f7d35212ae8c0d5dc5e4a22a6c6191/src/zarr/core/dtype/npy/bytes.py#L783-L785).
+This module describes what zarr-python writes. Its definition is in the
+`ZARR_PYTHON_CONTEXT` scope, not in `CORE_AND_EXTENSIONS_CONTEXT`.
+
+Its names start with `ZarrPython` because `RawBytes` names the spec's raw
+bits, `r*`, in `zarr_metadata.v3.data_type.raw`.
+"""
+
+import base64
+from collections.abc import Iterator
+from typing import Final, Literal, NotRequired
+
+from typing_extensions import ReadOnly, TypedDict
+
+from zarr_metadata._json import ValidationProblem
+from zarr_metadata._numpy import Base64Bytes
+from zarr_metadata.definition._definition import Nested
+from zarr_metadata.v3._definition import DataTypeDefinition, single_byte
+from zarr_metadata.v3.data_type._byte import base64_decoded, base64_problems, base64_spelling
+
+ZARR_PYTHON_RAW_BYTES_DATA_TYPE_NAME: Final = "raw_bytes"
+"""The `name` field value of zarr-python's `raw_bytes` data type."""
+
+ZarrPythonRawBytesDataTypeName = Literal["raw_bytes"]
+"""Literal type of the `name` field of zarr-python's `raw_bytes` data type."""
+
+
+class ZarrPythonRawBytesConfiguration(TypedDict, closed=True):
+ """
+ Configuration for zarr-python's `raw_bytes` data type.
+
+ Attributes
+ ----------
+ length_bytes
+ The size in bytes of every value, at least 1.
+ """
+
+ length_bytes: ReadOnly[int]
+
+
+class ZarrPythonRawBytes(TypedDict, closed=True):
+ """zarr-python's `raw_bytes` data type metadata."""
+
+ name: ZarrPythonRawBytesDataTypeName
+ configuration: ZarrPythonRawBytesConfiguration
+ must_understand: NotRequired[bool]
+
+
+ZarrPythonRawBytesFillValue = Base64Bytes
+"""Permitted JSON shape of the `fill_value` field for zarr-python's `raw_bytes`: the bytes as a `Base64Bytes` string, which zarr-python writes `length_bytes` long."""
+
+
+def _rules(
+ configuration: ZarrPythonRawBytesConfiguration, nested: Nested
+) -> Iterator[ValidationProblem]:
+ """Check that `length_bytes` is positive. zarr-python refuses a length below 1, since such a type holds no data."""
+ length_bytes = configuration["length_bytes"]
+ if length_bytes < 1:
+ yield ValidationProblem(
+ ("length_bytes",), f"expected a positive size, got {length_bytes}", "invalid_value"
+ )
+
+
+def _fill_value_rules(
+ configuration: ZarrPythonRawBytesConfiguration,
+ nested: Nested,
+ value: ZarrPythonRawBytesFillValue,
+) -> Iterator[ValidationProblem]:
+ """Check that the fill value is base64, and that the bytes it encodes fit in `length_bytes`.
+
+ zarr-python cuts a longer value to `length_bytes` without a word, which
+ loses the bytes past the end, so a longer value is a problem.
+ """
+ yield from base64_problems(value)
+ data = base64_decoded(value)
+ if data is not None and len(data) > configuration["length_bytes"]:
+ yield ValidationProblem(
+ (),
+ f"expected at most {configuration['length_bytes']} bytes, got {len(data)}",
+ "invalid_value",
+ )
+
+
+def _fill_value_canonical(
+ configuration: ZarrPythonRawBytesConfiguration,
+ nested: Nested,
+ value: ZarrPythonRawBytesFillValue,
+) -> str:
+ """Return the bytes padded with null bytes to `length_bytes`, encoded in base64 again, as zarr-python writes them.
+
+ zarr-python pads a shorter value with null bytes, so `"QQ=="` and
+ `"QQAAAA=="` are the same value in a type of 4 bytes.
+ """
+ data = base64.b64decode(value).ljust(configuration["length_bytes"], b"\x00")
+ return base64_spelling(data)
+
+
+ZARR_PYTHON_RAW_BYTES_DATA_TYPE: Final = DataTypeDefinition(
+ name=ZARR_PYTHON_RAW_BYTES_DATA_TYPE_NAME,
+ configuration=ZarrPythonRawBytesConfiguration,
+ rules=_rules,
+ fill_value=ZarrPythonRawBytesFillValue,
+ fill_value_rules=_fill_value_rules,
+ fill_value_canonical=_fill_value_canonical,
+ storage=single_byte,
+)
+"""zarr-python's `raw_bytes` data type: NumPy's `V` without fields. Each value is `length_bytes` single bytes."""
+
+
+__all__ = [
+ "ZARR_PYTHON_RAW_BYTES_DATA_TYPE",
+ "ZARR_PYTHON_RAW_BYTES_DATA_TYPE_NAME",
+ "ZarrPythonRawBytes",
+ "ZarrPythonRawBytesConfiguration",
+ "ZarrPythonRawBytesDataTypeName",
+ "ZarrPythonRawBytesFillValue",
+]
diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/definition.py b/packages/zarr-metadata/src/zarr_metadata/v3/definition.py
index 90cb1e466e..0f2c236b9e 100644
--- a/packages/zarr-metadata/src/zarr_metadata/v3/definition.py
+++ b/packages/zarr-metadata/src/zarr_metadata/v3/definition.py
@@ -11,8 +11,8 @@
which both formats share. `ZarrV3Context` is the v3 scope, with one slot
for each kind: `data_types`, `chunk_grids`, `chunk_key_encodings`,
`codecs`, `storage_transformers`, `array_extra_fields` and
-`group_extra_fields`. `CORE` and `CORE_AND_EXTENSIONS` are the v3 scopes
-this package provides.
+`group_extra_fields`. `CORE_CONTEXT`, `CORE_AND_EXTENSIONS_CONTEXT` and `ZARR_PYTHON_CONTEXT` are
+the v3 scopes this package provides.
A scope reads whole documents as well as fields.
`read_array_metadata_v3(document, context=CONTEXT)`, from
@@ -93,6 +93,9 @@
`zarr.json`, as a JSON Schema.
"""
+from typing import TYPE_CHECKING
+
+from zarr_metadata._renamed import renamed_scope
from zarr_metadata.v3._common import (
ZarrV3ChunkGridField,
ZarrV3ChunkKeyEncodingField,
@@ -102,7 +105,12 @@
ZarrV3StorageTransformerField,
)
from zarr_metadata.v3._context import ZarrV3Context, ZarrV3ContextSlots, ZarrV3Kind
-from zarr_metadata.v3._core import CORE, CORE_AND_EXTENSIONS
+from zarr_metadata.v3._core import (
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
+ RENAMED_SCOPES,
+ ZARR_PYTHON_CONTEXT,
+)
from zarr_metadata.v3._definition import (
ArrayExtraFieldDefinition,
Chunk,
@@ -122,8 +130,9 @@
from zarr_metadata.v3._pipeline import Stage
__all__ = [
- "CORE",
- "CORE_AND_EXTENSIONS",
+ "CORE_AND_EXTENSIONS_CONTEXT",
+ "CORE_CONTEXT",
+ "ZARR_PYTHON_CONTEXT",
"ArrayExtraFieldDefinition",
"Chunk",
"ChunkGridDefinition",
@@ -149,3 +158,11 @@
"ZarrV3StorageTransformerField",
"storage_of",
]
+
+
+if not TYPE_CHECKING:
+ # Hidden from type checkers, which would otherwise read every missing
+ # name of this module as a scope.
+ def __getattr__(name: str) -> object:
+ """`CORE` and `CORE_AND_EXTENSIONS`, the scopes' names before they ended in `_CONTEXT`, with a `DeprecationWarning`."""
+ return renamed_scope(__name__, name, RENAMED_SCOPES)
diff --git a/packages/zarr-metadata/tests/model/test_array.py b/packages/zarr-metadata/tests/model/test_array.py
index ab5e7c046b..9c976db86e 100644
--- a/packages/zarr-metadata/tests/model/test_array.py
+++ b/packages/zarr-metadata/tests/model/test_array.py
@@ -54,8 +54,8 @@
from zarr_metadata.v3.array import ZarrV3ArrayMetadataJSON
from zarr_metadata.v3.codec.gzip import GZIP_CODEC
from zarr_metadata.v3.definition import (
- CORE,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
ArrayExtraFieldDefinition,
CodecDefinition,
)
@@ -474,16 +474,16 @@ def test_update_can_add_an_extension_member() -> None:
def test_update_reads_every_member_in_the_models_own_scope() -> None:
"""`update` reads the new document in the scope the model was read in, so a name that scope leaves unclaimed stays unclaimed. Use `with_context` to read it in another scope.
- `zstd` is an extension, so `CORE` leaves it unclaimed and unchecked.
+ `zstd` is an extension, so `CORE_CONTEXT` leaves it unclaimed and unchecked.
"""
little: ZarrV3NamedConfigJSON = {"name": "bytes", "configuration": {"endian": "little"}}
zstd: ZarrV3NamedConfigJSON = {"name": "zstd", "configuration": {"level": 3, "checksum": False}}
- base = ZarrV3ArrayMetadata.create_default(context=CORE, codecs=(little, zstd))
+ base = ZarrV3ArrayMetadata.create_default(context=CORE_CONTEXT, codecs=(little, zstd))
assert isinstance(base.codecs[1], UnclaimedField)
kept = base.update(attributes={"k": 1})
assert kept.codecs[1] == base.codecs[1]
- assert kept.context == CORE
- given = base.with_context(CORE_AND_EXTENSIONS).update(codecs=(little, zstd))
+ assert kept.context == CORE_CONTEXT
+ given = base.with_context(CORE_AND_EXTENSIONS_CONTEXT).update(codecs=(little, zstd))
assert isinstance(given.codecs[1], AcceptedField)
@@ -690,7 +690,7 @@ def test_a_model_pickles_and_copies_with_its_definitions() -> None:
{"name": "bytes", "configuration": {"endian": "little"}},
{"name": "gzip", "configuration": {"level": 1}},
),
- context=CORE,
+ context=CORE_CONTEXT,
)
for again in (pickle.loads(pickle.dumps(model)), copy.copy(model), copy.deepcopy(model)):
assert again == model
@@ -997,14 +997,14 @@ def test_v3_parser_accepts_bare_string_data_type() -> None:
@pytest.mark.parametrize("name", ["bytes", "acme.codec", "urn:example:codec"])
def test_metadata_field_accepts_a_name_as_the_spec_names_one(name: str) -> None:
"""The envelope check accepts a name in a form the spec allows for an extension name, whether or not a definition in scope claims it."""
- assert resolve({"name": name}, CodecDefinition, CORE_AND_EXTENSIONS)[1] == ()
+ assert resolve({"name": name}, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)[1] == ()
@pytest.mark.parametrize("value", [0, 1, "false", None])
def test_metadata_field_must_understand_must_be_boolean(value: object) -> None:
"""must_understand is a JSON boolean, not a truthy scalar."""
_, problems = resolve(
- {"name": "acme.x", "must_understand": value}, CodecDefinition, CORE_AND_EXTENSIONS
+ {"name": "acme.x", "must_understand": value}, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT
)
assert [(problem.loc, problem.kind) for problem in problems] == [
(("must_understand",), "invalid_type")
@@ -1013,7 +1013,9 @@ def test_metadata_field_must_understand_must_be_boolean(value: object) -> None:
def test_metadata_field_rejects_unknown_envelope_member() -> None:
"""Unknown envelope keys cannot be silently discarded during normalization."""
- _, problems = resolve({"name": "acme.x", "typo": 1}, CodecDefinition, CORE_AND_EXTENSIONS)
+ _, problems = resolve(
+ {"name": "acme.x", "typo": 1}, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT
+ )
assert [(problem.loc, problem.kind) for problem in problems] == [(("typo",), "unknown_key")]
@@ -1241,7 +1243,7 @@ def test_a_field_read_from_abstract_containers_writes_canonical_containers() ->
"""`resolve` reads a field given as abstract containers, and the field writes canonical containers at every nesting level."""
value = UserDict({"name": "example", "configuration": UserDict({"values": range(2)})})
- field, problems = resolve(value, CodecDefinition, CORE_AND_EXTENSIONS)
+ field, problems = resolve(value, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert isinstance(field, UnclaimedField)
written = field.to_json()
@@ -1283,7 +1285,7 @@ def test_resolve_reports_the_problems_of_a_metadata_field(
case: Expect[object, frozenset[tuple[str | int, ...]]],
) -> None:
"""`resolve` reports the problems of a metadata-field value, read as a codec."""
- _, problems = resolve(case.input, CodecDefinition, CORE_AND_EXTENSIONS)
+ _, problems = resolve(case.input, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert (problems == ()) is (case.output == frozenset())
assert {p.loc for p in problems} >= case.output
@@ -1965,11 +1967,11 @@ def test_a_field_s_configuration_member_is_counted_from_the_field_s_root() -> No
for _ in range(JSON_DEPTH - 2):
nested = {"x": nested}
field = {"name": "acme.x", "configuration": {"y": nested}}
- _, problems = resolve(field, CodecDefinition, CORE_AND_EXTENSIONS)
+ _, problems = resolve(field, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert [(len(p.loc), p.kind) for p in problems] == [(JSON_DEPTH, "invalid_value")]
assert problems[0].loc[:2] == ("configuration", "y")
shallower = {"name": "acme.x", "configuration": {"y": nested["x"]}}
- assert resolve(shallower, CodecDefinition, CORE_AND_EXTENSIONS)[1] == ()
+ assert resolve(shallower, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)[1] == ()
def test_a_v2_dtype_of_nested_records_is_read_to_the_levels_a_reader_walks() -> None:
@@ -2451,7 +2453,9 @@ def test_a_problem_shows_an_integer_too_long_to_write_by_its_size(
document = {**ZarrV3ArrayMetadata.create_default().to_json(), "zarr_format": 10**5000}
(problem,) = read_array_metadata_v3(document).problems
assert problem.message == f"expected 3, got an integer of {(10**5000).bit_length()} bits"
- (problem,) = resolve({"name": "gzip", 10**5000: 1}, CodecDefinition, CORE_AND_EXTENSIONS)[1]
+ (problem,) = resolve(
+ {"name": "gzip", 10**5000: 1}, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT
+ )[1]
assert problem.message == (
f"non-string key an integer of {(10**5000).bit_length()} bits in JSON object"
)
@@ -2463,7 +2467,9 @@ def test_a_problem_shows_an_integer_too_long_to_write_by_its_size(
assert (
problem.message == "expected 3, got a value of type list the interpreter will not write"
)
- (problem,) = resolve({"name": "gzip", (10**5000,): 1}, CodecDefinition, CORE_AND_EXTENSIONS)[1]
+ (problem,) = resolve(
+ {"name": "gzip", (10**5000,): 1}, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT
+ )[1]
assert problem.message == (
"non-string key a value of type tuple the interpreter will not write in JSON object"
)
diff --git a/packages/zarr-metadata/tests/model/test_consolidating.py b/packages/zarr-metadata/tests/model/test_consolidating.py
index 850888d223..3c7f4e423e 100644
--- a/packages/zarr-metadata/tests/model/test_consolidating.py
+++ b/packages/zarr-metadata/tests/model/test_consolidating.py
@@ -23,8 +23,8 @@
from zarr_metadata.v3.codec.gzip import GZIP_CODEC
from zarr_metadata.v3.data_type.raw import RAW_BYTES_DATA_TYPE
from zarr_metadata.v3.definition import (
- CORE,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
CodecDefinition,
ZarrV3Context,
)
@@ -41,7 +41,7 @@
MY_BYTES = CodecDefinition(
name="bytes", configuration=EmptyConfiguration, kind="array_bytes", size="static"
)
-PRIVATE = CORE.update(codecs={**CORE.codecs, "bytes": MY_BYTES})
+PRIVATE = CORE_CONTEXT.update(codecs={**CORE_CONTEXT.codecs, "bytes": MY_BYTES})
INLINE: dict[str, Any] = {"kind": "inline", "must_understand": False}
@@ -64,11 +64,16 @@ def _group_document(**entries: object) -> dict[str, Any]:
@pytest.mark.parametrize(
("context", "child", "beside", "gained"),
[
- (CORE_AND_EXTENSIONS, ZarrV3ArrayMetadata(ARRAY), {}, False),
- (CORE_AND_EXTENSIONS, ZarrV3ArrayMetadata(ARRAY, context=CORE), {}, False),
- (CORE_AND_EXTENSIONS, ZarrV3ArrayMetadata(WITH_ZSTD, context=CORE), {}, True),
+ (CORE_AND_EXTENSIONS_CONTEXT, ZarrV3ArrayMetadata(ARRAY), {}, False),
+ (CORE_AND_EXTENSIONS_CONTEXT, ZarrV3ArrayMetadata(ARRAY, context=CORE_CONTEXT), {}, False),
(
- CORE,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ ZarrV3ArrayMetadata(WITH_ZSTD, context=CORE_CONTEXT),
+ {},
+ True,
+ ),
+ (
+ CORE_CONTEXT,
ZarrV3GroupMetadata(_group_document(x=ARRAY), context=ZarrV3Context()),
{"a/x": ZarrV3ArrayMetadata(ARRAY, context=ZarrV3Context())},
True,
@@ -124,7 +129,7 @@ def test_error_a_model_that_conflicts_with_the_scope_is_refused_at_its_path() ->
"""A model read with a private `bytes`, given to consolidated metadata whose scope reads the core `bytes`, is a conflict. The problem is at the entry's path, names the kind and the name, and tells the two definitions apart. The model is not silently read again."""
child = ZarrV3ArrayMetadata(ARRAY, context=PRIVATE)
with pytest.raises(MetadataValidationError) as raised:
- _consolidated(CORE_AND_EXTENSIONS, a=child)
+ _consolidated(CORE_AND_EXTENSIONS_CONTEXT, a=child)
(problem,) = raised.value.problems
assert problem.loc == ("metadata", "a", "codecs", 0)
assert problem.kind == "invalid_value"
@@ -137,9 +142,9 @@ def test_error_a_model_that_conflicts_with_the_scope_is_refused_at_its_path() ->
def test_error_a_model_that_would_lose_a_meaning_is_refused_at_its_path() -> None:
"""A model read in a scope that claims `zstd`, given to consolidated metadata whose scope leaves `zstd` unclaimed, would lose information. It is refused, the same way a conflict is."""
- child = ZarrV3ArrayMetadata(WITH_ZSTD, context=CORE_AND_EXTENSIONS)
+ child = ZarrV3ArrayMetadata(WITH_ZSTD, context=CORE_AND_EXTENSIONS_CONTEXT)
with pytest.raises(MetadataValidationError) as raised:
- _consolidated(CORE, a=child)
+ _consolidated(CORE_CONTEXT, a=child)
(problem,) = raised.value.problems
assert problem.loc == ("metadata", "a", "codecs", 1)
assert problem.message.endswith("which that scope leaves unclaimed")
@@ -147,9 +152,9 @@ def test_error_a_model_that_would_lose_a_meaning_is_refused_at_its_path() -> Non
def test_error_a_gain_that_surfaces_a_problem_is_reported_at_the_problem() -> None:
"""A model whose scope left `zstd` unclaimed, given to consolidated metadata whose scope claims it, is read again in that scope. A level that the `zstd` definition refuses is reported as a problem at its location."""
- child = ZarrV3ArrayMetadata(WITH_LOOSE_ZSTD, context=CORE)
+ child = ZarrV3ArrayMetadata(WITH_LOOSE_ZSTD, context=CORE_CONTEXT)
with pytest.raises(MetadataValidationError) as raised:
- _consolidated(CORE_AND_EXTENSIONS, a=child)
+ _consolidated(CORE_AND_EXTENSIONS_CONTEXT, a=child)
assert [problem.loc for problem in raised.value.problems] == [
("metadata", "a", "codecs", 1, "configuration", "level")
]
@@ -175,25 +180,25 @@ def test_error_the_reader_reports_a_conflicting_model_as_the_constructor_does()
def test_a_group_writes_the_consolidated_metadata_it_is_given_in_its_own_scope() -> None:
"""A `ZarrV3ConsolidatedMetadata` given to `update` is written as its document. It does not bring its own scope into the group: the group's `context` stays its own, and reading the written member in that scope gives the models in it."""
- held = _consolidated(CORE_AND_EXTENSIONS, a=ZarrV3ArrayMetadata(ARRAY))
- group = ZarrV3GroupMetadata.create_default(context=CORE, attributes={"t": 1})
+ held = _consolidated(CORE_AND_EXTENSIONS_CONTEXT, a=ZarrV3ArrayMetadata(ARRAY))
+ group = ZarrV3GroupMetadata.create_default(context=CORE_CONTEXT, attributes={"t": 1})
updated = group.update(consolidated_metadata=held)
- assert updated.context == CORE
+ assert updated.context == CORE_CONTEXT
assert updated.attributes == {"t": 1}
assert updated.consolidated_metadata == held.to_json()
again = ZarrV3ConsolidatedMetadata(updated.consolidated_metadata, context=updated.context)
- assert again.metadata["a"].context == CORE
+ assert again.metadata["a"].context == CORE_CONTEXT
assert again == held
def test_error_a_group_refuses_consolidated_metadata_that_conflicts_with_its_scope() -> None:
"""A `ZarrV3ConsolidatedMetadata` read in another scope is read again in the group's scope before the group writes it, as a listing of node models is. A model read with a private `bytes` conflicts with the core `bytes` the group reads, so `update` and `create_default` refuse it at the child's path."""
held = _consolidated(PRIVATE, a=ZarrV3ArrayMetadata(ARRAY, context=PRIVATE))
- group = ZarrV3GroupMetadata.create_default(context=CORE_AND_EXTENSIONS)
+ group = ZarrV3GroupMetadata.create_default(context=CORE_AND_EXTENSIONS_CONTEXT)
for write in (
lambda: group.update(consolidated_metadata=held),
lambda: ZarrV3GroupMetadata.create_default(
- context=CORE_AND_EXTENSIONS, consolidated_metadata=held
+ context=CORE_AND_EXTENSIONS_CONTEXT, consolidated_metadata=held
),
):
with pytest.raises(MetadataValidationError) as raised:
@@ -205,9 +210,9 @@ def test_error_a_group_refuses_consolidated_metadata_that_conflicts_with_its_sco
def test_error_a_group_refuses_consolidated_metadata_that_would_lose_a_meaning() -> None:
"""A listing read in a scope that claims `zstd`, given to a group whose scope leaves `zstd` unclaimed, would lose information when the group's scope reads it, so the group refuses it at the child's path."""
- held = _consolidated(CORE_AND_EXTENSIONS, a=ZarrV3ArrayMetadata(WITH_ZSTD))
+ held = _consolidated(CORE_AND_EXTENSIONS_CONTEXT, a=ZarrV3ArrayMetadata(WITH_ZSTD))
with pytest.raises(MetadataValidationError) as raised:
- ZarrV3GroupMetadata.create_default(context=CORE).update(consolidated_metadata=held)
+ ZarrV3GroupMetadata.create_default(context=CORE_CONTEXT).update(consolidated_metadata=held)
assert [problem.loc for problem in raised.value.problems] == [
("consolidated_metadata", "metadata", "a", "codecs", 1)
]
@@ -217,7 +222,7 @@ def test_error_a_group_refuses_consolidated_metadata_that_would_lose_a_meaning()
("context", "found"),
[
(ZarrV3Context(), []),
- (CORE, [("metadata", "g", "consolidated_metadata", "garbage")]),
+ (CORE_CONTEXT, [("metadata", "g", "consolidated_metadata", "garbage")]),
],
ids=["unclaimed", "claimed"],
)
@@ -232,12 +237,12 @@ def test_a_listed_group_s_listing_is_read_only_where_the_scope_claims_it(
def test_a_group_reads_its_listing_in_its_own_scope() -> None:
"""`consolidated()` reads the documents a group's `consolidated_metadata` lists in the group's own scope, so a child reads as the group's reader would read it, and returns None for a group that lists none."""
- group = ZarrV3GroupMetadata(_group_document(a=WITH_ZSTD), context=CORE)
+ group = ZarrV3GroupMetadata(_group_document(a=WITH_ZSTD), context=CORE_CONTEXT)
listing = group.consolidated()
assert listing is not None
- assert listing.context == CORE
- assert listing.metadata["a"].context == CORE
- assert listing == ZarrV3ConsolidatedMetadata(group.consolidated_metadata, context=CORE)
+ assert listing.context == CORE_CONTEXT
+ assert listing.metadata["a"].context == CORE_CONTEXT
+ assert listing == ZarrV3ConsolidatedMetadata(group.consolidated_metadata, context=CORE_CONTEXT)
assert ZarrV3GroupMetadata.create_default().consolidated() is None
@@ -267,11 +272,11 @@ def test_error_a_group_s_listing_that_is_not_an_object(listing: object) -> None:
def test_children_of_different_scopes_consolidate_in_their_union() -> None:
"""Children read in different scopes are consolidated in the union of their scopes. Each child refines into that scope, and the consolidated metadata reads in it."""
- a = ZarrV3ArrayMetadata(ARRAY, context=CORE)
- b = ZarrV3ArrayMetadata(WITH_ZSTD, context=CORE_AND_EXTENSIONS)
+ a = ZarrV3ArrayMetadata(ARRAY, context=CORE_CONTEXT)
+ b = ZarrV3ArrayMetadata(WITH_ZSTD, context=CORE_AND_EXTENSIONS_CONTEXT)
context = a.context.union(b.context)
held = _consolidated(context, a=a, b=b)
- assert held.context == CORE_AND_EXTENSIONS
+ assert held.context == CORE_AND_EXTENSIONS_CONTEXT
assert held.metadata["a"] == a
assert held.metadata["b"] == b
@@ -310,10 +315,10 @@ def test_error_a_data_type_conflict_names_the_kind_and_the_written_name() -> Non
)
child = ZarrV3ArrayMetadata(
{**ARRAY, "data_type": "r16", "fill_value": [0, 0], "codecs": ("bytes",)},
- context=CORE.update(data_types={**CORE.data_types, "r*": strict}),
+ context=CORE_CONTEXT.update(data_types={**CORE_CONTEXT.data_types, "r*": strict}),
)
with pytest.raises(MetadataValidationError) as raised:
- _consolidated(CORE, a=child)
+ _consolidated(CORE_CONTEXT, a=child)
(problem,) = raised.value.problems
assert problem.loc == ("metadata", "a", "data_type")
assert problem.message == (
@@ -328,10 +333,10 @@ def test_error_a_conflict_between_definitions_alike_says_they_differ() -> None:
strict = dataclasses.replace(GZIP_CODEC, rules=lambda configuration, nested: iter(()))
child = ZarrV3ArrayMetadata(
{**ARRAY, "codecs": ("bytes", {"name": "gzip", "configuration": {"level": 1}})},
- context=CORE.update(codecs={**CORE.codecs, "gzip": strict}),
+ context=CORE_CONTEXT.update(codecs={**CORE_CONTEXT.codecs, "gzip": strict}),
)
with pytest.raises(MetadataValidationError) as raised:
- _consolidated(CORE, a=child)
+ _consolidated(CORE_CONTEXT, a=child)
(problem,) = raised.value.problems
assert problem.message == (
"expected a document read in the scope of the consolidated metadata, got a model that "
diff --git a/packages/zarr-metadata/tests/model/test_construction.py b/packages/zarr-metadata/tests/model/test_construction.py
index e3c15a6d2b..5861edeabe 100644
--- a/packages/zarr-metadata/tests/model/test_construction.py
+++ b/packages/zarr-metadata/tests/model/test_construction.py
@@ -31,8 +31,8 @@
from zarr_metadata.v3.codec.gzip import GZIP_CODEC, GzipCodecConfiguration
from zarr_metadata.v3.data_type.int8 import INT8_DATA_TYPE
from zarr_metadata.v3.definition import (
- CORE,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
CodecDefinition,
)
@@ -195,8 +195,8 @@ def counted(
yield from ()
counting = dataclasses.replace(INT8_DATA_TYPE, fill_value_rules=counted)
- context = CORE_AND_EXTENSIONS.update(
- data_types={**CORE_AND_EXTENSIONS.data_types, "int8": counting}
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
+ data_types={**CORE_AND_EXTENSIONS_CONTEXT.data_types, "int8": counting}
)
model = ZarrV3ArrayMetadata.create_default(context=context, data_type="int8", fill_value=3)
assert values == [3]
@@ -324,7 +324,9 @@ def test_error_update_takes_fields_as_json() -> None:
def test_error_update_takes_no_refused_field() -> None:
"""A field refused by its definition holds JSON its definition does not read, so it has no JSON for `update` to write: given in a member, it is refused with a `TypeError` that says to give the member as JSON."""
- refused, _ = resolve({"name": "gzip", "configuration": {"level": 99}}, CodecDefinition, CORE)
+ refused, _ = resolve(
+ {"name": "gzip", "configuration": {"level": 99}}, CodecDefinition, CORE_CONTEXT
+ )
assert isinstance(refused, RefusedField)
with pytest.raises(TypeError, match="a refused field has no JSON"):
ARRAY.update(codecs=(ARRAY.to_json()["codecs"][0], refused)) # pyright: ignore[reportArgumentType]
@@ -456,9 +458,9 @@ def counting(configuration: GzipCodecConfiguration) -> GzipCodecConfiguration:
asked.append(1)
return configuration
- context = CORE_AND_EXTENSIONS.update(
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
codecs={
- **CORE_AND_EXTENSIONS.codecs,
+ **CORE_AND_EXTENSIONS_CONTEXT.codecs,
"gzip": dataclasses.replace(GZIP_CODEC, canonical=counting),
}
)
@@ -489,9 +491,9 @@ def test_error_a_canonical_that_raises_is_raised_when_the_model_is_first_compare
def refuses(configuration: GzipCodecConfiguration) -> GzipCodecConfiguration:
raise ValueError("no")
- context = CORE_AND_EXTENSIONS.update(
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
codecs={
- **CORE_AND_EXTENSIONS.codecs,
+ **CORE_AND_EXTENSIONS_CONTEXT.codecs,
"gzip": dataclasses.replace(GZIP_CODEC, canonical=refuses),
}
)
diff --git a/packages/zarr-metadata/tests/model/test_extension_points.py b/packages/zarr-metadata/tests/model/test_extension_points.py
index 063338e160..38d738f620 100644
--- a/packages/zarr-metadata/tests/model/test_extension_points.py
+++ b/packages/zarr-metadata/tests/model/test_extension_points.py
@@ -2,7 +2,7 @@
The model checks a document's structure itself. The configuration of a
codec, data type, chunk grid or chunk key encoding is checked by its
-definition. The definition is found in a scope (`CORE_AND_EXTENSIONS`
+definition. The definition is found in a scope (`CORE_AND_EXTENSIONS_CONTEXT`
unless a caller passes another), and problems are reported at the field's
location in the document. A name that no definition in the scope claims
is not checked.
@@ -25,8 +25,8 @@
)
from zarr_metadata.problems import MetadataValidationError
from zarr_metadata.v3.definition import (
- CORE,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
CodecDefinition,
)
@@ -63,21 +63,24 @@ class LenientGzipConfiguration(TypedDict, closed=True):
@pytest.mark.parametrize(
("document", "context"),
[
- (_document(), CORE_AND_EXTENSIONS),
+ (_document(), CORE_AND_EXTENSIONS_CONTEXT),
(
_document(codecs=[BYTES, {"name": "gzip", "configuration": {"level": 5}}]),
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
),
(
_document(codecs=[BYTES, {"name": "acme.lz9", "configuration": {"anything": 1}}]),
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
),
- (_document(codecs=[BYTES, {"name": "zstd", "configuration": {"level": 99}}]), CORE),
+ (_document(codecs=[BYTES, {"name": "zstd", "configuration": {"level": 99}}]), CORE_CONTEXT),
(
_document(codecs=[BYTES, {"name": "gzip", "configuration": {"level": 99}}]),
- CORE.update(codecs={**CORE.codecs, "gzip": LENIENT_GZIP}),
+ CORE_CONTEXT.update(codecs={**CORE_CONTEXT.codecs, "gzip": LENIENT_GZIP}),
+ ),
+ (
+ dict(ZarrV3ArrayMetadata.create_default(shape=(0, 4)).to_json()),
+ CORE_AND_EXTENSIONS_CONTEXT,
),
- (dict(ZarrV3ArrayMetadata.create_default(shape=(0, 4)).to_json()), CORE_AND_EXTENSIONS),
],
ids=[
"default",
@@ -173,7 +176,7 @@ def test_error_an_array_in_consolidated_metadata() -> None:
def test_an_array_in_consolidated_metadata_reads_in_the_scope_it_is_read_in() -> None:
"""An array listed in consolidated metadata is read in the scope the member is read in: a lenient `gzip` accepts the level the core `gzip` refuses, and the model round-trips in that scope."""
member = {"kind": "inline", "must_understand": False, "metadata": {"a": BAD_GZIP_ARRAY}}
- lenient = CORE.update(codecs={**CORE.codecs, "gzip": LENIENT_GZIP})
+ lenient = CORE_CONTEXT.update(codecs={**CORE_CONTEXT.codecs, "gzip": LENIENT_GZIP})
reading = read_consolidated_metadata_v3(member, context=lenient)
assert reading.problems == ()
assert reading.metadata is not None
diff --git a/packages/zarr-metadata/tests/model/test_extra_fields.py b/packages/zarr-metadata/tests/model/test_extra_fields.py
index 86684ce928..06982cc252 100644
--- a/packages/zarr-metadata/tests/model/test_extra_fields.py
+++ b/packages/zarr-metadata/tests/model/test_extra_fields.py
@@ -23,7 +23,7 @@
from zarr_metadata.problems import MetadataValidationError
from zarr_metadata.typed_json import UserJSONValue
from zarr_metadata.v3.definition import (
- CORE,
+ CORE_CONTEXT,
ArrayExtraFieldDefinition,
GroupExtraFieldDefinition,
ZarrV3Context,
@@ -51,8 +51,8 @@ class AcmeNoteConfiguration(TypedDict, closed=True):
ACME_NOTE = ArrayExtraFieldDefinition(name="acme.note", configuration=AcmeNoteConfiguration)
"""A reader's own definition of the `acme.note` member of an array, whose numbers must be finite."""
-CONTEXT = CORE.update(
- array_extra_fields=[*CORE.array_extra_fields.values(), ACME_TILING, ACME_NOTE]
+CONTEXT = CORE_CONTEXT.update(
+ array_extra_fields=[*CORE_CONTEXT.array_extra_fields.values(), ACME_TILING, ACME_NOTE]
)
ARRAY: dict[str, Any] = dict(ZarrV3ArrayMetadata.create_default(shape=(4,)).to_json())
@@ -66,10 +66,10 @@ class AcmeNoteConfiguration(TypedDict, closed=True):
("document", "context", "claimed", "must_understand"),
[
({**ARRAY, "acme.tiling": TILING}, CONTEXT, {"acme.tiling"}, {}),
- ({**ARRAY, "acme.tiling": TILING}, CORE, set(), {"acme.tiling": TILING}),
+ ({**ARRAY, "acme.tiling": TILING}, CORE_CONTEXT, set(), {"acme.tiling": TILING}),
({**ARRAY, "acme.other": {"must_understand": False}}, CONTEXT, set(), {}),
({**ARRAY, "acme.other": 5}, CONTEXT, set(), {"acme.other": 5}),
- ({**GROUP, "consolidated_metadata": INLINE}, CORE, {"consolidated_metadata"}, {}),
+ ({**GROUP, "consolidated_metadata": INLINE}, CORE_CONTEXT, {"consolidated_metadata"}, {}),
({**GROUP, "consolidated_metadata": INLINE}, ZarrV3Context(), set(), {}),
(
{**GROUP, "consolidated_metadata": None},
@@ -77,8 +77,13 @@ class AcmeNoteConfiguration(TypedDict, closed=True):
set(),
{"consolidated_metadata": None},
),
- ({**ARRAY, "consolidated_metadata": INLINE}, CORE, set(), {}),
- ({**GROUP, "consolidated_metadata": NAN_LISTING}, CORE, {"consolidated_metadata"}, {}),
+ ({**ARRAY, "consolidated_metadata": INLINE}, CORE_CONTEXT, set(), {}),
+ (
+ {**GROUP, "consolidated_metadata": NAN_LISTING},
+ CORE_CONTEXT,
+ {"consolidated_metadata"},
+ {},
+ ),
({**GROUP, "consolidated_metadata": NAN_LISTING}, ZarrV3Context(), set(), {}),
({**ARRAY, "acme.other": {"x": math.inf}}, CONTEXT, set(), {"acme.other": {"x": math.inf}}),
],
@@ -102,7 +107,7 @@ def test_a_scope_reads_the_extra_fields_it_claims(
claimed: set[str],
must_understand: dict[str, object],
) -> None:
- """Each member the spec does not define is an extra field. A member whose key a definition in scope claims is an `AcceptedField`, and a reader understands it. Any other member is an `UnclaimedField`, kept as written, and `must_understand_fields` lists it unless it says `"must_understand": false`. Nothing reads an unclaimed member, so like attributes it may hold any number, finite or not. A definition of a group's extra field does not read a member of an array, and `CORE` reads a group's `consolidated_metadata`. The document is written back as it was read."""
+ """Each member the spec does not define is an extra field. A member whose key a definition in scope claims is an `AcceptedField`, and a reader understands it. Any other member is an `UnclaimedField`, kept as written, and `must_understand_fields` lists it unless it says `"must_understand": false`. Nothing reads an unclaimed member, so like attributes it may hold any number, finite or not. A definition of a group's extra field does not read a member of an array, and `CORE_CONTEXT` reads a group's `consolidated_metadata`. The document is written back as it was read."""
model: ZarrV3ArrayMetadata | ZarrV3GroupMetadata
if document["node_type"] == "array":
model = ZarrV3ArrayMetadata(document, context=context)
@@ -167,16 +172,16 @@ def test_an_extra_field_is_a_field_of_the_reading() -> None:
def test_the_simplest_spelling_of_an_extra_field_is_plain_json() -> None:
- """`canonical_of` returns plain JSON for an extra field, with no read-only views inside, so `json.dumps` writes it. `CORE` claims a group's `consolidated_metadata`, whose configuration holds objects with no nested field."""
- model = ZarrV3GroupMetadata({**GROUP, "consolidated_metadata": INLINE}, context=CORE)
+ """`canonical_of` returns plain JSON for an extra field, with no read-only views inside, so `json.dumps` writes it. `CORE_CONTEXT` claims a group's `consolidated_metadata`, whose configuration holds objects with no nested field."""
+ model = ZarrV3GroupMetadata({**GROUP, "consolidated_metadata": INLINE}, context=CORE_CONTEXT)
simplest = canonical_of(model.extra_fields["consolidated_metadata"], ())
assert json.loads(json.dumps(simplest)) == {"consolidated_metadata": INLINE}
def test_error_a_claimed_extra_field_that_is_not_an_object() -> None:
- """A definition reads a member whose value is an object. Any other value of a claimed member is a problem at the member's key, as `CORE` refuses a `consolidated_metadata` of null."""
+ """A definition reads a member whose value is an object. Any other value of a claimed member is a problem at the member's key, as `CORE_CONTEXT` refuses a `consolidated_metadata` of null."""
with pytest.raises(MetadataValidationError) as raised:
- ZarrV3GroupMetadata({**GROUP, "consolidated_metadata": None}, context=CORE)
+ ZarrV3GroupMetadata({**GROUP, "consolidated_metadata": None}, context=CORE_CONTEXT)
assert [(p.loc, p.kind) for p in raised.value.problems] == [
(("consolidated_metadata",), "invalid_type")
]
@@ -230,7 +235,7 @@ def test_error_resolve_a_member_the_spec_defines_as_an_extra_field(
member: dict[str, Any],
) -> None:
"""`resolve` reads a member the spec defines, read as an extra field, as a `RefusedField` with one `invalid_value` problem that says the spec defines it, for an array and for a group."""
- read, problems = resolve(member, kind, CORE)
+ read, problems = resolve(member, kind, CORE_CONTEXT)
assert isinstance(read, RefusedField)
assert [(p.loc, p.kind) for p in problems] == [((), "invalid_value")]
assert "the spec defines" in problems[0].message
diff --git a/packages/zarr-metadata/tests/model/test_group.py b/packages/zarr-metadata/tests/model/test_group.py
index 4594ba1f4e..37a8ed0e99 100644
--- a/packages/zarr-metadata/tests/model/test_group.py
+++ b/packages/zarr-metadata/tests/model/test_group.py
@@ -49,8 +49,8 @@
from zarr_metadata.v3.array import ZarrV3ArrayMetadataJSON
from zarr_metadata.v3.codec.gzip import GZIP_CODEC, GzipCodecConfiguration
from zarr_metadata.v3.definition import (
- CORE,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
CodecDefinition,
)
from zarr_metadata.v3.group import ZarrV3GroupMetadataJSON
@@ -148,7 +148,9 @@ def _v2_consolidated_problems(document: object) -> list[tuple[tuple[str | int, .
def _v3_field_problems(field: object) -> list[tuple[tuple[str | int, ...], str]]:
- return [(p.loc, p.kind) for p in resolve(field, CodecDefinition, CORE_AND_EXTENSIONS)[1]]
+ return [
+ (p.loc, p.kind) for p in resolve(field, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)[1]
+ ]
@pytest.mark.parametrize(
@@ -641,7 +643,9 @@ def test_error_a_consolidated_envelope_reports_the_members_it_lacks_in_its_order
def test_group_update_keeps_the_documents_it_holds() -> None:
"""`update` does not read the consolidated documents again: a child read in an extended scope keeps each field as it was read, and the group keeps its scope."""
- context = CORE_AND_EXTENSIONS.update(codecs=[*CORE_AND_EXTENSIONS.codecs.values(), ACME_X])
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
+ codecs=[*CORE_AND_EXTENSIONS_CONTEXT.codecs.values(), ACME_X]
+ )
child = ZarrV3ArrayMetadata.create_default(
context=context, shape=(4,), codecs=(LITTLE, {"name": "acme.x"})
)
@@ -656,18 +660,18 @@ def test_group_update_keeps_the_documents_it_holds() -> None:
def test_group_update_writes_the_consolidated_metadata_it_is_given() -> None:
- """`update` takes consolidated metadata as a document writes it, or as a `ZarrV3ConsolidatedMetadata`, whose document it writes; the group holds the same member either way. `UNSET` removes it. `zstd` is an extension, so `CORE` leaves it unclaimed, and the member read in the group's scope holds it as unclaimed."""
+ """`update` takes consolidated metadata as a document writes it, or as a `ZarrV3ConsolidatedMetadata`, whose document it writes; the group holds the same member either way. `UNSET` removes it. `zstd` is an extension, so `CORE_CONTEXT` leaves it unclaimed, and the member read in the group's scope holds it as unclaimed."""
zstd = {"name": "zstd", "configuration": {"level": 3, "checksum": False}}
member = cast("Any", _inline(a=_array(codecs=[LITTLE, zstd])))
- held = ZarrV3ConsolidatedMetadata(member, context=CORE)
+ held = ZarrV3ConsolidatedMetadata(member, context=CORE_CONTEXT)
child = held.metadata["a"]
assert isinstance(child, ZarrV3ArrayMetadata)
assert isinstance(child.codecs[1], UnclaimedField)
- group = ZarrV3GroupMetadata.create_default(context=CORE)
+ group = ZarrV3GroupMetadata.create_default(context=CORE_CONTEXT)
updated = group.update(consolidated_metadata=held)
assert updated.consolidated_metadata == held.to_json()
assert updated == group.update(consolidated_metadata=member)
- assert ZarrV3ConsolidatedMetadata(updated.consolidated_metadata, context=CORE) == held
+ assert ZarrV3ConsolidatedMetadata(updated.consolidated_metadata, context=CORE_CONTEXT) == held
removed = updated.update(consolidated_metadata=UNSET)
assert removed.consolidated_metadata is UNSET
@@ -692,7 +696,7 @@ def _fields_of_an_array(*at: str | int) -> list[tuple[str | int, ...]]:
# the outer group also lists. The listed group's own listing is
# checked against the outer one, and its documents are not models
# of the outer member. The listed group's member is its extra field,
- # which `CORE` reads as an envelope.
+ # which `CORE_CONTEXT` reads as an envelope.
(
_inline(g=_group(consolidated_metadata=_inline(b=_array())), **{"g/b": _array()}),
["g", "g/b"],
@@ -861,9 +865,9 @@ def counted(
reads.append(configuration)
yield from ()
- context = CORE_AND_EXTENSIONS.update(
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
codecs={
- **CORE_AND_EXTENSIONS.codecs,
+ **CORE_AND_EXTENSIONS_CONTEXT.codecs,
"gzip": dataclasses.replace(GZIP_CODEC, rules=counted),
}
)
diff --git a/packages/zarr-metadata/tests/model/test_pair.py b/packages/zarr-metadata/tests/model/test_pair.py
index 0f1b06341e..f551fa9c8d 100644
--- a/packages/zarr-metadata/tests/model/test_pair.py
+++ b/packages/zarr-metadata/tests/model/test_pair.py
@@ -34,8 +34,8 @@
from zarr_metadata.v3.codec.bytes import BYTES_CODEC
from zarr_metadata.v3.codec.zstd import ZSTD_CODEC
from zarr_metadata.v3.definition import (
- CORE,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
CodecDefinition,
ZarrV3Context,
)
@@ -57,7 +57,7 @@
name="bytes", configuration=EmptyConfiguration, kind="array_bytes", size="static"
)
"""A private `bytes` definition: a different meaning under the core name, with no configuration."""
-PRIVATE = CORE.update(codecs={**CORE.codecs, "bytes": MY_BYTES})
+PRIVATE = CORE_CONTEXT.update(codecs={**CORE_CONTEXT.codecs, "bytes": MY_BYTES})
SPELLED_OUT: dict[str, Any] = {
**ARRAY,
"data_type": {"name": "uint8"},
@@ -69,8 +69,8 @@
@pytest.mark.parametrize(
("document", "context", "expected_context"),
[
- (ARRAY, None, CORE_AND_EXTENSIONS),
- (ARRAY, CORE, CORE),
+ (ARRAY, None, CORE_AND_EXTENSIONS_CONTEXT),
+ (ARRAY, CORE_CONTEXT, CORE_CONTEXT),
(SPELLED_OUT, ZarrV3Context(), ZarrV3Context()),
],
ids=["default-scope", "core", "empty-scope"],
@@ -78,7 +78,7 @@
def test_a_model_is_its_document_read_in_its_scope(
document: dict[str, Any], context: ZarrV3Context | None, expected_context: ZarrV3Context
) -> None:
- """A model built from a document holds that document, as written and refined, and the scope it was read in. The scope is `CORE_AND_EXTENSIONS` when none is given."""
+ """A model built from a document holds that document, as written and refined, and the scope it was read in. The scope is `CORE_AND_EXTENSIONS_CONTEXT` when none is given."""
model = ZarrV3ArrayMetadata(document, context=context)
assert model.context == expected_context
assert model.to_json() == refine_json(document)[0]
@@ -141,7 +141,7 @@ def test_error_a_document_with_a_problem_is_refused_at_construction() -> None:
("left", "right", "equal"),
[
(ZarrV3ArrayMetadata(ARRAY), ZarrV3ArrayMetadata(SPELLED_OUT), True),
- (ZarrV3ArrayMetadata(ARRAY, context=CORE), ZarrV3ArrayMetadata(ARRAY), True),
+ (ZarrV3ArrayMetadata(ARRAY, context=CORE_CONTEXT), ZarrV3ArrayMetadata(ARRAY), True),
(ZarrV3ArrayMetadata(ARRAY), ZarrV3ArrayMetadata(ARRAY, context=PRIVATE), False),
(ZarrV3ArrayMetadata(ARRAY), ZarrV3ArrayMetadata(ARRAY, context=ZarrV3Context()), False),
(ZarrV3ArrayMetadata(ARRAY), ZarrV3ArrayMetadata({**ARRAY, "attributes": {}}), True),
@@ -158,7 +158,9 @@ def test_models_are_equal_by_what_their_documents_mean(
@pytest.mark.parametrize(
- "context", [None, CORE, PRIVATE, ZarrV3Context()], ids=["default", "core", "private", "empty"]
+ "context",
+ [None, CORE_CONTEXT, PRIVATE, ZarrV3Context()],
+ ids=["default", "core", "private", "empty"],
)
def test_a_model_round_trips_through_its_document_in_its_scope(
context: ZarrV3Context | None,
@@ -183,7 +185,7 @@ def test_error_a_model_whose_scope_does_not_pickle_says_so() -> None:
)
model = ZarrV3ArrayMetadata(
{**ARRAY, "codecs": ["bytes", "acme.c"]},
- context=CORE.update(codecs=[*CORE.codecs.values(), local]),
+ context=CORE_CONTEXT.update(codecs=[*CORE_CONTEXT.codecs.values(), local]),
)
with pytest.raises((pickle.PicklingError, AttributeError)):
pickle.dumps(model)
@@ -266,10 +268,10 @@ def test_error_update_refuses_a_document_with_a_problem() -> None:
@pytest.mark.parametrize(
("model", "context", "gained"),
[
- (ZarrV3ArrayMetadata(ARRAY, context=ZarrV3Context()), CORE, True),
- (ZarrV3ArrayMetadata(ARRAY, context=CORE), CORE_AND_EXTENSIONS, False),
- (ZarrV3ArrayMetadata(FLOAT, context=ZarrV3Context()), CORE, True),
- (ZarrV3ArrayMetadata(SHARDED, context=ZarrV3Context()), CORE, True),
+ (ZarrV3ArrayMetadata(ARRAY, context=ZarrV3Context()), CORE_CONTEXT, True),
+ (ZarrV3ArrayMetadata(ARRAY, context=CORE_CONTEXT), CORE_AND_EXTENSIONS_CONTEXT, False),
+ (ZarrV3ArrayMetadata(FLOAT, context=ZarrV3Context()), CORE_CONTEXT, True),
+ (ZarrV3ArrayMetadata(SHARDED, context=ZarrV3Context()), CORE_CONTEXT, True),
],
ids=["gain", "nothing-to-gain", "gain-data-type-with-fill-value", "gain-of-a-shard"],
)
@@ -291,7 +293,7 @@ def test_refined_in_moves_a_model_up_the_order(
("model", "context"),
[
(ZarrV3ArrayMetadata(ARRAY), PRIVATE),
- (ZarrV3ArrayMetadata(ARRAY, context=CORE), ZarrV3Context()),
+ (ZarrV3ArrayMetadata(ARRAY, context=CORE_CONTEXT), ZarrV3Context()),
],
ids=["conflict", "loss"],
)
@@ -311,7 +313,7 @@ def test_with_context_reads_the_document_in_any_scope() -> None:
assert private.context == PRIVATE
assert private.codecs[0].definition == MY_BYTES
assert isinstance(model.with_context(ZarrV3Context()).codecs[0], UnclaimedField)
- assert model.with_context(None).context == CORE_AND_EXTENSIONS
+ assert model.with_context(None).context == CORE_AND_EXTENSIONS_CONTEXT
def test_error_with_context_refuses_a_document_the_scope_reads_with_a_problem() -> None:
@@ -321,7 +323,7 @@ def test_error_with_context_refuses_a_document_the_scope_reads_with_a_problem()
context=ZarrV3Context(),
)
with pytest.raises(MetadataValidationError):
- loose.with_context(CORE)
+ loose.with_context(CORE_CONTEXT)
@pytest.mark.parametrize(
@@ -333,17 +335,17 @@ def test_error_with_context_refuses_a_document_the_scope_reads_with_a_problem()
(ZarrV3ArrayMetadata(ARRAY, context=PRIVATE), ZarrV3ArrayMetadata(ARRAY), False),
(ZarrV3ArrayMetadata({**ARRAY, "attributes": {"a": 1}}), ZarrV3ArrayMetadata(ARRAY), False),
(
- ZarrV3ArrayMetadata(FLOAT, context=CORE),
+ ZarrV3ArrayMetadata(FLOAT, context=CORE_CONTEXT),
ZarrV3ArrayMetadata(FLOAT, context=ZarrV3Context()),
True,
),
(
- ZarrV3ArrayMetadata(SHARDED, context=CORE),
+ ZarrV3ArrayMetadata(SHARDED, context=CORE_CONTEXT),
ZarrV3ArrayMetadata(SHARDED, context=ZarrV3Context()),
True,
),
(
- ZarrV3ArrayMetadata(FLOAT, context=CORE),
+ ZarrV3ArrayMetadata(FLOAT, context=CORE_CONTEXT),
ZarrV3ArrayMetadata({**FLOAT, "fill_value": "banana"}, context=ZarrV3Context()),
False,
),
@@ -393,7 +395,7 @@ def test_refines_orders_models_by_information(
def test_a_group_holds_its_consolidated_metadata_as_written() -> None:
"""A group model holds its `consolidated_metadata` member as written, refined and read-only, and does not read the documents it lists. `ZarrV3ConsolidatedMetadata` reads them: each becomes a model in its scope, and writes its document as written."""
- group = ZarrV3GroupMetadata(CONSOLIDATED, context=CORE)
+ group = ZarrV3GroupMetadata(CONSOLIDATED, context=CORE_CONTEXT)
member = group.consolidated_metadata
assert member == refine_json(MEMBER)[0]
with pytest.raises(TypeError):
@@ -415,8 +417,8 @@ def counted(configuration: object, nested: Nested) -> Iterator[ValidationProblem
calls.append(1)
return iter(())
- context = CORE.update(
- codecs={**CORE.codecs, "bytes": dataclasses.replace(BYTES_CODEC, rules=counted)}
+ context = CORE_CONTEXT.update(
+ codecs={**CORE_CONTEXT.codecs, "bytes": dataclasses.replace(BYTES_CODEC, rules=counted)}
)
ZarrV3GroupMetadata(CONSOLIDATED, context=context)
assert calls == []
@@ -432,7 +434,11 @@ def _listing(a: dict[str, Any]) -> dict[str, Any]:
@pytest.mark.parametrize(
("left", "right", "equal"),
[
- (ZarrV3GroupMetadata(CONSOLIDATED), ZarrV3GroupMetadata(CONSOLIDATED, context=CORE), True),
+ (
+ ZarrV3GroupMetadata(CONSOLIDATED),
+ ZarrV3GroupMetadata(CONSOLIDATED, context=CORE_CONTEXT),
+ True,
+ ),
(
ZarrV3GroupMetadata(CONSOLIDATED),
ZarrV3GroupMetadata(CONSOLIDATED, context=PRIVATE),
@@ -446,7 +452,7 @@ def _listing(a: dict[str, Any]) -> dict[str, Any]:
),
(
ZarrV3ConsolidatedMetadata(MEMBER),
- ZarrV3ConsolidatedMetadata(MEMBER, context=CORE),
+ ZarrV3ConsolidatedMetadata(MEMBER, context=CORE_CONTEXT),
True,
),
(
@@ -484,7 +490,7 @@ def test_groups_and_consolidated_metadata_are_equal_by_what_they_read(
@pytest.mark.parametrize("document", [GROUP, CONSOLIDATED], ids=["group", "consolidated"])
def test_a_group_round_trips_through_its_document_in_its_scope(document: dict[str, Any]) -> None:
"""`from_json(g.to_json(), context=g.context) == g`, and pickle preserves both the document and the scope."""
- group = ZarrV3GroupMetadata(document, context=CORE)
+ group = ZarrV3GroupMetadata(document, context=CORE_CONTEXT)
assert ZarrV3GroupMetadata(group.to_json(), context=group.context) == group
assert pickle.loads(pickle.dumps(group)) == group
@@ -494,18 +500,18 @@ def test_group_update_with_context_and_refined_in_behave_as_the_arrays_do() -> N
group = ZarrV3GroupMetadata(CONSOLIDATED, context=ZarrV3Context())
assert group.update(attributes={"g": 2}).consolidated_metadata == group.consolidated_metadata
assert isinstance(group.extra_fields["consolidated_metadata"], UnclaimedField)
- for moved in (group.refined_in(CORE), group.with_context(PRIVATE)):
+ for moved in (group.refined_in(CORE_CONTEXT), group.with_context(PRIVATE)):
assert isinstance(moved.extra_fields["consolidated_metadata"], AcceptedField)
assert moved.consolidated_metadata == group.consolidated_metadata
assert moved.refines(group)
assert not group.refines(moved)
assert moved != group
- assert group.refined_in(CORE).context == CORE
+ assert group.refined_in(CORE_CONTEXT).context == CORE_CONTEXT
def test_error_group_refined_in_refuses_a_scope_that_loses_an_extra_field() -> None:
- """A group read in `CORE` reads its `consolidated_metadata` as the envelope. A scope that leaves the member unclaimed would lose that reading, so `refined_in` raises `ScopeConflictError`, located at the member."""
- group = ZarrV3GroupMetadata(CONSOLIDATED, context=CORE)
+ """A group read in `CORE_CONTEXT` reads its `consolidated_metadata` as the envelope. A scope that leaves the member unclaimed would lose that reading, so `refined_in` raises `ScopeConflictError`, located at the member."""
+ group = ZarrV3GroupMetadata(CONSOLIDATED, context=CORE_CONTEXT)
with pytest.raises(ScopeConflictError) as raised:
group.refined_in(ZarrV3Context())
assert [conflict.loc for conflict in raised.value.conflicts] == [("consolidated_metadata",)]
@@ -514,9 +520,9 @@ def test_error_group_refined_in_refuses_a_scope_that_loses_an_extra_field() -> N
def test_consolidated_metadata_with_context_and_refined_in_behave_as_the_arrays_do() -> None:
"""`refined_in` moves every document up the order. `with_context` reads every document in another scope."""
held = ZarrV3ConsolidatedMetadata(MEMBER, context=ZarrV3Context())
- refined = held.refined_in(CORE)
+ refined = held.refined_in(CORE_CONTEXT)
assert refined.refines(held)
- assert refined.context == CORE
+ assert refined.context == CORE_CONTEXT
array = refined.metadata["a"]
assert isinstance(array, ZarrV3ArrayMetadata)
assert isinstance(array.codecs[0], AcceptedField)
@@ -527,7 +533,7 @@ def test_consolidated_metadata_with_context_and_refined_in_behave_as_the_arrays_
def test_error_consolidated_metadata_refined_in_refuses_a_conflict() -> None:
"""`refined_in` raises `ScopeConflictError` for a scope that reads a name of a listed document by another definition, and locates each conflict under that document's path."""
- held = ZarrV3ConsolidatedMetadata(MEMBER, context=CORE)
+ held = ZarrV3ConsolidatedMetadata(MEMBER, context=CORE_CONTEXT)
with pytest.raises(ScopeConflictError) as raised:
held.refined_in(PRIVATE)
assert {conflict.loc for conflict in raised.value.conflicts} == {
@@ -538,12 +544,12 @@ def test_error_consolidated_metadata_refined_in_refuses_a_conflict() -> None:
def test_consolidated_metadata_reads_on_its_own() -> None:
"""`ZarrV3ConsolidatedMetadata(member, context)` reads a member as written, or as a group holds it, with each document as a model in that scope. `read_consolidated_metadata_v3` returns the reading of the same model."""
- held = ZarrV3ConsolidatedMetadata(MEMBER, context=CORE)
- group = ZarrV3GroupMetadata(CONSOLIDATED, context=CORE)
- assert held == ZarrV3ConsolidatedMetadata(group.consolidated_metadata, context=CORE)
+ held = ZarrV3ConsolidatedMetadata(MEMBER, context=CORE_CONTEXT)
+ group = ZarrV3GroupMetadata(CONSOLIDATED, context=CORE_CONTEXT)
+ assert held == ZarrV3ConsolidatedMetadata(group.consolidated_metadata, context=CORE_CONTEXT)
assert held.to_json() == refine_json(MEMBER)[0]
assert ZarrV3ConsolidatedMetadata.from_json(MEMBER) == ZarrV3ConsolidatedMetadata(MEMBER)
- reading = read_consolidated_metadata_v3(MEMBER, context=CORE)
+ reading = read_consolidated_metadata_v3(MEMBER, context=CORE_CONTEXT)
assert reading.problems == ()
assert reading.metadata == held
assert held.reading.metadata is held
@@ -575,13 +581,13 @@ def test_error_refined_in_refuses_a_gain_that_surfaces_a_problem() -> None:
"""A scope that claims a name the current scope left unclaimed may refuse what was written under that name. Then `refined_in` raises `MetadataValidationError`, because the document has a problem in that scope."""
loose = ZarrV3ArrayMetadata({**ARRAY, "fill_value": "banana"}, context=ZarrV3Context())
with pytest.raises(MetadataValidationError) as raised:
- loose.refined_in(CORE)
+ loose.refined_in(CORE_CONTEXT)
assert [problem.loc for problem in raised.value.problems] == [("fill_value",)]
def test_a_scope_conflict_says_where_each_conflict_sits() -> None:
"""`refined_in` reports each conflict with the field's location in the document, the same way a problem is located. For example, a loss of `bytes` is reported at `codecs.0`, including inside the shard."""
- model = ZarrV3ArrayMetadata(SHARDED, context=CORE)
+ model = ZarrV3ArrayMetadata(SHARDED, context=CORE_CONTEXT)
with pytest.raises(ScopeConflictError) as raised:
model.refined_in(ZarrV3Context())
assert sorted((conflict.key[1], conflict.loc) for conflict in raised.value.conflicts) == [
@@ -598,8 +604,8 @@ def test_a_scope_conflict_says_where_each_conflict_sits() -> None:
@pytest.mark.parametrize(
"build",
[
- lambda: ZarrV3ConsolidatedMetadata(MEMBER, context=CORE_AND_EXTENSIONS),
- lambda: read_consolidated_metadata_v3(MEMBER, context=CORE_AND_EXTENSIONS).metadata,
+ lambda: ZarrV3ConsolidatedMetadata(MEMBER, context=CORE_AND_EXTENSIONS_CONTEXT),
+ lambda: read_consolidated_metadata_v3(MEMBER, context=CORE_AND_EXTENSIONS_CONTEXT).metadata,
],
ids=["constructor", "reader"],
)
@@ -612,7 +618,7 @@ def test_with_context_moves_every_listed_model_without_reading_again(
assert held.reading.metadata is held
for node in held.metadata.values():
assert node.reading.metadata is node
- context = CORE.update(codecs=[*CORE.codecs.values(), ZSTD_CODEC])
+ context = CORE_CONTEXT.update(codecs=[*CORE_CONTEXT.codecs.values(), ZSTD_CODEC])
moved = held.with_context(context)
assert moved.context is context
before = held.metadata["a"]
diff --git a/packages/zarr-metadata/tests/model/test_pair_v2.py b/packages/zarr-metadata/tests/model/test_pair_v2.py
index 400ecf9c6a..5a95aa2bc8 100644
--- a/packages/zarr-metadata/tests/model/test_pair_v2.py
+++ b/packages/zarr-metadata/tests/model/test_pair_v2.py
@@ -29,7 +29,7 @@
from zarr_metadata.v2.codec.compression import ZLIB_CODEC_V2
from zarr_metadata.v2.data_type.scalar import FLOAT_DATA_TYPE_V2, UINT_DATA_TYPE_V2
from zarr_metadata.v2.definition import (
- CORE_V2,
+ CORE_V2_CONTEXT,
ZarrV2CodecDefinition,
ZarrV2Context,
)
@@ -49,7 +49,7 @@
}
BARE_ZLIB = ZarrV2CodecDefinition(name="zlib", configuration=EmptyConfiguration)
SMALL = ZarrV2Context(data_types=[FLOAT_DATA_TYPE_V2, UINT_DATA_TYPE_V2])
-PRIVATE = CORE_V2.update(codecs={**CORE_V2.codecs, "zlib": BARE_ZLIB})
+PRIVATE = CORE_V2_CONTEXT.update(codecs={**CORE_V2_CONTEXT.codecs, "zlib": BARE_ZLIB})
def _document(changes: dict[str, Any]) -> dict[str, Any]:
@@ -58,9 +58,9 @@ def _document(changes: dict[str, Any]) -> dict[str, Any]:
def test_a_model_is_its_document_read_in_its_scope() -> None:
- """The constructor reads the document in the given scope (`CORE_V2` by default). `to_json` returns the refined document (arrays as tuples), which shares nothing with the model. A missing `dimension_separator` stays missing. `context` is the scope, and the reading holds the model."""
+ """The constructor reads the document in the given scope (`CORE_V2_CONTEXT` by default). `to_json` returns the refined document (arrays as tuples), which shares nothing with the model. A missing `dimension_separator` stays missing. `context` is the scope, and the reading holds the model."""
model = ZarrV2ArrayMetadata(ARRAY)
- assert model.context is CORE_V2
+ assert model.context is CORE_V2_CONTEXT
document = model.to_json()
assert document["shape"] == (4,)
assert document.get("attributes") == {"a": (1, 2)}
@@ -153,7 +153,9 @@ def test_a_model_read_in_two_scopes_that_read_it_alike_is_one_model() -> None:
"""Equality depends on interpretation. The same document read with a private `zlib` that takes no parameters and with the core `zlib` gives two different models. Read in two scopes that hold the same `zlib`, it gives equal models."""
document = {**ARRAY, "compressor": {"id": "zlib"}}
assert ZarrV2ArrayMetadata(document) != ZarrV2ArrayMetadata(document, PRIVATE)
- same = ZarrV2Context(data_types=CORE_V2.data_types.values(), codecs=CORE_V2.codecs.values())
+ same = ZarrV2Context(
+ data_types=CORE_V2_CONTEXT.data_types.values(), codecs=CORE_V2_CONTEXT.codecs.values()
+ )
assert ZarrV2ArrayMetadata(document) == ZarrV2ArrayMetadata(document, same)
@@ -227,7 +229,7 @@ def test_with_context_and_refined_in_read_the_document_in_another_scope() -> Non
assert gained.refines(unclaimed)
assert not unclaimed.refines(gained)
with pytest.raises(ScopeConflictError) as conflict:
- gained.refined_in(CORE_V2)
+ gained.refined_in(CORE_V2_CONTEXT)
assert [c.loc for c in conflict.value.conflicts] == [("compressor",)]
lost = gained.with_context(SMALL)
assert isinstance(lost.compressor, UnclaimedField)
@@ -407,7 +409,7 @@ def test_consolidated_metadata_is_equal_by_its_nodes_and_moves_scope_with_them()
assert gained.refines(small)
assert not small.refines(gained)
with pytest.raises(ScopeConflictError) as conflict:
- gained.refined_in(CORE_V2)
+ gained.refined_in(CORE_V2_CONTEXT)
assert [c.loc for c in conflict.value.conflicts] == [("metadata", "a/.zarray", "compressor")]
assert gained.with_context(SMALL) == small
@@ -463,7 +465,7 @@ def test_error_a_conflict_is_located_at_the_key_the_document_writes() -> None:
{"zarr_consolidated_format": 1, "metadata": {"a/.zarray": ZARRAY}}, PRIVATE
)
with pytest.raises(ScopeConflictError) as conflict:
- gained.refined_in(CORE_V2)
+ gained.refined_in(CORE_V2_CONTEXT)
assert [c.loc for c in conflict.value.conflicts] == [("metadata", "a/.zarray", "compressor")]
diff --git a/packages/zarr-metadata/tests/model/test_pydantic_module.py b/packages/zarr-metadata/tests/model/test_pydantic_module.py
index 1458f8be7a..49b6cd0c73 100644
--- a/packages/zarr-metadata/tests/model/test_pydantic_module.py
+++ b/packages/zarr-metadata/tests/model/test_pydantic_module.py
@@ -31,10 +31,10 @@
from zarr_metadata.problems import ValidationProblem
from zarr_metadata.typed_json import JSONValue
from zarr_metadata.v2.array import ZarrV2ArrayMetadataJSON
-from zarr_metadata.v2.definition import CORE_V2, ZarrV2Context
+from zarr_metadata.v2.definition import CORE_V2_CONTEXT, ZarrV2Context
from zarr_metadata.v2.group import ZarrV2GroupMetadataJSON
from zarr_metadata.v3.codec.gzip import GZIP_CODEC
-from zarr_metadata.v3.definition import CORE, CORE_AND_EXTENSIONS, ZarrV3Context
+from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS_CONTEXT, CORE_CONTEXT, ZarrV3Context
V3_ARRAY_DOC = dict(ZarrV3ArrayMetadata.create_default(shape=(4,)).to_json())
V2_ARRAY_DOC = dict(ZarrV2ArrayMetadata.create_default(shape=(4,), chunks=(2,)).to_json())
@@ -314,8 +314,8 @@ class Manifest(BaseModel):
("context", "read"),
[
(None, True),
- (CORE, False),
- ({zmp.CONTEXT_KEY: CORE}, False),
+ (CORE_CONTEXT, False),
+ ({zmp.CONTEXT_KEY: CORE_CONTEXT}, False),
({"another validator's": 1}, True),
],
ids=["none", "a-scope", "a-mapping-holding-one", "a-mapping-holding-none"],
@@ -323,24 +323,28 @@ class Manifest(BaseModel):
def test_a_v3_field_type_reads_in_the_scope_the_validation_context_holds(
context: object, read: bool
) -> None:
- """The field type reads in the scope from the validation context, which pydantic passes to every validator. `zstd` is an extension, so `CORE` leaves it unclaimed."""
+ """The field type reads in the scope from the validation context, which pydantic passes to every validator. `zstd` is an extension, so `CORE_CONTEXT` leaves it unclaimed."""
model = TypeAdapter(zmp.ZarrV3ArrayMetadataField).validate_python(_WITH_ZSTD, context=context)
assert isinstance(model.codecs[1], AcceptedField) is read
class _Bound(BaseModel):
- """A model whose field is read in `CORE`, whatever the validation context holds."""
+ """A model whose field is read in `CORE_CONTEXT`, whatever the validation context holds."""
- metadata: Annotated[ZarrV3ArrayMetadata, zmp.InScope(CORE)]
+ metadata: Annotated[ZarrV3ArrayMetadata, zmp.InScope(CORE_CONTEXT)]
def test_a_field_bound_to_a_scope_reads_in_that_scope() -> None:
- """`Annotated[ZarrV3ArrayMetadata, InScope(CORE)]` reads a document in `CORE`, whatever the validation context holds, and reads an instance again in `CORE`: an instance read in `CORE` with `zstd` unclaimed stays as it is. The field writes the document as written, and a type checker sees the model class."""
- for context in (None, CORE_AND_EXTENSIONS, {zmp.CONTEXT_KEY: CORE_AND_EXTENSIONS}):
+ """`Annotated[ZarrV3ArrayMetadata, InScope(CORE_CONTEXT)]` reads a document in `CORE_CONTEXT`, whatever the validation context holds, and reads an instance again in `CORE_CONTEXT`: an instance read in `CORE_CONTEXT` with `zstd` unclaimed stays as it is. The field writes the document as written, and a type checker sees the model class."""
+ for context in (
+ None,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ {zmp.CONTEXT_KEY: CORE_AND_EXTENSIONS_CONTEXT},
+ ):
read = _Bound.model_validate({"metadata": _WITH_ZSTD}, context=context).metadata
- assert read.context == CORE
+ assert read.context == CORE_CONTEXT
assert isinstance(read.codecs[1], UnclaimedField)
- held = ZarrV3ArrayMetadata(_WITH_ZSTD, context=CORE)
+ held = ZarrV3ArrayMetadata(_WITH_ZSTD, context=CORE_CONTEXT)
assert _Bound(metadata=held).metadata == held
assert _Bound(metadata=held).model_dump()["metadata"] == held.to_json()
@@ -349,16 +353,22 @@ def test_a_field_bound_to_a_scope_reads_in_that_scope() -> None:
@pytest.mark.parametrize(
("bound", "field_type"),
[
- (Annotated[ZarrV3ArrayMetadata, zmp.InScope(CORE)], zmp.ZarrV3ArrayMetadataField),
- (Annotated[ZarrV3GroupMetadata, zmp.InScope(CORE)], zmp.ZarrV3GroupMetadataField),
+ (Annotated[ZarrV3ArrayMetadata, zmp.InScope(CORE_CONTEXT)], zmp.ZarrV3ArrayMetadataField),
+ (Annotated[ZarrV3GroupMetadata, zmp.InScope(CORE_CONTEXT)], zmp.ZarrV3GroupMetadataField),
(
- Annotated[ZarrV3ConsolidatedMetadata, zmp.InScope(CORE)],
+ Annotated[ZarrV3ConsolidatedMetadata, zmp.InScope(CORE_CONTEXT)],
zmp.ZarrV3ConsolidatedMetadataField,
),
- (Annotated[ZarrV2ArrayMetadata, zmp.InScope(CORE_V2)], zmp.ZarrV2ArrayMetadataField),
- (Annotated[ZarrV2GroupMetadata, zmp.InScope(CORE_V2)], zmp.ZarrV2GroupMetadataField),
(
- Annotated[ZarrV2ConsolidatedMetadata, zmp.InScope(CORE_V2)],
+ Annotated[ZarrV2ArrayMetadata, zmp.InScope(CORE_V2_CONTEXT)],
+ zmp.ZarrV2ArrayMetadataField,
+ ),
+ (
+ Annotated[ZarrV2GroupMetadata, zmp.InScope(CORE_V2_CONTEXT)],
+ zmp.ZarrV2GroupMetadataField,
+ ),
+ (
+ Annotated[ZarrV2ConsolidatedMetadata, zmp.InScope(CORE_V2_CONTEXT)],
zmp.ZarrV2ConsolidatedMetadataField,
),
],
@@ -383,24 +393,24 @@ def test_a_field_bound_to_a_scope_has_the_schema_of_its_field_type(
def test_error_a_field_bound_to_a_scope_refuses_an_instance_it_reads_differently() -> None:
- """An instance read where `zstd` is claimed would lose that meaning in `CORE`, so a field bound to `CORE` refuses it with a `ValidationError`, rather than passing it through in its own scope."""
+ """An instance read where `zstd` is claimed would lose that meaning in `CORE_CONTEXT`, so a field bound to `CORE_CONTEXT` refuses it with a `ValidationError`, rather than passing it through in its own scope."""
with pytest.raises(ValidationError):
- _Bound(metadata=ZarrV3ArrayMetadata(_WITH_ZSTD, context=CORE_AND_EXTENSIONS))
+ _Bound(metadata=ZarrV3ArrayMetadata(_WITH_ZSTD, context=CORE_AND_EXTENSIONS_CONTEXT))
def test_error_a_field_bound_to_a_scope_of_the_other_format() -> None:
"""A field of a v3 model bound to a v2 scope is refused when pydantic builds its schema, as every v3 reader refuses a v2 scope."""
with pytest.raises(TypeError, match="v3 document is read in a scope of that format"):
- TypeAdapter(Annotated[ZarrV3ArrayMetadata, zmp.InScope(CORE_V2)])
+ TypeAdapter(Annotated[ZarrV3ArrayMetadata, zmp.InScope(CORE_V2_CONTEXT)])
def test_error_a_scope_given_to_validation_reads_an_instance_in_it() -> None:
"""A scope given in the validation context reads an instance again in that scope, as a bound field does, so an instance that would lose a meaning there is refused. With no scope given, an instance passes through unchanged."""
adapter = TypeAdapter(zmp.ZarrV3ArrayMetadataField)
- instance = ZarrV3ArrayMetadata(_WITH_ZSTD, context=CORE_AND_EXTENSIONS)
+ instance = ZarrV3ArrayMetadata(_WITH_ZSTD, context=CORE_AND_EXTENSIONS_CONTEXT)
assert adapter.validate_python(instance) is instance
with pytest.raises(ValidationError):
- adapter.validate_python(instance, context=CORE)
+ adapter.validate_python(instance, context=CORE_CONTEXT)
_READ_IN_SCOPE = [
@@ -430,7 +440,7 @@ def test_error_an_instance_a_scope_finds_problems_in_reports_each_one(validate:
)
instance = ZarrV3ArrayMetadata(document, context=ZarrV3Context())
with pytest.raises(ValidationError) as raised:
- validate(instance, CORE_AND_EXTENSIONS)
+ validate(instance, CORE_AND_EXTENSIONS_CONTEXT)
assert [(line["type"], line["loc"], line["input"]) for line in raised.value.errors()] == [
("invalid_value", ("codecs", 1, "configuration", "level"), 99),
("invalid_type", ("codecs", 2, "configuration", "level"), "x"),
@@ -439,10 +449,10 @@ def test_error_an_instance_a_scope_finds_problems_in_reports_each_one(validate:
@pytest.mark.parametrize("validate", _READ_IN_SCOPE)
def test_error_an_instance_a_scope_reads_differently_reports_each_conflict(validate: Any) -> None: # noqa: ANN401 - a validating function
- """An instance read where `zstd` is claimed would lose that meaning in `CORE`. pydantic gets one `scope_conflict` line error per conflict, at the field it is in, with that field as written as its input, and a message that names the codec."""
- instance = ZarrV3ArrayMetadata(_WITH_ZSTD, context=CORE_AND_EXTENSIONS)
+ """An instance read where `zstd` is claimed would lose that meaning in `CORE_CONTEXT`. pydantic gets one `scope_conflict` line error per conflict, at the field it is in, with that field as written as its input, and a message that names the codec."""
+ instance = ZarrV3ArrayMetadata(_WITH_ZSTD, context=CORE_AND_EXTENSIONS_CONTEXT)
with pytest.raises(ValidationError) as raised:
- validate(instance, CORE)
+ validate(instance, CORE_CONTEXT)
(line,) = raised.value.errors()
assert (line["type"], line["loc"]) == ("scope_conflict", ("codecs", 1))
assert line["input"] == instance.to_json()["codecs"][1]
@@ -453,7 +463,7 @@ def test_error_an_instance_a_scope_reads_differently_reports_each_conflict(valid
_V3_LISTING_WITH_ZSTD = ZarrV3ConsolidatedMetadata(
- {**V3_CONSOLIDATED_DOC, "metadata": {"a": _WITH_ZSTD}}, context=CORE_AND_EXTENSIONS
+ {**V3_CONSOLIDATED_DOC, "metadata": {"a": _WITH_ZSTD}}, context=CORE_AND_EXTENSIONS_CONTEXT
)
_V2_LISTING_WITH_ZLIB = ZarrV2ConsolidatedMetadata(
{
@@ -463,9 +473,9 @@ def test_error_an_instance_a_scope_reads_differently_reports_each_conflict(valid
"a/.zarray": {**V2_ARRAY_DOC, "compressor": _ZLIB_V2},
},
},
- context=CORE_V2,
+ context=CORE_V2_CONTEXT,
)
-_V2_WITHOUT_CODECS = ZarrV2Context(data_types=CORE_V2.data_types.values())
+_V2_WITHOUT_CODECS = ZarrV2Context(data_types=CORE_V2_CONTEXT.data_types.values())
@pytest.mark.parametrize(
@@ -473,7 +483,7 @@ def test_error_an_instance_a_scope_reads_differently_reports_each_conflict(valid
[
pytest.param(
lambda value: TypeAdapter(zmp.ZarrV3ConsolidatedMetadataField).validate_python(
- value, context=CORE
+ value, context=CORE_CONTEXT
),
_V3_LISTING_WITH_ZSTD,
("metadata", "a", "codecs", 1),
@@ -483,7 +493,7 @@ def test_error_an_instance_a_scope_reads_differently_reports_each_conflict(valid
),
pytest.param(
lambda value: TypeAdapter(
- Annotated[ZarrV3ConsolidatedMetadata, zmp.InScope(CORE)]
+ Annotated[ZarrV3ConsolidatedMetadata, zmp.InScope(CORE_CONTEXT)]
).validate_python(value),
_V3_LISTING_WITH_ZSTD,
("metadata", "a", "codecs", 1),
@@ -532,8 +542,8 @@ def test_error_a_consolidated_instance_a_scope_reads_differently_reports_each_co
@pytest.mark.parametrize(
"annotation",
[
- Annotated[ZarrV3ArrayMetadata | None, zmp.InScope(CORE)],
- Annotated[dict[str, Any], zmp.InScope(CORE)],
+ Annotated[ZarrV3ArrayMetadata | None, zmp.InScope(CORE_CONTEXT)],
+ Annotated[dict[str, Any], zmp.InScope(CORE_CONTEXT)],
],
ids=["an-optional-model", "a-dict"],
)
@@ -553,14 +563,14 @@ def test_error_a_validation_context_names_a_key_of_this_package_it_does_not_have
"""A mapping validation context with a key that starts like this package's keys, but is not one of them, is a `TypeError`, so a misspelled key is not read as no scope at all."""
with pytest.raises(TypeError, match="zarr_metadata_contxt"):
TypeAdapter(zmp.ZarrV3ArrayMetadataField).validate_python(
- _WITH_ZSTD, context={"zarr_metadata_contxt": CORE}
+ _WITH_ZSTD, context={"zarr_metadata_contxt": CORE_CONTEXT}
)
def test_error_a_validation_context_holds_a_scope_that_is_not_one() -> None:
with pytest.raises(TypeError, match=zmp.CONTEXT_KEY):
TypeAdapter(zmp.ZarrV3ArrayMetadataField).validate_python(
- _WITH_ZSTD, context={zmp.CONTEXT_KEY: "CORE"}
+ _WITH_ZSTD, context={zmp.CONTEXT_KEY: "CORE_CONTEXT"}
)
@@ -684,14 +694,14 @@ def test_the_pydantic_schema_names_an_extension_as_the_reader_does(field: object
def test_v2_field_types_read_in_the_validation_contexts_scope() -> None:
- """The v2 field types read a document in the scope that the validation context holds. The context is either a `Context` itself or holds one in its `zarr_metadata_context_v2` item. When it holds none, they read in `CORE_V2`. The v3 field types work the same way."""
+ """The v2 field types read a document in the scope that the validation context holds. The context is either a `Context` itself or holds one in its `zarr_metadata_context_v2` item. When it holds none, they read in `CORE_V2_CONTEXT`. The v3 field types work the same way."""
from zarr_metadata.definition import UnclaimedField
from zarr_metadata.v2.data_type.scalar import UINT_DATA_TYPE_V2
- from zarr_metadata.v2.definition import CORE_V2, ZarrV2Context
+ from zarr_metadata.v2.definition import CORE_V2_CONTEXT, ZarrV2Context
adapter = TypeAdapter(zmp.ZarrV2ArrayMetadataField)
doc = json.loads(json.dumps(V2_ARRAY_DOC))
- assert adapter.validate_python(doc).context is CORE_V2
+ assert adapter.validate_python(doc).context is CORE_V2_CONTEXT
small = ZarrV2Context(data_types=[UINT_DATA_TYPE_V2])
read = adapter.validate_python({**doc, "compressor": {"id": "zlib"}}, context=small)
assert read.context is small
@@ -703,20 +713,23 @@ def test_v2_field_types_read_in_the_validation_contexts_scope() -> None:
def test_each_format_reads_in_its_own_context_key() -> None:
- """A mapping context gives each format's scope under its own key: `zarr_metadata_context` for v3 and `zarr_metadata_context_v2` for v2. So a v3 scope given for the v3 fields leaves the v2 fields in `CORE_V2`. A bare `Context` is the scope for every field type."""
+ """A mapping context gives each format's scope under its own key: `zarr_metadata_context` for v3 and `zarr_metadata_context_v2` for v2. So a v3 scope given for the v3 fields leaves the v2 fields in `CORE_V2_CONTEXT`. A bare `Context` is the scope for every field type."""
from zarr_metadata.definition import UnclaimedField
from zarr_metadata.v2.data_type.scalar import UINT_DATA_TYPE_V2
- from zarr_metadata.v2.definition import CORE_V2, ZarrV2Context
- from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS
+ from zarr_metadata.v2.definition import CORE_V2_CONTEXT, ZarrV2Context
+ from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS_CONTEXT
adapter = TypeAdapter(zmp.ZarrV2ArrayMetadataField)
doc = json.loads(json.dumps(V2_ARRAY_DOC))
small = ZarrV2Context(data_types=[UINT_DATA_TYPE_V2])
- v3_only = adapter.validate_python(doc, context={"zarr_metadata_context": CORE_AND_EXTENSIONS})
- assert v3_only.context is CORE_V2
+ v3_only = adapter.validate_python(
+ doc, context={"zarr_metadata_context": CORE_AND_EXTENSIONS_CONTEXT}
+ )
+ assert v3_only.context is CORE_V2_CONTEXT
with pytest.raises(ValidationError):
adapter.validate_python(
- {**doc, "fill_value": "garbage"}, context={"zarr_metadata_context": CORE_AND_EXTENSIONS}
+ {**doc, "fill_value": "garbage"},
+ context={"zarr_metadata_context": CORE_AND_EXTENSIONS_CONTEXT},
)
own = adapter.validate_python(doc, context={"zarr_metadata_context_v2": small})
assert own.context is small
diff --git a/packages/zarr-metadata/tests/model/test_read_array_metadata.py b/packages/zarr-metadata/tests/model/test_read_array_metadata.py
index 921aa19aae..7335a9abce 100644
--- a/packages/zarr-metadata/tests/model/test_read_array_metadata.py
+++ b/packages/zarr-metadata/tests/model/test_read_array_metadata.py
@@ -31,8 +31,8 @@
)
from zarr_metadata.problems import ValidationProblem
from zarr_metadata.v3.definition import (
- CORE,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
ChunkGridDefinition,
ChunkKeyEncodingDefinition,
CodecDefinition,
@@ -82,13 +82,18 @@ def _shard(chunk_shape: list[int], codecs: list[object] | None = None, **more: o
@pytest.mark.parametrize(
("document", "context", "fields", "handed"),
[
- (_document(), CORE_AND_EXTENSIONS, [*POINTS, _codec(("codecs", 0))], [(frozenset({4}),)]),
+ (
+ _document(),
+ CORE_AND_EXTENSIONS_CONTEXT,
+ [*POINTS, _codec(("codecs", 0))],
+ [(frozenset({4}),)],
+ ),
# Each codec receives the chunk that the previous codec outputs.
(
_document(
(4, 6), codecs=[{"name": "transpose", "configuration": {"order": [1, 0]}}, "bytes"]
),
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
[*POINTS, _codec(("codecs", 0)), _codec(("codecs", 1))],
[(frozenset({4}), frozenset({6})), (frozenset({6}), frozenset({4}))],
),
@@ -101,7 +106,7 @@ def _shard(chunk_shape: list[int], codecs: list[object] | None = None, **more: o
chunk_grid={"name": "regular", "configuration": {"chunk_shape": [8, 8]}},
codecs=[_shard([4, 4], codecs=[LITTLE, ZSTD], index_codecs=[LITTLE, "crc32c"])],
),
- CORE,
+ CORE_CONTEXT,
[
*POINTS,
_codec(("codecs", 0)),
@@ -119,7 +124,7 @@ def _shard(chunk_shape: list[int], codecs: list[object] | None = None, **more: o
chunk_grid={"name": "regular", "configuration": {"chunk_shape": [8, 8]}},
codecs=[_shard([4, 4], codecs=[_shard([2, 2])])],
),
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
[
*POINTS,
_codec(("codecs", 0)),
@@ -139,7 +144,7 @@ def _shard(chunk_shape: list[int], codecs: list[object] | None = None, **more: o
chunk_grid={"name": "regular", "configuration": {"chunk_shape": [8, 8]}},
codecs=[_shard([4, 4], index_location="middle", codecs=[LITTLE, ZSTD])],
),
- CORE,
+ CORE_CONTEXT,
[
*POINTS,
_codec(("codecs", 0), RefusedField),
@@ -165,7 +170,7 @@ def _shard(chunk_shape: list[int], codecs: list[object] | None = None, **more: o
fill_value={"a": 0, "b": "anything"},
codecs=[LITTLE],
),
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
[
POINTS[0],
(
@@ -192,7 +197,7 @@ def _shard(chunk_shape: list[int], codecs: list[object] | None = None, **more: o
codecs=["bytes", {"name": "gzip", "configuration": {"level": 99}}],
storage_transformers=[{"name": "acme.transformer"}],
),
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
[
POINTS[0],
(("chunk_grid",), ChunkGridDefinition, UnclaimedField),
@@ -206,7 +211,7 @@ def _shard(chunk_shape: list[int], codecs: list[object] | None = None, **more: o
# A data type that is not JSON is refused.
(
_document(data_type=float("nan")),
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
[
(("data_type",), DataTypeDefinition, RefusedField),
*POINTS[1:],
@@ -273,7 +278,7 @@ def _field_fields(
resolved, problems = resolve(
cast("dict[str, Any]", document)["codecs"][0],
CodecDefinition,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
("codecs", 0),
)
return with_problems(fields_of(resolved, ("codecs", 0)), problems)
diff --git a/packages/zarr-metadata/tests/model/test_read_array_metadata_v2.py b/packages/zarr-metadata/tests/model/test_read_array_metadata_v2.py
index afbd7fe0da..db8de96677 100644
--- a/packages/zarr-metadata/tests/model/test_read_array_metadata_v2.py
+++ b/packages/zarr-metadata/tests/model/test_read_array_metadata_v2.py
@@ -21,7 +21,7 @@
from zarr_metadata.v2.codec.compression import ZLIB_CODEC_V2
from zarr_metadata.v2.data_type.scalar import FLOAT_DATA_TYPE_V2
from zarr_metadata.v2.definition import (
- CORE_V2,
+ CORE_V2_CONTEXT,
ZarrV2CodecDefinition,
ZarrV2Context,
)
@@ -75,7 +75,7 @@ def test_a_reading_holds_each_field_as_the_scope_read_it(
filters: object,
locs: list[Loc],
) -> None:
- """`read_array_metadata_v2` reads the document once in the given scope (`CORE_V2` by default). Each of `dtype`, `compressor` and `filters` is an `AcceptedField`, an `UnclaimedField`, or None, depending on what the document contains. `fields()` returns them in document order, with a struct's record types after the struct. A valid document produces no problems."""
+ """`read_array_metadata_v2` reads the document once in the given scope (`CORE_V2_CONTEXT` by default). Each of `dtype`, `compressor` and `filters` is an `AcceptedField`, an `UnclaimedField`, or None, depending on what the document contains. `fields()` returns them in document order, with a struct's record types after the struct. A valid document produces no problems."""
reading = read_array_metadata_v2({**BASE, **changes}, context=context)
assert reading.problems == ()
assert type(reading.dtype) is dtype
@@ -125,7 +125,7 @@ def test_the_v2_readers_read_in_the_scope_given() -> None:
doc = {**BASE, "compressor": {"id": "zlib", "level": 1}}
assert read_array_metadata_v2(doc).problems == ()
found = read_array_metadata_v2(
- doc, context=CORE_V2.update(codecs={**CORE_V2.codecs, "zlib": bare})
+ doc, context=CORE_V2_CONTEXT.update(codecs={**CORE_V2_CONTEXT.codecs, "zlib": bare})
).problems
assert [p.loc for p in found] == [("compressor", "level")]
diff --git a/packages/zarr-metadata/tests/model/test_scope_threading.py b/packages/zarr-metadata/tests/model/test_scope_threading.py
index 1a8192d185..2efb9ae2be 100644
--- a/packages/zarr-metadata/tests/model/test_scope_threading.py
+++ b/packages/zarr-metadata/tests/model/test_scope_threading.py
@@ -1,4 +1,4 @@
-"""Tests that every entry point reads in the scope it is given, and in `CORE_AND_EXTENSIONS` when given none."""
+"""Tests that every entry point reads in the scope it is given, and in `CORE_AND_EXTENSIONS_CONTEXT` when given none."""
from __future__ import annotations
@@ -13,7 +13,7 @@
ZarrV3ArrayMetadata,
)
from zarr_metadata.problems import MetadataValidationError
-from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS, ZarrV3Context
+from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS_CONTEXT, ZarrV3Context
if TYPE_CHECKING:
from collections.abc import Callable
@@ -110,7 +110,7 @@ def test_every_entry_point_reads_in_the_scope_it_is_given(read: Callable[..., ob
"""Each entry point refuses a gzip level of 12 in the default scope (also selected by `None`), and accepts it in a scope that leaves gzip unclaimed. This shows that each entry point reads in the scope it is given."""
accepted = _accepts(read)
assert accepted(EMPTY) is True
- assert accepted(CORE_AND_EXTENSIONS) is False
+ assert accepted(CORE_AND_EXTENSIONS_CONTEXT) is False
assert accepted(None) is False
@@ -119,7 +119,7 @@ def test_a_group_reads_the_same_in_every_scope(read: Callable[..., object]) -> N
"""A group document has no fields, and a group does not read the documents its consolidated metadata lists. So each group entry point accepts a group that lists `BAD`, in every scope."""
accepted = _accepts(read)
assert accepted(EMPTY) is True
- assert accepted(CORE_AND_EXTENSIONS) is True
+ assert accepted(CORE_AND_EXTENSIONS_CONTEXT) is True
assert accepted(None) is True
@@ -138,10 +138,10 @@ def test_error_every_entry_point_refuses_a_scope_of_another_format(
read: Callable[..., object],
) -> None:
"""A v3 entry point given a v2 scope raises `TypeError`. A scope reads documents of one format, and a v2 scope claims nothing that a v3 document contains."""
- from zarr_metadata.v2.definition import CORE_V2
+ from zarr_metadata.v2.definition import CORE_V2_CONTEXT
with pytest.raises(TypeError, match="format"):
- read(context=CORE_V2)
+ read(context=CORE_V2_CONTEXT)
NOT_JSON = b"{not json"
@@ -164,9 +164,9 @@ def test_error_a_v3_store_reader_refuses_a_scope_of_another_format_before_decodi
name: str,
) -> None:
"""A v3 entry point that reads a store's bytes raises `TypeError` for a v2 scope before it decodes the bytes, so the wrong scope is reported even when the bytes are not JSON. With a v3 scope, the same bytes raise `MetadataValidationError`."""
- from zarr_metadata.v2.definition import CORE_V2
+ from zarr_metadata.v2.definition import CORE_V2_CONTEXT
with pytest.raises(TypeError, match="format"):
- STORE_READERS[name](context=CORE_V2)
+ STORE_READERS[name](context=CORE_V2_CONTEXT)
with pytest.raises(MetadataValidationError):
- STORE_READERS[name](context=CORE_AND_EXTENSIONS)
+ STORE_READERS[name](context=CORE_AND_EXTENSIONS_CONTEXT)
diff --git a/packages/zarr-metadata/tests/model/test_scope_threading_v2.py b/packages/zarr-metadata/tests/model/test_scope_threading_v2.py
index 677889ec50..2a7e48d6b2 100644
--- a/packages/zarr-metadata/tests/model/test_scope_threading_v2.py
+++ b/packages/zarr-metadata/tests/model/test_scope_threading_v2.py
@@ -1,4 +1,4 @@
-"""Tests that every v2 entry point reads in the scope it is given, uses `CORE_V2` when given none, and refuses a scope of another format."""
+"""Tests that every v2 entry point reads in the scope it is given, uses `CORE_V2_CONTEXT` when given none, and refuses a scope of another format."""
from __future__ import annotations
@@ -11,10 +11,10 @@
from zarr_metadata.model import ZarrV2ArrayMetadata
from zarr_metadata.problems import MetadataValidationError
from zarr_metadata.v2.definition import (
- CORE_V2,
+ CORE_V2_CONTEXT,
ZarrV2Context,
)
-from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS
+from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS_CONTEXT
if TYPE_CHECKING:
from collections.abc import Callable
@@ -116,10 +116,10 @@ def accepted(context: Context | None) -> bool:
@pytest.mark.parametrize("name", ENTRY_POINTS.keys())
def test_every_v2_entry_point_reads_in_the_scope_it_is_given(name: str) -> None:
- """Each v2 entry point accepts its document in an empty scope. In `CORE_V2` (also selected by `None`), it refuses the non-integer gzip level when the document contains it. This shows that each entry point reads in the scope it is given."""
+ """Each v2 entry point accepts its document in an empty scope. In `CORE_V2_CONTEXT` (also selected by `None`), it refuses the non-integer gzip level when the document contains it. This shows that each entry point reads in the scope it is given."""
accepted = _accepts(ENTRY_POINTS[name])
assert accepted(EMPTY) is True
- assert accepted(CORE_V2) is (name not in REFUSES_BAD)
+ assert accepted(CORE_V2_CONTEXT) is (name not in REFUSES_BAD)
assert accepted(None) is (name not in REFUSES_BAD)
@@ -127,7 +127,7 @@ def test_every_v2_entry_point_reads_in_the_scope_it_is_given(name: str) -> None:
def test_error_every_v2_entry_point_refuses_a_scope_of_another_format(name: str) -> None:
"""A v2 entry point given a v3 scope raises `TypeError`. A scope reads documents of one format, and a v3 scope claims nothing that a v2 document contains."""
with pytest.raises(TypeError, match="format"):
- ENTRY_POINTS[name](context=CORE_AND_EXTENSIONS)
+ ENTRY_POINTS[name](context=CORE_AND_EXTENSIONS_CONTEXT)
NOT_JSON = b"{not json"
@@ -156,6 +156,6 @@ def test_error_a_v2_store_reader_refuses_a_scope_of_another_format_before_decodi
) -> None:
"""A v2 entry point that reads a store's bytes raises `TypeError` for a v3 scope before it decodes the bytes, so the wrong scope is reported even when the bytes are not JSON. With a v2 scope, the same bytes raise `MetadataValidationError`."""
with pytest.raises(TypeError, match="format"):
- STORE_READERS[name](context=CORE_AND_EXTENSIONS)
+ STORE_READERS[name](context=CORE_AND_EXTENSIONS_CONTEXT)
with pytest.raises(MetadataValidationError):
- STORE_READERS[name](context=CORE_V2)
+ STORE_READERS[name](context=CORE_V2_CONTEXT)
diff --git a/packages/zarr-metadata/tests/model/test_unclaimed_fields.py b/packages/zarr-metadata/tests/model/test_unclaimed_fields.py
index cef80f6c70..332ac4618f 100644
--- a/packages/zarr-metadata/tests/model/test_unclaimed_fields.py
+++ b/packages/zarr-metadata/tests/model/test_unclaimed_fields.py
@@ -16,7 +16,7 @@
ZarrV3GroupMetadata,
ZarrV3NodeMetadata,
)
-from zarr_metadata.v3 import CORE
+from zarr_metadata.v3 import CORE_CONTEXT
from zarr_metadata.v3.definition import (
ArrayExtraFieldDefinition,
GroupExtraFieldDefinition,
@@ -36,21 +36,21 @@ class _MirrorConfiguration(TypedDict, closed=True):
must_understand: NotRequired[bool]
-MIRRORING = CORE.update(
+MIRRORING = CORE_CONTEXT.update(
array_extra_fields=[
- *CORE.array_extra_fields.values(),
+ *CORE_CONTEXT.array_extra_fields.values(),
ArrayExtraFieldDefinition(name="acme.mirror", configuration=_MirrorConfiguration),
]
)
-"""`CORE`, with a definition of an `acme.mirror` member that holds a codec."""
+"""`CORE_CONTEXT`, with a definition of an `acme.mirror` member that holds a codec."""
-GROUP_MIRRORING = CORE.update(
+GROUP_MIRRORING = CORE_CONTEXT.update(
group_extra_fields=[
- *CORE.group_extra_fields.values(),
+ *CORE_CONTEXT.group_extra_fields.values(),
GroupExtraFieldDefinition(name="acme.mirror", configuration=_MirrorConfiguration),
]
)
-"""`CORE`, with a definition of an `acme.mirror` member of a group that holds a codec."""
+"""`CORE_CONTEXT`, with a definition of an `acme.mirror` member of a group that holds a codec."""
SHARD = {
"name": "sharding_indexed",
@@ -65,15 +65,23 @@ class _MirrorConfiguration(TypedDict, closed=True):
@pytest.mark.parametrize(
("model", "unclaimed"),
[
- (ZarrV3ArrayMetadata(ARRAY, context=CORE), set()),
- (ZarrV3ArrayMetadata({**ARRAY, "codecs": ("bytes", ZSTD)}, context=CORE), {("codecs", 1)}),
+ (ZarrV3ArrayMetadata(ARRAY, context=CORE_CONTEXT), set()),
(
- ZarrV3ArrayMetadata({**ARRAY, "codecs": (SHARD,)}, context=CORE),
+ ZarrV3ArrayMetadata({**ARRAY, "codecs": ("bytes", ZSTD)}, context=CORE_CONTEXT),
+ {("codecs", 1)},
+ ),
+ (
+ ZarrV3ArrayMetadata({**ARRAY, "codecs": (SHARD,)}, context=CORE_CONTEXT),
{("codecs", 0, "configuration", "codecs", 1)},
),
- (ZarrV3ArrayMetadata({**ARRAY, "acme": {"x": 1}}, context=CORE), {("acme",)}),
- (ZarrV3ArrayMetadata({**ARRAY, "acme": {"must_understand": False}}, context=CORE), set()),
- (ZarrV3GroupMetadata({**GROUP, "acme": 1}, context=CORE), {("acme",)}),
+ (ZarrV3ArrayMetadata({**ARRAY, "acme": {"x": 1}}, context=CORE_CONTEXT), {("acme",)}),
+ (
+ ZarrV3ArrayMetadata(
+ {**ARRAY, "acme": {"must_understand": False}}, context=CORE_CONTEXT
+ ),
+ set(),
+ ),
+ (ZarrV3GroupMetadata({**GROUP, "acme": 1}, context=CORE_CONTEXT), {("acme",)}),
(ZarrV2ArrayMetadata({**V2_ARRAY, "compressor": {"id": "acme"}}), {("compressor",)}),
(ZarrV2GroupMetadata.create_default(), set()),
(
@@ -154,8 +162,8 @@ def test_a_model_that_refines_another_has_no_more_to_understand(
wider: set[tuple[str | int, ...]],
narrower: set[tuple[str | int, ...]],
) -> None:
- """A scope that claims more of a document reads a model that refines the model of a scope that claims less, and its `unclaimed_fields` lie at or inside those of the model it refines, for an array and for a group. Here `CORE` leaves an unwaived `acme.mirror` member unclaimed as a whole; the scope that claims the member leaves at most the codec inside it, which lies inside the member, and a waived member is left out by both."""
- claimed_less = kind({**document, "acme.mirror": member}, context=CORE)
+ """A scope that claims more of a document reads a model that refines the model of a scope that claims less, and its `unclaimed_fields` lie at or inside those of the model it refines, for an array and for a group. Here `CORE_CONTEXT` leaves an unwaived `acme.mirror` member unclaimed as a whole; the scope that claims the member leaves at most the codec inside it, which lies inside the member, and a waived member is left out by both."""
+ claimed_less = kind({**document, "acme.mirror": member}, context=CORE_CONTEXT)
claimed_more = kind({**document, "acme.mirror": member}, context=context)
assert claimed_more.refines(claimed_less)
assert set(claimed_less.unclaimed_fields) == wider
diff --git a/packages/zarr-metadata/tests/test_contexts.py b/packages/zarr-metadata/tests/test_contexts.py
index ef8f408727..ed06f05df1 100644
--- a/packages/zarr-metadata/tests/test_contexts.py
+++ b/packages/zarr-metadata/tests/test_contexts.py
@@ -19,14 +19,14 @@
read_array_metadata_v2,
read_array_metadata_v3,
)
-from zarr_metadata.v2 import CORE_V2, GZIP_CODEC_V2, ZarrV2Context
+from zarr_metadata.v2 import CORE_V2_CONTEXT, GZIP_CODEC_V2, ZarrV2Context
from zarr_metadata.v2._context import _KINDS as V2_KINDS
from zarr_metadata.v2.definition import (
ZarrV2CodecDefinition,
ZarrV2ContextSlots,
ZarrV2DataTypeDefinition,
)
-from zarr_metadata.v3 import CORE, GZIP_CODEC, ZarrV3Context
+from zarr_metadata.v3 import CORE_CONTEXT, GZIP_CODEC, ZarrV3Context
from zarr_metadata.v3._context import _KINDS as V3_KINDS
from zarr_metadata.v3._context import is_v3_kind
from zarr_metadata.v3.definition import CodecDefinition, DataTypeDefinition, ZarrV3ContextSlots
@@ -41,8 +41,8 @@ def _other(definition: Any) -> Any: # noqa: ANN401 - a definition of either for
V2_ARRAY = ZarrV2ArrayMetadata.create_default().to_json()
FORMATS = [
- pytest.param(ZarrV3Context, CORE, CodecDefinition, GZIP_CODEC, 3, id="v3"),
- pytest.param(ZarrV2Context, CORE_V2, ZarrV2CodecDefinition, GZIP_CODEC_V2, 2, id="v2"),
+ pytest.param(ZarrV3Context, CORE_CONTEXT, CodecDefinition, GZIP_CODEC, 3, id="v3"),
+ pytest.param(ZarrV2Context, CORE_V2_CONTEXT, ZarrV2CodecDefinition, GZIP_CODEC_V2, 2, id="v2"),
]
@@ -234,7 +234,12 @@ def test_error_a_reader_refuses_what_is_not_a_scope_of_its_format(
@pytest.mark.parametrize(
("core", "given", "zarr_format"),
- [(CORE, "nope", 3), (CORE, CORE_V2, 3), (CORE_V2, "nope", 2), (CORE_V2, CORE, 2)],
+ [
+ (CORE_CONTEXT, "nope", 3),
+ (CORE_CONTEXT, CORE_V2_CONTEXT, 3),
+ (CORE_V2_CONTEXT, "nope", 2),
+ (CORE_V2_CONTEXT, CORE_CONTEXT, 2),
+ ],
ids=["v3-not-a-scope", "v3-a-v2-scope", "v2-not-a-scope", "v2-a-v3-scope"],
)
def test_error_union_refuses_what_is_not_a_scope_of_its_format(
@@ -256,7 +261,7 @@ def test_each_declared_slot_has_a_view(cls: Any) -> None: # noqa: ANN401 - eith
slots_type = ZarrV3ContextSlots if cls is ZarrV3Context else ZarrV2ContextSlots
assert list(cls.kinds) == list(slots_type.__annotations__)
assert set(cls.kinds.values()) == (V3_KINDS if cls is ZarrV3Context else V2_KINDS)
- scope: Any = CORE if cls is ZarrV3Context else CORE_V2
+ scope: Any = CORE_CONTEXT if cls is ZarrV3Context else CORE_V2_CONTEXT
for slot, kind in cls.kinds.items():
assert getattr(scope, slot) == scope.definitions_of(kind)
diff --git a/packages/zarr-metadata/tests/test_json_schema.py b/packages/zarr-metadata/tests/test_json_schema.py
index 7145af7e0c..ec09f8da16 100644
--- a/packages/zarr-metadata/tests/test_json_schema.py
+++ b/packages/zarr-metadata/tests/test_json_schema.py
@@ -34,11 +34,12 @@
)
from zarr_metadata.model import read_node_metadata_v3
from zarr_metadata.typed_json import JSONValue, check
-from zarr_metadata.v2.definition import CORE_V2
+from zarr_metadata.v2.definition import CORE_V2_CONTEXT
from zarr_metadata.v3.codec.gzip import GzipCodecConfiguration
from zarr_metadata.v3.definition import (
- CORE,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
+ ZARR_PYTHON_CONTEXT,
ChunkGridDefinition,
ChunkKeyEncodingDefinition,
CodecDefinition,
@@ -472,7 +473,7 @@ def _changed(draw: st.DrawFn, value: object) -> object:
return value
-_CONTEXTS = (CORE, CORE_AND_EXTENSIONS, ZarrV3Context())
+_CONTEXTS = (CORE_CONTEXT, CORE_AND_EXTENSIONS_CONTEXT, ZARR_PYTHON_CONTEXT, ZarrV3Context())
_FIELD_BASES: tuple[tuple[str, object], ...] = (
*CASES,
# Names that nothing claims, so nothing checks their configuration.
@@ -498,12 +499,19 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
# it, and whether the scope reads it without a problem. Where the two
# differ, a rule found a problem that the schema cannot express.
FIELDS: list[tuple[str, object, type[ZarrV3Kind], ZarrV3Context, bool, bool]] = [
- ("object", {"name": "gzip", "configuration": {"level": 5}}, CodecDefinition, CORE, True, True),
+ (
+ "object",
+ {"name": "gzip", "configuration": {"level": 5}},
+ CodecDefinition,
+ CORE_CONTEXT,
+ True,
+ True,
+ ),
(
"out-of-bounds",
{"name": "gzip", "configuration": {"level": 12}},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
False,
False,
),
@@ -511,17 +519,17 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
"unknown-key",
{"name": "gzip", "configuration": {"level": 5, "window": 15}},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
False,
False,
),
- ("bare-name-needing-a-configuration", "gzip", CodecDefinition, CORE, False, False),
- ("bare-name", "bytes", CodecDefinition, CORE, True, True),
+ ("bare-name-needing-a-configuration", "gzip", CodecDefinition, CORE_CONTEXT, False, False),
+ ("bare-name", "bytes", CodecDefinition, CORE_CONTEXT, True, True),
(
"understood",
{"name": "gzip", "configuration": {"level": 5}, "must_understand": True},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
True,
True,
),
@@ -529,7 +537,7 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
"not-understood",
{"name": "gzip", "configuration": {"level": 5}, "must_understand": False},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
False,
False,
),
@@ -537,7 +545,7 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
"stray-member",
{"name": "gzip", "configuration": {"level": 5}, "version": 2},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
False,
False,
),
@@ -545,16 +553,16 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
"unclaimed",
{"name": "zfpy", "configuration": {"mode": 4}},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
True,
True,
),
- ("unclaimed-bare", "zfpy", CodecDefinition, CORE, True, True),
+ ("unclaimed-bare", "zfpy", CodecDefinition, CORE_CONTEXT, True, True),
(
"unclaimed-not-understood",
{"name": "zfpy", "must_understand": False},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
False,
False,
),
@@ -574,7 +582,7 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
"configuration": {"cname": "lz4", "clevel": 1, "shuffle": "shuffle", "blocksize": 0},
},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
True,
False,
),
@@ -585,7 +593,7 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
"configuration": {**_INDEXED, "index_codecs": ["bytes", "crc32c"]},
},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
True,
True,
),
@@ -599,7 +607,7 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
},
},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
False,
False,
),
@@ -610,7 +618,7 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
"configuration": {**_INDEXED, "index_codecs": ["bytes", "acme.sum"]},
},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
True,
True,
),
@@ -625,25 +633,32 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
},
},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
False,
False,
),
- ("raw-bits", "r16", DataTypeDefinition, CORE, True, True),
- ("raw-bits-object", {"name": "r16", "configuration": {}}, DataTypeDefinition, CORE, True, True),
+ ("raw-bits", "r16", DataTypeDefinition, CORE_CONTEXT, True, True),
+ (
+ "raw-bits-object",
+ {"name": "r16", "configuration": {}},
+ DataTypeDefinition,
+ CORE_CONTEXT,
+ True,
+ True,
+ ),
# The values a raw-bits name carries are not also written in a configuration.
(
"raw-bits-configured",
{"name": "r16", "configuration": {"bits": 16}},
DataTypeDefinition,
- CORE,
+ CORE_CONTEXT,
False,
False,
),
- ("raw-bits-of-a-size-the-spec-refuses", "r12", DataTypeDefinition, CORE, True, False),
+ ("raw-bits-of-a-size-the-spec-refuses", "r12", DataTypeDefinition, CORE_CONTEXT, True, False),
# `r*` is the notation of the spec's table, not a name: `*` is not
# allowed in a name.
- ("raw-bits-notation", "r*", DataTypeDefinition, CORE, False, False),
+ ("raw-bits-notation", "r*", DataTypeDefinition, CORE_CONTEXT, False, False),
# The pattern is matched to the end of the name, as the package matches
# it, so a validator that matches as Python does must not accept a final
# newline. It is also not a name, because a newline is not allowed in one.
@@ -651,7 +666,7 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
"raw-bits-and-a-newline",
{"name": "r16\n", "configuration": {"x": 1}},
DataTypeDefinition,
- CORE,
+ CORE_CONTEXT,
False,
False,
),
@@ -672,7 +687,7 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
},
},
DataTypeDefinition,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
False,
False,
),
@@ -680,7 +695,7 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
"grid-out-of-bounds",
{"name": "regular", "configuration": {"chunk_shape": [0]}},
ChunkGridDefinition,
- CORE,
+ CORE_CONTEXT,
False,
False,
),
@@ -688,11 +703,11 @@ def _field_validator(kind: type[ZarrV3Kind], context: int) -> Draft202012Validat
"encoding",
{"name": "v2", "configuration": {"separator": "/"}},
ChunkKeyEncodingDefinition,
- CORE,
+ CORE_CONTEXT,
True,
True,
),
- ("transformer", {"name": "acme.cache"}, StorageTransformerDefinition, CORE, True, True),
+ ("transformer", {"name": "acme.cache"}, StorageTransformerDefinition, CORE_CONTEXT, True, True),
]
@@ -714,14 +729,14 @@ def test_field_json_schema_says_what_the_scope_reads(
("key", "field"), CASES, ids=[f"{key}:{index}" for index, (key, _) in enumerate(CASES)]
)
def test_every_example_field_is_one_its_schema_accepts(key: str, field: object) -> None:
- schema = field_json_schema_v3(KINDS[key.split(":")[0]], context=CORE_AND_EXTENSIONS)
+ schema = field_json_schema_v3(KINDS[key.split(":")[0]], context=ZARR_PYTHON_CONTEXT)
assert Draft202012Validator(schema).is_valid(cast("Any", field))
@_PROPERTY
@given(st.data())
def test_a_field_read_without_a_problem_is_one_its_schema_accepts(data: st.DataObject) -> None:
- # Each example is changed in one place and read in one of three scopes.
+ # Each example is changed in one place and read in one of four scopes.
# If the scope still reads it without a problem, the schema accepts it.
key, example = data.draw(st.sampled_from(_FIELD_BASES), label="example")
kind = KINDS[key.split(":")[0]]
@@ -736,12 +751,12 @@ def test_a_field_read_without_a_problem_is_one_its_schema_accepts(data: st.DataO
def test_error_field_json_schema_v3_refuses_a_scope_of_another_format() -> None:
"""A v2 scope cannot read a v3 field, so `field_json_schema_v3` raises `TypeError` for one, as every v3 reader does."""
with pytest.raises(TypeError, match="v3 document is read in a scope of that format"):
- field_json_schema_v3(CodecDefinition, context=CORE_V2) # pyright: ignore[reportArgumentType]
+ field_json_schema_v3(CodecDefinition, context=CORE_V2_CONTEXT) # pyright: ignore[reportArgumentType]
def test_field_json_schema_refuses_what_is_not_a_kind() -> None:
with pytest.raises(TypeError, match="is not a kind of metadata"):
- field_json_schema_v3(Definition, context=CORE) # pyright: ignore[reportArgumentType]
+ field_json_schema_v3(Definition, context=CORE_CONTEXT) # pyright: ignore[reportArgumentType]
# --- a zarr.json ------------------------------------------------------------
@@ -889,7 +904,7 @@ def _node_validator(context: int) -> Draft202012Validator:
@_PROPERTY
@given(st.data())
def test_a_document_read_without_a_problem_is_one_its_schema_accepts(data: st.DataObject) -> None:
- # Each document is changed in one place and read in one of three scopes.
+ # Each document is changed in one place and read in one of four scopes.
# If `read_node_metadata_v3` finds no problem, the schema accepts it.
base = data.draw(st.sampled_from(_BASES), label="base")
context = data.draw(st.sampled_from(range(len(_CONTEXTS))), label="context")
@@ -902,8 +917,8 @@ def test_a_document_read_without_a_problem_is_one_its_schema_accepts(data: st.Da
@pytest.mark.parametrize(
"context",
- [CORE, CORE_AND_EXTENSIONS, ZarrV3Context()],
- ids=["core", "core-and-extensions", "empty"],
+ [CORE_CONTEXT, CORE_AND_EXTENSIONS_CONTEXT, ZARR_PYTHON_CONTEXT, ZarrV3Context()],
+ ids=["core", "core-and-extensions", "zarr-python", "empty"],
)
def test_every_schema_is_a_json_schema_written_the_same_each_time(context: ZarrV3Context) -> None:
schema = node_metadata_json_schema_v3(context=context)
@@ -959,12 +974,12 @@ def test_a_stray_member_beside_an_unclaimed_name_is_refused_by_the_schema() -> N
# The schema rejects it, as the reader does: the envelope of an
# unclaimed name holds only the name and its configuration.
field = {"name": "zfpy", "version": 2}
- assert [(p.loc, p.kind) for p in resolve(field, CodecDefinition, CORE)[1]] == [
+ assert [(p.loc, p.kind) for p in resolve(field, CodecDefinition, CORE_CONTEXT)[1]] == [
(("version",), "unknown_key")
]
- assert not Draft202012Validator(field_json_schema_v3(CodecDefinition, context=CORE)).is_valid(
- field
- )
+ assert not Draft202012Validator(
+ field_json_schema_v3(CodecDefinition, context=CORE_CONTEXT)
+ ).is_valid(field)
@pytest.mark.parametrize(
@@ -1000,10 +1015,12 @@ def test_the_schema_names_an_extension_as_the_reader_does(name: str) -> None:
# A name matches the spec's regex or is a URI. The schema's `pattern`
# accepts exactly the names that `well_named` accepts.
field = {"name": name}
- _, problems = resolve(field, CodecDefinition, CORE)
+ _, problems = resolve(field, CodecDefinition, CORE_CONTEXT)
accepted = len(problems) == 0
assert (
- Draft202012Validator(field_json_schema_v3(CodecDefinition, context=CORE)).is_valid(field)
+ Draft202012Validator(field_json_schema_v3(CodecDefinition, context=CORE_CONTEXT)).is_valid(
+ field
+ )
is accepted
)
assert accepted is (
diff --git a/packages/zarr-metadata/tests/test_problem_data.py b/packages/zarr-metadata/tests/test_problem_data.py
index c2ecafacb6..9a2f059183 100644
--- a/packages/zarr-metadata/tests/test_problem_data.py
+++ b/packages/zarr-metadata/tests/test_problem_data.py
@@ -50,7 +50,7 @@
)
from zarr_metadata.v3.codec.gzip import GZIP_CODEC
from zarr_metadata.v3.definition import (
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
CodecDefinition,
DataTypeDefinition,
)
@@ -219,18 +219,18 @@ def _raised(read: Callable[[], object]) -> Sequence[ValidationProblem]:
(lambda value: check(value, GzipLevelOnly)[1], {"level": 12, "extra": [1]}),
(lambda value: GZIP_CODEC.read_configuration(value)[1], {"level": 12, "extra": [1]}),
(
- lambda value: resolve(value, CodecDefinition, CORE_AND_EXTENSIONS)[1],
+ lambda value: resolve(value, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)[1],
{"name": "gzip", "configuration": {"level": 12}, "must_understand": "yes"},
),
(
lambda value: fill_value_problems(
- resolve("uint8", DataTypeDefinition, CORE_AND_EXTENSIONS)[0], value
+ resolve("uint8", DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)[0], value
),
300,
),
(validate_json, {"a": [math.inf]}),
(
- lambda value: resolve(value, CodecDefinition, CORE_AND_EXTENSIONS)[1],
+ lambda value: resolve(value, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)[1],
{"name": 1, "configuration": {"a": [math.nan]}, "extra": [1]},
),
(validate_node_name_v3, "__/"),
@@ -316,9 +316,9 @@ def test_a_problem_with_what_a_name_carries_is_the_field_s() -> None:
# refuses. The problem is located at the field, because the name is what
# the document wrote there. The bound on `bits` is not a bound on the
# name, so the problem's `ctx` is empty.
- context = CORE_AND_EXTENSIONS.update(
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
data_types={
- **CORE_AND_EXTENSIONS.data_types,
+ **CORE_AND_EXTENSIONS_CONTEXT.data_types,
"r*": DataTypeDefinition(name="r*", configuration=WideBits),
}
)
@@ -391,7 +391,7 @@ def test_a_problem_at_the_first_element_holds_it() -> None:
(problem,) = with_input([ValidationProblem(("a", 0), "bad", "invalid_value")], {"a": [7]})
assert problem.input == 7
assert value_at([5, 6], (0,)) == 5
- read = resolve("bytes", DataTypeDefinition, CORE_AND_EXTENSIONS)[0]
+ read = resolve("bytes", DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)[0]
(found,) = fill_value_problems(read, [-1])
assert (found.loc, found.input) == ((0,), -1)
diff --git a/packages/zarr-metadata/tests/test_public_api.py b/packages/zarr-metadata/tests/test_public_api.py
index e0095aec31..237a2272de 100644
--- a/packages/zarr-metadata/tests/test_public_api.py
+++ b/packages/zarr-metadata/tests/test_public_api.py
@@ -88,6 +88,8 @@ def _group_rank(s: str) -> int:
"ScaleOffsetCodecMetadata",
"ShardingIndexedCodecMetadata",
"TransposeCodecMetadata",
+ "VlenBytesCodecMetadata",
+ "VlenUtf8CodecMetadata",
"ZstdCodecMetadata",
# Category C — grid/key canonical unions
"RegularChunkGridMetadata",
@@ -150,6 +152,10 @@ def _group_rank(s: str) -> int:
"StringDataTypeName",
"STRING_DATA_TYPE_NAME",
"StringFillValue",
+ # fixed_length_utf32
+ "FixedLengthUtf32DataTypeName",
+ "FIXED_LENGTH_UTF32_DATA_TYPE_NAME",
+ "FixedLengthUtf32FillValue",
# numpy_datetime64
"NumpyDatetime64DataTypeName",
"NUMPY_DATETIME64_DATA_TYPE_NAME",
@@ -188,6 +194,10 @@ def _group_rank(s: str) -> int:
"CastValueCodecName",
"CRC32C_CODEC_NAME",
"Crc32cCodecName",
+ "VLEN_BYTES_CODEC_NAME",
+ "VlenBytesCodecName",
+ "VLEN_UTF8_CODEC_NAME",
+ "VlenUtf8CodecName",
"GZIP_CODEC_NAME",
"GzipCodecName",
"SCALE_OFFSET_CODEC_NAME",
@@ -277,6 +287,9 @@ def test_all_is_grouped_and_unique() -> None:
# example, `ZarrV3CodecField`, read in the same scope as the field that
# contains it.
"Field",
+ # How to draw one kind of definition's configurations, in
+ # `zarr_metadata.testing.strategies`: `CodecStrategy`.
+ "Strategy",
)
_EXTENSION_NAME = re.compile(r"^(?:[A-Z][a-z0-9]*)+?(?:" + "|".join(_EXTENSION_ROLES) + r")$")
@@ -294,6 +307,9 @@ def test_all_is_grouped_and_unique() -> None:
# two definitions of one name.
"ClaimKey",
"Conflict",
+ # What one definition's configurations are drawn from, in
+ # `zarr_metadata.testing.strategies`.
+ "Configurations",
"ScopeConflictError",
# A reader's problems, as the signatures of every reader name them.
"Problems",
@@ -327,6 +343,9 @@ def test_all_is_grouped_and_unique() -> None:
# A pydantic field's scope, fixed: `Annotated[Model, InScope(CONTEXT)]`.
"InScope",
"MetadataValidationError",
+ "FixedLengthUtf32",
+ "NullTerminatedBytes",
+ "ZarrPythonRawBytes",
"NumpyDatetime64",
"NumpyTimeUnit",
"NumpyTimedelta64",
@@ -339,6 +358,8 @@ def test_all_is_grouped_and_unique() -> None:
"ScalarMap",
"ScalarMapEntry",
"ShardingIndexLocation",
+ "Spellings",
+ "Strategies",
"Struct",
"StructField",
"ValidationProblem",
@@ -355,10 +376,24 @@ def _iter_module_names() -> set[str]:
return module_names
+_ALIAS_PACKAGES = (
+ "zarr_metadata.v3.chunk_grid",
+ "zarr_metadata.v3.chunk_key_encoding",
+ "zarr_metadata.v3.codec",
+ "zarr_metadata.v3.data_type",
+)
+"""The packages that re-export every name their public submodules export, as a convenience. Each name's home stays its submodule."""
+
+
+def _home_module_names() -> set[str]:
+ """Every public module except the alias packages, whose names are already checked in their submodules."""
+ return _iter_module_names() - set(_ALIAS_PACKAGES)
+
+
def _public_type_names() -> set[tuple[str, str]]:
"""Every (module, CamelCase name) pair exported via a public `__all__`."""
out: set[tuple[str, str]] = set()
- for module_name in _iter_module_names():
+ for module_name in _home_module_names():
module = importlib.import_module(module_name)
for name in getattr(module, "__all__", ()):
if name.startswith("_") or name.isupper() or name.islower():
@@ -438,7 +473,7 @@ def _literal_backed_constants() -> list[tuple[str, str, str]]:
simply absent from the result.
"""
out: list[tuple[str, str, str]] = []
- for module_name in _iter_module_names():
+ for module_name in _home_module_names():
module = importlib.import_module(module_name)
literals = {
name: frozenset(get_args(obj))
@@ -479,7 +514,7 @@ def _value_tied_constants() -> set[str]:
These are invisible to the derivation check, so they are surfaced here and
counted, rather than silently dropped inside the pairing helper."""
tied: set[str] = set()
- for module_name in _iter_module_names():
+ for module_name in _home_module_names():
module = importlib.import_module(module_name)
literals = [
frozenset(get_args(obj))
@@ -564,7 +599,7 @@ def _builds_scopes(value: object) -> bool:
def _homes() -> dict[str, list[str]]:
"""Each public name, and every public submodule that exports it, with a format's front door left out for a definition or scope it aliases."""
homes: dict[str, list[str]] = {}
- for module_name in sorted(_iter_module_names() - {"zarr_metadata"}):
+ for module_name in sorted(_home_module_names() - {"zarr_metadata"}):
for name in getattr(importlib.import_module(module_name), "__all__", ()):
homes.setdefault(name, []).append(module_name)
for name, modules in homes.items():
@@ -575,7 +610,7 @@ def _homes() -> dict[str, list[str]]:
def test_every_name_has_one_public_home() -> None:
- """Each public name is exported by exactly one public submodule, its home. The top-level `zarr_metadata` namespace may also export it, as a convenience alias of the same object, and so may the front door of its format (`zarr_metadata.v2` or `zarr_metadata.v3`) when the name is a definition, a scope or the scope class of that format."""
+ """Each public name is exported by exactly one public submodule, its home. The top-level `zarr_metadata` namespace may also export it, as a convenience alias of the same object, and so may the front door of its format (`zarr_metadata.v2` or `zarr_metadata.v3`) when the name is a definition, a scope or the scope class of that format. An alias package also exports the names of its submodules; `test_an_alias_package_exports_its_submodules_names` pins those."""
homes = _homes()
duplicated = {name: modules for name, modules in homes.items() if len(modules) > 1}
assert duplicated == {}
@@ -600,13 +635,31 @@ def test_every_name_has_one_public_home() -> None:
assert getattr(module, name) is value, f"{door}.{name} is another object"
+@pytest.mark.parametrize("package", _ALIAS_PACKAGES)
+def test_an_alias_package_exports_its_submodules_names(package: str) -> None:
+ """An alias package exports every name its public submodules export, and nothing else, each the same object as in its submodule, so `from zarr_metadata.v3.data_type import BoolDataTypeName` reads the name that `zarr_metadata.v3.data_type.bool` defines."""
+ module = importlib.import_module(package)
+ submodules = [
+ importlib.import_module(info.name)
+ for info in pkgutil.iter_modules(module.__path__, prefix=f"{package}.")
+ if not info.name.rsplit(".", 1)[1].startswith("_")
+ ]
+ expected = {name: submodule for submodule in submodules for name in submodule.__all__}
+ assert sorted(module.__all__) == sorted(expected)
+ assert [
+ name
+ for name in module.__all__
+ if getattr(module, name) is not getattr(expected[name], name)
+ ] == []
+
+
@pytest.mark.parametrize("door", _FRONT_DOORS)
def test_a_front_door_exports_every_definition_and_scope_of_its_format(door: str) -> None:
"""`zarr_metadata.v2` and `zarr_metadata.v3` each export every definition, scope and scope class that a submodule of theirs exports, so one import builds or extends a scope of that format."""
exported = set(importlib.import_module(door).__all__)
missing = sorted(
name
- for module_name in _iter_module_names()
+ for module_name in _home_module_names()
if module_name.startswith(f"{door}.")
for name in getattr(importlib.import_module(module_name), "__all__", ())
if _builds_scopes(getattr(importlib.import_module(module_name), name))
diff --git a/packages/zarr-metadata/tests/test_renamed_scopes.py b/packages/zarr-metadata/tests/test_renamed_scopes.py
new file mode 100644
index 0000000000..a14a1c771d
--- /dev/null
+++ b/packages/zarr-metadata/tests/test_renamed_scopes.py
@@ -0,0 +1,45 @@
+"""`CORE`, `CORE_AND_EXTENSIONS` and `CORE_V2`, the scopes' names before they ended in `_CONTEXT`, which still work with a warning."""
+
+from __future__ import annotations
+
+import importlib
+
+import pytest
+
+from zarr_metadata.v2.definition import CORE_V2_CONTEXT
+from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS_CONTEXT, CORE_CONTEXT
+
+_V3 = ("zarr_metadata.v3", "zarr_metadata.v3.definition")
+_V2 = ("zarr_metadata.v2", "zarr_metadata.v2.definition")
+
+
+@pytest.mark.parametrize(
+ ("module", "old", "new", "scope"),
+ [
+ *((module, "CORE", "CORE_CONTEXT", CORE_CONTEXT) for module in _V3),
+ *(
+ (
+ module,
+ "CORE_AND_EXTENSIONS",
+ "CORE_AND_EXTENSIONS_CONTEXT",
+ CORE_AND_EXTENSIONS_CONTEXT,
+ )
+ for module in _V3
+ ),
+ *((module, "CORE_V2", "CORE_V2_CONTEXT", CORE_V2_CONTEXT) for module in _V2),
+ ],
+)
+def test_an_old_name_is_the_same_scope_with_a_deprecation_warning(
+ module: str, old: str, new: str, scope: object
+) -> None:
+ with pytest.warns(DeprecationWarning, match=f"`{old}` is deprecated.*use `{new}`") as caught:
+ found = getattr(importlib.import_module(module), old)
+ assert found is scope
+ # The warning points at the code that used the old name, not at this package.
+ assert caught[0].filename == __file__
+
+
+@pytest.mark.parametrize("module", [*_V3, *_V2])
+def test_error_a_name_that_is_neither_old_nor_new(module: str) -> None:
+ with pytest.raises(AttributeError, match="has no attribute 'CORE_V3'"):
+ importlib.import_module(module).CORE_V3 # noqa: B018
diff --git a/packages/zarr-metadata/tests/testing/__init__.py b/packages/zarr-metadata/tests/testing/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/zarr-metadata/tests/testing/test_strategies.py b/packages/zarr-metadata/tests/testing/test_strategies.py
new file mode 100644
index 0000000000..4d64269f8a
--- /dev/null
+++ b/packages/zarr-metadata/tests/testing/test_strategies.py
@@ -0,0 +1,432 @@
+"""The strategies against the definitions they draw for, and against what they ought to reach.
+
+Every draw is read in `ZARR_PYTHON_CONTEXT` and must read without a
+problem, by the definition it was drawn for: the field alone, a fill
+value beside its data type, a chunk grid over the shape it was drawn
+for, a codec handed the chunk it was drawn for. A strategy that only
+drew valid metadata could draw almost none, so reach is checked too,
+with targets worked out from each configuration's TypedDict rather than
+from the strategies: every optional key present and absent, every
+`Literal` value, and for a definition with a simplest spelling, a
+configuration it simplifies.
+"""
+
+from __future__ import annotations
+
+import json
+from typing import TYPE_CHECKING, Any, Literal, cast, get_args, get_origin
+
+import pytest
+from hypothesis import event, find, given, settings
+from hypothesis import strategies as st
+
+from zarr_metadata._typed_json import typeddict_keys
+from zarr_metadata.definition import (
+ AcceptedField,
+ Definition,
+ EmptyConfiguration,
+ fill_value_problems,
+ resolve,
+)
+from zarr_metadata.definition._definition import as_kind, canonicalize, kind_of, unchanged
+from zarr_metadata.testing.strategies import (
+ DEFAULT_STRATEGIES,
+ ChunkKeyEncodingStrategy,
+ CodecStrategy,
+ Spellings,
+ chunk_grid_configurations,
+ codec_configurations,
+ data_type_configurations,
+ fields,
+ fill_values,
+)
+from zarr_metadata.v2.definition import CORE_V2_CONTEXT
+from zarr_metadata.v3._definition import chunk_grid_lengths
+from zarr_metadata.v3.chunk_grid.rectilinear import RECTILINEAR_CHUNK_GRID
+from zarr_metadata.v3.codec.cast_value import CAST_VALUE_CODEC
+from zarr_metadata.v3.codec.gzip import GZIP_CODEC
+from zarr_metadata.v3.codec.scale_offset import SCALE_OFFSET_CODEC
+from zarr_metadata.v3.codec.sharding_indexed import SHARDING_INDEXED_CODEC
+from zarr_metadata.v3.data_type._float import FloatSpecialFillValue
+from zarr_metadata.v3.data_type.bytes import BYTES_DATA_TYPE
+from zarr_metadata.v3.data_type.complex64 import COMPLEX64_DATA_TYPE
+from zarr_metadata.v3.data_type.complex128 import COMPLEX128_DATA_TYPE
+from zarr_metadata.v3.data_type.float16 import FLOAT16_DATA_TYPE
+from zarr_metadata.v3.data_type.float32 import FLOAT32_DATA_TYPE
+from zarr_metadata.v3.data_type.float64 import FLOAT64_DATA_TYPE
+from zarr_metadata.v3.data_type.numpy_datetime64 import NUMPY_DATETIME64_DATA_TYPE
+from zarr_metadata.v3.data_type.numpy_timedelta64 import NUMPY_TIMEDELTA64_DATA_TYPE
+from zarr_metadata.v3.data_type.struct import STRUCT_DATA_TYPE
+from zarr_metadata.v3.definition import (
+ CORE_CONTEXT,
+ ZARR_PYTHON_CONTEXT,
+ Chunk,
+ ChunkGridDefinition,
+ CodecDefinition,
+ DataTypeDefinition,
+ ZarrV3Context,
+)
+
+if TYPE_CHECKING:
+ from collections.abc import Mapping
+
+ from zarr_metadata._common import JSONValue
+ from zarr_metadata.v3 import ZarrV3MetadataFieldJSON
+
+_HOLDING_FIELDS = frozenset({SHARDING_INDEXED_CODEC, CAST_VALUE_CODEC, STRUCT_DATA_TYPE})
+"""The definitions whose configurations hold other fields, which have no strategy yet."""
+
+_IN_SCOPE = frozenset(
+ (
+ *ZARR_PYTHON_CONTEXT.data_types.values(),
+ *ZARR_PYTHON_CONTEXT.chunk_grids.values(),
+ *ZARR_PYTHON_CONTEXT.chunk_key_encodings.values(),
+ *ZARR_PYTHON_CONTEXT.codecs.values(),
+ *ZARR_PYTHON_CONTEXT.storage_transformers.values(),
+ )
+)
+"""The definitions at the spec's extension points that `ZARR_PYTHON_CONTEXT` holds; members of a document it defines are not fields."""
+_CORE_DATA_TYPES = frozenset(CORE_CONTEXT.data_types.values())
+
+_DEFINITIONS = sorted(DEFAULT_STRATEGIES, key=lambda definition: definition.name)
+_DATA_TYPES = [d for d in _DEFINITIONS if isinstance(d, DataTypeDefinition)]
+_CHUNK_GRIDS = [d for d in _DEFINITIONS if isinstance(d, ChunkGridDefinition)]
+_CODECS = [d for d in _DEFINITIONS if isinstance(d, CodecDefinition)]
+
+_SHAPES = st.lists(st.integers(0, 12), max_size=4).map(tuple)
+
+
+def _id(value: object) -> str:
+ if isinstance(value, Definition):
+ return f"{type(value).__name__}:{value.name}"
+ return str(value)
+
+
+def _read(definition: Definition[Any], field: object) -> Any: # noqa: ANN401
+ """`field` read in `ZARR_PYTHON_CONTEXT`, asserted to read without a problem by `definition`."""
+ resolved, problems = resolve(field, as_kind(kind_of(definition)), ZARR_PYTHON_CONTEXT)
+ assert problems == ()
+ assert isinstance(resolved, AcceptedField)
+ assert resolved.definition is definition
+ return resolved
+
+
+def test_every_definition_has_a_strategy() -> None:
+ """Each definition in scope has a strategy, but those that hold other fields; and none is out of scope."""
+ assert _IN_SCOPE - frozenset(DEFAULT_STRATEGIES) == _HOLDING_FIELDS
+ assert frozenset(DEFAULT_STRATEGIES) <= _IN_SCOPE
+
+
+@pytest.mark.parametrize("spellings", get_args(Spellings))
+@pytest.mark.parametrize("definition", _DEFINITIONS, ids=_id)
+@given(data=st.data())
+def test_fields_read(
+ definition: Definition[Any], spellings: Spellings, data: st.DataObject
+) -> None:
+ """A drawn field reads by its definition, through JSON too; a canonical one is its own simplest spelling."""
+ field = data.draw(fields(definition, spellings=spellings))
+ _read(definition, field)
+ _read(definition, json.loads(json.dumps(field)))
+ if definition in _CORE_DATA_TYPES:
+ assert isinstance(field, str)
+ if spellings == "canonical":
+ canonical, _ = canonicalize(field, as_kind(kind_of(definition)), ZARR_PYTHON_CONTEXT)
+ assert canonical == field
+
+
+@pytest.mark.parametrize("definition", _DEFINITIONS, ids=_id)
+@given(data=st.data())
+def test_a_changed_draw_changes_no_other(definition: Definition[Any], data: st.DataObject) -> None:
+ """Each draw is a fresh value: a test that changes the configuration it drew does not change the next draw, which Hypothesis would report as flaky."""
+ field = data.draw(fields(definition))
+ if isinstance(field, dict) and "configuration" in field:
+ configuration = cast("dict[str, JSONValue]", field["configuration"])
+ assert "changed" not in configuration
+ configuration["changed"] = True
+
+
+@pytest.mark.parametrize("definition", _DATA_TYPES, ids=_id)
+@given(data=st.data())
+def test_fill_values_fit_their_data_type(
+ definition: DataTypeDefinition[Any], data: st.DataObject
+) -> None:
+ configuration = data.draw(data_type_configurations(definition))
+ data_type = _read(definition, data.draw(fields(definition, st.just(configuration))))
+ value = data.draw(fill_values(definition, configuration))
+ assert fill_value_problems(data_type, value) == ()
+ assert fill_value_problems(data_type, json.loads(json.dumps(value))) == ()
+
+
+@pytest.mark.parametrize("definition", _CHUNK_GRIDS, ids=_id)
+@given(data=st.data(), shape=_SHAPES)
+def test_chunk_grids_fit_their_shape(
+ definition: ChunkGridDefinition[Any], data: st.DataObject, shape: tuple[int, ...]
+) -> None:
+ configurations = chunk_grid_configurations(definition, shape)
+ chunk_grid = _read(definition, data.draw(fields(definition, configurations)))
+ lengths, problems = chunk_grid_lengths(chunk_grid, shape)
+ assert problems == ()
+ # "Chunk sizes must be greater than zero", which the regular grid's rules do not yet refuse.
+ assert all(0 not in axis for axis in lengths if axis is not None)
+
+
+@st.composite
+def _chunks(draw: st.DrawFn) -> Chunk:
+ """A chunk of a data type drawn from the strategies, or of none known, with lengths known along some axes, all, or none."""
+ definition = draw(st.none() | st.sampled_from(_DATA_TYPES))
+ data_type = None if definition is None else _read(definition, draw(fields(definition)))
+ axis = st.none() | st.integers(1, 8).map(lambda length: frozenset({length}))
+ lengths = draw(st.none() | st.lists(axis, max_size=4).map(tuple))
+ return Chunk(lengths, data_type)
+
+
+@pytest.mark.parametrize("definition", _CODECS, ids=_id)
+@given(data=st.data(), chunk=_chunks())
+def test_codecs_take_their_chunk(
+ definition: CodecDefinition[Any], data: st.DataObject, chunk: Chunk
+) -> None:
+ """A codec drawn for a chunk takes it; one drawn for none is one the spec does not give it, which needs the data type known."""
+ configurations = codec_configurations(definition, chunk)
+ if configurations is None:
+ assert chunk.data_type is not None
+ assert chunk.data_type.definition is not None
+ data_type_name = chunk.data_type.definition.name
+ event(f"takes no {data_type_name}")
+ codec = _read(definition, data.draw(fields(definition)))
+ assert list(definition.chunk_rules(codec.configuration, codec.nested, chunk)) != []
+ return
+ codec = _read(definition, data.draw(fields(definition, configurations)))
+ assert list(definition.chunk_rules(codec.configuration, codec.nested, chunk)) == []
+
+
+Target = tuple[Literal["present", "absent"], str] | tuple[Literal["value"], str, object]
+FieldForm = Literal["bare", "object", "configured", "must_understand"]
+
+
+def _configuration_targets(
+ definition: Definition[Any],
+) -> list[Target | tuple[Literal["simplified"]]]:
+ """What a strategy for `definition` ought to reach, by its configuration's TypedDict."""
+ targets: list[Target | tuple[Literal["simplified"]]] = []
+ for key, (annotation, required) in typeddict_keys(definition.configuration).members.items():
+ if not required:
+ targets.extend([("present", key), ("absent", key)])
+ if get_origin(annotation) is Literal:
+ targets.extend(("value", key, value) for value in get_args(annotation))
+ if definition.canonical is not unchanged:
+ targets.append(("simplified",))
+ return targets
+
+
+def _field_forms(definition: Definition[Any]) -> list[FieldForm]:
+ """The envelopes the spec allows a field of `definition` to be written in."""
+ if definition in _CORE_DATA_TYPES:
+ return [] # only ever bare, which `test_fields_read` asserts
+ if definition.requires_configuration:
+ return ["configured", "must_understand"]
+ return ["bare", "object", "configured", "must_understand"]
+
+
+def _form_of(field: ZarrV3MetadataFieldJSON) -> set[FieldForm]:
+ if isinstance(field, str):
+ return {"bare"}
+ forms: set[FieldForm] = {"configured" if "configuration" in field else "object"}
+ if "must_understand" in field:
+ forms.add("must_understand")
+ return forms
+
+
+def _hits(
+ definition: Definition[Any],
+ target: Target | tuple[Literal["simplified"]],
+ configuration: Mapping[str, JSONValue],
+) -> bool:
+ match target:
+ case ("present", key):
+ return key in configuration
+ case ("absent", key):
+ return key not in configuration
+ case ("value", key, value):
+ return configuration.get(key) == value
+ case ("simplified",):
+ return definition.canonical(configuration) != configuration
+
+
+_REACH = settings(max_examples=2000, database=None)
+
+
+@pytest.mark.parametrize(
+ ("definition", "target"),
+ [(d, target) for d in _DEFINITIONS for target in _configuration_targets(d)],
+ ids=_id,
+)
+def test_configurations_reach(
+ definition: Definition[Any], target: Target | tuple[Literal["simplified"]]
+) -> None:
+ configurations = fields(definition).map(lambda field: _read(definition, field).configuration)
+ find(
+ configurations,
+ lambda configuration: _hits(definition, target, configuration),
+ settings=_REACH,
+ )
+
+
+@pytest.mark.parametrize(
+ ("definition", "form"),
+ [(d, form) for d in _DEFINITIONS for form in _field_forms(d)],
+ ids=_id,
+)
+def test_fields_reach(definition: Definition[Any], form: FieldForm) -> None:
+ find(fields(definition), lambda field: form in _form_of(field), settings=_REACH)
+
+
+def test_an_added_strategy_is_drawn_from() -> None:
+ """A strategy joined to the defaults, as an extension author adds one, is the one drawn from."""
+ level_three = CodecStrategy(configurations=lambda _chunk: st.just({"level": 3}))
+ strategies = DEFAULT_STRATEGIES | {GZIP_CODEC: level_three}
+ field = find(fields(GZIP_CODEC, strategies=strategies), lambda _field: True, settings=_REACH)
+ assert _read(GZIP_CODEC, field).configuration == {"level": 3}
+
+
+def test_a_definition_without_a_strategy_is_a_lookup_error() -> None:
+ with pytest.raises(LookupError, match="no strategy for 'sharding_indexed'"):
+ fields(SHARDING_INDEXED_CODEC)
+
+
+def test_a_strategy_of_another_kind_is_a_type_error() -> None:
+ # Unannotated, as an extension author writes it: a mapping keyed by a
+ # `CodecDefinition` is a `Strategies`.
+ strategies = {GZIP_CODEC: ChunkKeyEncodingStrategy(configurations=st.just({}))}
+ with pytest.raises(TypeError, match="draws configurations of a ChunkKeyEncodingDefinition"):
+ codec_configurations(GZIP_CODEC, strategies=strategies)
+
+
+def test_a_codec_strategy_refusing_an_unknown_chunk_is_a_value_error() -> None:
+ with pytest.raises(ValueError, match=r"Chunk\(\), a chunk nothing is known of"):
+ CodecStrategy(configurations=lambda _chunk: None)
+
+
+@pytest.mark.parametrize("spellings", get_args(Spellings))
+def test_a_refused_draw_is_a_value_error(spellings: Spellings) -> None:
+ refused = fields(GZIP_CODEC, st.just({"level": 99}), spellings=spellings)
+ with pytest.raises(ValueError, match="which is refused"):
+ find(refused, lambda _field: True, settings=settings(database=None))
+
+
+_BFLOAT16 = DataTypeDefinition(name="bfloat16", configuration=EmptyConfiguration)
+"""A number type this package does not define, as an extension author would."""
+
+
+@given(data=st.data())
+def test_scale_offset_takes_a_data_type_the_package_does_not_define(data: st.DataObject) -> None:
+ """Values of a type the package does not define cannot be drawn, so the configuration is the identity, which its rules take."""
+ data_type, problems = resolve(
+ "bfloat16", DataTypeDefinition, ZarrV3Context(data_types=[_BFLOAT16])
+ )
+ assert problems == ()
+ chunk = Chunk(None, data_type)
+ configurations = codec_configurations(SCALE_OFFSET_CODEC, chunk)
+ assert configurations is not None
+ configuration = data.draw(configurations)
+ assert configuration == {}
+ assert list(SCALE_OFFSET_CODEC.chunk_rules(configuration, {}, chunk)) == []
+
+
+def test_a_definition_no_v3_scope_holds_is_a_type_error() -> None:
+ zlib = CORE_V2_CONTEXT.codecs["zlib"]
+ with pytest.raises(TypeError, match="which no slot of a Zarr v3 scope holds"):
+ fields(zlib, st.just({}))
+
+
+@pytest.mark.parametrize(
+ "definition", [FLOAT16_DATA_TYPE, FLOAT32_DATA_TYPE, FLOAT64_DATA_TYPE], ids=_id
+)
+@pytest.mark.parametrize("member", ["offset", "scale"])
+def test_scale_offset_reaches_a_value_written_in_hex(
+ definition: DataTypeDefinition[Any], member: str
+) -> None:
+ """A float's `offset` and `scale` are fill values of its type, which the bits in hex spell too."""
+ chunk = Chunk(None, _read(definition, definition.name))
+ configurations = codec_configurations(SCALE_OFFSET_CODEC, chunk)
+ assert configurations is not None
+ find(
+ configurations,
+ lambda configuration: isinstance(configuration.get(member), str),
+ settings=_REACH,
+ )
+
+
+def _has_pair(configuration: Mapping[str, Any]) -> bool:
+ return any(
+ isinstance(entry, tuple)
+ for dimension in configuration["chunk_shapes"]
+ if isinstance(dimension, tuple)
+ for entry in cast("tuple[object, ...]", dimension)
+ )
+
+
+def test_rectilinear_edges_reach_a_run_written_as_a_pair() -> None:
+ """The `[size, count]` spelling, which no TypedDict target asks for: it is one arm of a union."""
+ find(chunk_grid_configurations(RECTILINEAR_CHUNK_GRID), _has_pair, settings=_REACH)
+
+
+FillValueSpelling = Literal[
+ "number", "named", "hex lowercase", "hex uppercase", "array", "base64", "NaT"
+]
+
+_FILL_VALUE_SPELLINGS: list[tuple[DataTypeDefinition[Any], FillValueSpelling]] = [
+ *(
+ (definition, spelling)
+ for definition in (
+ FLOAT16_DATA_TYPE,
+ FLOAT32_DATA_TYPE,
+ FLOAT64_DATA_TYPE,
+ COMPLEX64_DATA_TYPE,
+ COMPLEX128_DATA_TYPE,
+ )
+ for spelling in get_args(FillValueSpelling)[:4]
+ ),
+ (BYTES_DATA_TYPE, "array"),
+ (BYTES_DATA_TYPE, "base64"),
+ (NUMPY_DATETIME64_DATA_TYPE, "number"),
+ (NUMPY_DATETIME64_DATA_TYPE, "NaT"),
+ (NUMPY_TIMEDELTA64_DATA_TYPE, "number"),
+ (NUMPY_TIMEDELTA64_DATA_TYPE, "NaT"),
+]
+"""The spellings the specs give a fill value, by the data types whose fill values take them."""
+
+
+def _spellings_of(value: object) -> set[FillValueSpelling]:
+ """How a fill value is spelled; a complex one, how either component is."""
+ if isinstance(value, bool):
+ return set()
+ if isinstance(value, int | float):
+ return {"number"}
+ if isinstance(value, tuple):
+ parts = cast("tuple[object, ...]", value)
+ if all(isinstance(part, int) for part in parts):
+ return {"array"}
+ return {spelling for part in parts for spelling in _spellings_of(part)}
+ if not isinstance(value, str):
+ return set()
+ if value == "NaT":
+ return {"NaT"}
+ if value in get_args(FloatSpecialFillValue):
+ return {"named"}
+ if value.startswith("0x"):
+ digits = value[2:]
+ return {
+ *(("hex lowercase",) if any(c in "abcdef" for c in digits) else ()),
+ *(("hex uppercase",) if any(c in "ABCDEF" for c in digits) else ()),
+ }
+ return {"base64"}
+
+
+@pytest.mark.parametrize(("definition", "spelling"), _FILL_VALUE_SPELLINGS, ids=_id)
+def test_fill_values_reach(
+ definition: DataTypeDefinition[Any], spelling: FillValueSpelling
+) -> None:
+ values = data_type_configurations(definition).flatmap(
+ lambda configuration: fill_values(definition, configuration)
+ )
+ find(values, lambda value: spelling in _spellings_of(value), settings=_REACH)
diff --git a/packages/zarr-metadata/tests/v2/test_definition.py b/packages/zarr-metadata/tests/v2/test_definition.py
index 98f62d68a6..4a9efa37ad 100644
--- a/packages/zarr-metadata/tests/v2/test_definition.py
+++ b/packages/zarr-metadata/tests/v2/test_definition.py
@@ -14,44 +14,44 @@
from zarr_metadata.v2.codec import CODECS_V2
from zarr_metadata.v2.data_type import DATA_TYPES_V2
from zarr_metadata.v2.definition import (
- CORE_V2,
+ CORE_V2_CONTEXT,
ZarrV2CodecDefinition,
ZarrV2Context,
ZarrV2DataTypeDefinition,
)
from zarr_metadata.v3.codec.gzip import GZIP_CODEC
from zarr_metadata.v3.definition import (
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
CodecDefinition,
)
def test_core_v2_files_every_v2_definition_apart_from_v3() -> None:
- """`CORE_V2` files the 12 data types and 21 codecs by their v2 kinds, and is a scope of format 2: a v2 `gzip` and a v3 `gzip` are two definitions of two different kinds. No scope can file both, because `union` rejects scopes of two formats."""
- assert set(CORE_V2.data_types.values()) == set(DATA_TYPES_V2)
- assert set(CORE_V2.codecs.values()) == set(CODECS_V2)
- assert CORE_V2.zarr_format == 2
- assert CORE_V2.claimant(ZarrV2CodecDefinition, "gzip") is not GZIP_CODEC
- assert CORE_V2.claimant(ZarrV2CodecDefinition, "gzip") is not None
- assert CORE_AND_EXTENSIONS.claimant(CodecDefinition, "gzip") is GZIP_CODEC
- assert CORE_V2.claimant(ZarrV2DataTypeDefinition, " None:
"""`resolve` reads a v2 dtype or codec with its kind in a v2 scope, prefixing every problem with `loc`, as it reads a v3 field."""
- dtype, problems = resolve(" list[tuple[tuple[str | int
def test_core_is_a_subset_of_core_and_extensions() -> None:
- """Each slot of `CORE` files a subset of what the same slot of `CORE_AND_EXTENSIONS` files."""
+ """Each slot of `CORE_CONTEXT` files a subset of what the same slot of `CORE_AND_EXTENSIONS_CONTEXT` files."""
for kind in ZarrV3Context.kinds.values():
- assert CORE.definitions_of(kind).items() <= CORE_AND_EXTENSIONS.definitions_of(kind).items()
+ assert (
+ CORE_CONTEXT.definitions_of(kind).items()
+ <= CORE_AND_EXTENSIONS_CONTEXT.definitions_of(kind).items()
+ )
def test_a_read_field_keeps_the_fields_it_read_inside() -> None:
@@ -240,7 +243,7 @@ def test_a_read_field_keeps_the_fields_it_read_inside() -> None:
"index_codecs": ["bytes", "crc32c"],
},
}
- resolved, _ = resolve(shard, CodecDefinition, CORE_AND_EXTENSIONS)
+ resolved, _ = resolve(shard, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert isinstance(resolved, AcceptedField)
assert {loc: inner.json for loc, inner in resolved.nested.items()} == {
("codecs", 0): "bytes",
@@ -248,17 +251,19 @@ def test_a_read_field_keeps_the_fields_it_read_inside() -> None:
("index_codecs", 1): "crc32c",
}
cast_value = {"name": "cast_value", "configuration": {"data_type": "int8"}}
- resolved, _ = resolve(cast_value, CodecDefinition, CORE_AND_EXTENSIONS)
+ resolved, _ = resolve(cast_value, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert isinstance(resolved, AcceptedField)
assert resolved.nested[("data_type",)] == INT8
# A field with no nested fields has nothing inside. A field that nothing
# in scope claims holds no fields at all. A field refused by its check
# or rules keeps the nested fields it read.
- assert resolve("int8", DataTypeDefinition, CORE)[0] == INT8
+ assert resolve("int8", DataTypeDefinition, CORE_CONTEXT)[0] == INT8
unclaimed = {"name": "acme.cast", "configuration": {"data_type": "int8"}}
- assert isinstance(resolve(unclaimed, CodecDefinition, CORE_AND_EXTENSIONS)[0], UnclaimedField)
+ assert isinstance(
+ resolve(unclaimed, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)[0], UnclaimedField
+ )
refused = {"name": "cast_value", "configuration": {"data_type": "int8", "rounding": 1}}
- resolved, _ = resolve(refused, CodecDefinition, CORE_AND_EXTENSIONS)
+ resolved, _ = resolve(refused, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert isinstance(resolved, RefusedField)
assert resolved.nested[("data_type",)] == INT8
@@ -279,7 +284,7 @@ def test_a_read_field_keeps_the_fields_it_read_inside() -> None:
def test_a_reading_says_the_name_the_field_was_written_with(
field: JSONValue, kind: type[Definition[Any]], name: str | None
) -> None:
- assert resolve(field, kind, CORE_AND_EXTENSIONS)[0].name == name
+ assert resolve(field, kind, CORE_AND_EXTENSIONS_CONTEXT)[0].name == name
KINDLESS: Final = Definition(name="acme.kindless", configuration=EmptyConfiguration)
@@ -766,7 +771,7 @@ def test_error_must_understand_false_is_refused_wherever_the_model_refuses_it(
for problem in read_array_metadata_v3(document).problems
if problem.loc[: len(at)] == at
]
- assert _locs(resolve(field, kind, CORE_AND_EXTENSIONS)[1]) == by_model
+ assert _locs(resolve(field, kind, CORE_AND_EXTENSIONS_CONTEXT)[1]) == by_model
assert by_model == [(("must_understand",), "invalid_value")]
@@ -774,7 +779,7 @@ def _ruled_by(
rules: Callable[[GzipCodecConfiguration, Nested], Iterable[ValidationProblem]],
) -> Context:
lying = replace(GZIP_CODEC, name="acme.gzip", rules=rules)
- return CORE.update(codecs=[*CORE.codecs.values(), lying])
+ return CORE_CONTEXT.update(codecs=[*CORE_CONTEXT.codecs.values(), lying])
def test_error_a_rule_that_yields_something_else() -> None:
@@ -1044,7 +1049,7 @@ class _HugeFormat(_ClaimsWithNoFormat):
"context",
[
None,
- "CORE",
+ "CORE_CONTEXT",
ZarrV3Context,
_ClaimsWithNoFormat(),
_BooleanFormatScope(),
@@ -1126,7 +1131,9 @@ def test_a_scope_takes_a_name_over() -> None:
else []
),
)
- context = CORE_AND_EXTENSIONS.update(codecs={**CORE_AND_EXTENSIONS.codecs, "gzip": strict})
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
+ codecs={**CORE_AND_EXTENSIONS_CONTEXT.codecs, "gzip": strict}
+ )
_, found = resolve({"name": "gzip", "configuration": {"level": 0}}, CodecDefinition, context)
assert _locs(found) == [(("configuration", "level"), "invalid_value")]
assert context.claimant(ChunkGridDefinition, "regular") is REGULAR_CHUNK_GRID
@@ -1134,17 +1141,17 @@ def test_a_scope_takes_a_name_over() -> None:
def test_a_scope_is_shown_by_how_many_definitions_it_holds() -> None:
# Kept short, because `help` shows it as a validator's default argument.
- count = sum(len(CORE.definitions_of(kind)) for kind in ZarrV3Context.kinds.values())
- assert repr(CORE) == f"ZarrV3Context(<{count} definitions>)"
+ count = sum(len(CORE_CONTEXT.definitions_of(kind)) for kind in ZarrV3Context.kinds.values())
+ assert repr(CORE_CONTEXT) == f"ZarrV3Context(<{count} definitions>)"
def test_a_scope_pickles_as_its_definitions_and_copies_as_itself() -> None:
# A model holds the scope it was read in, and takes it along to another process.
- again = pickle.loads(pickle.dumps(CORE_AND_EXTENSIONS))
- assert again == CORE_AND_EXTENSIONS
- assert again.codecs == CORE_AND_EXTENSIONS.codecs
- assert copy.copy(CORE) is CORE
- assert copy.deepcopy(CORE) is CORE
+ again = pickle.loads(pickle.dumps(CORE_AND_EXTENSIONS_CONTEXT))
+ assert again == CORE_AND_EXTENSIONS_CONTEXT
+ assert again.codecs == CORE_AND_EXTENSIONS_CONTEXT.codecs
+ assert copy.copy(CORE_CONTEXT) is CORE_CONTEXT
+ assert copy.deepcopy(CORE_CONTEXT) is CORE_CONTEXT
LE: Final = {"name": "bytes", "configuration": {"endian": "little"}}
@@ -1279,8 +1286,8 @@ def test_two_fields_are_equal_when_they_read_the_same(
one: JSONValue, other: JSONValue, kind: type[Definition[Any]], equal: bool
) -> None:
"""However each field was spelled, equal fields have the same simplest spelling and the same hash, though each is written back as it was read."""
- first, _ = resolve(one, kind, CORE_AND_EXTENSIONS)
- second, _ = resolve(other, kind, CORE_AND_EXTENSIONS)
+ first, _ = resolve(one, kind, CORE_AND_EXTENSIONS_CONTEXT)
+ second, _ = resolve(other, kind, CORE_AND_EXTENSIONS_CONTEXT)
assert isinstance(first, (AcceptedField, UnclaimedField))
assert isinstance(second, (AcceptedField, UnclaimedField))
assert (first == second) is equal
@@ -1290,7 +1297,9 @@ def test_two_fields_are_equal_when_they_read_the_same(
def test_a_field_copied_or_pickled_is_read_by_a_definition_equal_to_its_own() -> None:
- field, _ = resolve({"name": "gzip", "configuration": {"level": 1}}, CodecDefinition, CORE)
+ field, _ = resolve(
+ {"name": "gzip", "configuration": {"level": 1}}, CodecDefinition, CORE_CONTEXT
+ )
for again in (pickle.loads(pickle.dumps(field)), copy.deepcopy(field)):
assert again == field
assert configuration_of(again, GZIP_CODEC) == {"level": 1}
@@ -1508,7 +1517,7 @@ def test_error_a_scope_refuses_a_definition_of_no_kind() -> None:
def test_an_extension_is_named_as_the_spec_names_one(name: str, valid: bool) -> None:
"""An extension name matches the spec's regex `^[a-z][a-z0-9-_.]+$`, or is a URI, which earlier versions of the spec required. Any other name is refused before any definition is consulted."""
for field, at in ((name, ()), ({"name": name}, ("name",))):
- resolved, problems = resolve(field, CodecDefinition, CORE)
+ resolved, problems = resolve(field, CodecDefinition, CORE_CONTEXT)
if valid:
assert not isinstance(resolved, RefusedField)
assert problems == ()
@@ -1523,7 +1532,7 @@ def test_error_a_bad_name_is_a_problem_when_the_field_is_not_json_too() -> None:
# A field whose configuration is not JSON reports its name problem as
# the JSON path does, and no definition is asked to claim it.
field = {"name": "Acme", "configuration": {"x": float("nan")}}
- resolved, problems = resolve(field, CodecDefinition, CORE)
+ resolved, problems = resolve(field, CodecDefinition, CORE_CONTEXT)
assert isinstance(resolved, RefusedField)
assert resolved.definition is None
assert [(p.loc, p.kind) for p in problems] == [
@@ -1547,7 +1556,7 @@ def test_error_a_field_built_by_hand_is_named_as_the_spec_names_one() -> None:
def test_error_a_field_without_a_name_is_missing_one() -> None:
- resolved, problems = resolve({"configuration": {}}, CodecDefinition, CORE)
+ resolved, problems = resolve({"configuration": {}}, CodecDefinition, CORE_CONTEXT)
assert isinstance(resolved, RefusedField)
assert [(p.loc, p.kind, p.message) for p in problems] == [
(("name",), "missing_key", "missing required key")
@@ -1559,7 +1568,9 @@ def test_raw_bits_of_more_than_a_hundred_digits_are_no_size_but_a_name(digits: i
# `int` refuses to convert more than 4,300 digits, and no real size has a
# hundred digits. Such a name is an extension name, which nothing in
# scope claims.
- resolved, problems = resolve("r" + "1" * digits, DataTypeDefinition, CORE_AND_EXTENSIONS)
+ resolved, problems = resolve(
+ "r" + "1" * digits, DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT
+ )
assert type(resolved) is UnclaimedField
assert problems == ()
@@ -1573,8 +1584,8 @@ def writes(
cast("dict[str, object]", configuration)["level"] = -1
yield from ()
- context = CORE.update(
- codecs={**CORE.codecs, "gzip": dataclasses.replace(GZIP_CODEC, rules=writes)}
+ context = CORE_CONTEXT.update(
+ codecs={**CORE_CONTEXT.codecs, "gzip": dataclasses.replace(GZIP_CODEC, rules=writes)}
)
with pytest.raises(TypeError, match="does not support item assignment") as raised:
resolve({"name": "gzip", "configuration": {"level": 1}}, CodecDefinition, context)
@@ -1598,8 +1609,11 @@ def folds_in_place(configuration: GzipCodecConfiguration) -> GzipCodecConfigurat
cast("dict[str, object]", configuration)["level"] = 0
return configuration
- context = CORE.update(
- codecs={**CORE.codecs, "gzip": dataclasses.replace(GZIP_CODEC, canonical=folds_in_place)}
+ context = CORE_CONTEXT.update(
+ codecs={
+ **CORE_CONTEXT.codecs,
+ "gzip": dataclasses.replace(GZIP_CODEC, canonical=folds_in_place),
+ }
)
read, _ = resolve({"name": "gzip", "configuration": {"level": 1}}, CodecDefinition, context)
with pytest.raises(TypeError, match="does not support item assignment"):
@@ -1610,8 +1624,8 @@ def test_error_a_canonical_that_raises_says_which_definition_raised_it() -> None
def refuses(configuration: GzipCodecConfiguration) -> GzipCodecConfiguration:
raise ValueError("no")
- context = CORE.update(
- codecs={**CORE.codecs, "gzip": dataclasses.replace(GZIP_CODEC, canonical=refuses)}
+ context = CORE_CONTEXT.update(
+ codecs={**CORE_CONTEXT.codecs, "gzip": dataclasses.replace(GZIP_CODEC, canonical=refuses)}
)
read, _ = resolve({"name": "gzip", "configuration": {"level": 1}}, CodecDefinition, context)
with pytest.raises(ValueError, match="no") as raised:
@@ -1622,7 +1636,9 @@ def refuses(configuration: GzipCodecConfiguration) -> GzipCodecConfiguration:
def test_error_a_field_s_configuration_and_nested_fields_are_read_only() -> None:
"""A field's `json`, `configuration` and `nested` attributes are read-only at every level. So a field cannot be changed into a state where its key, `==` and `refines` disagree."""
whole = {**SHARD, "index_codecs": [LE, {"name": "crc32c"}]}
- shard, _ = resolve({"name": "sharding_indexed", "configuration": whole}, CodecDefinition, CORE)
+ shard, _ = resolve(
+ {"name": "sharding_indexed", "configuration": whole}, CodecDefinition, CORE_CONTEXT
+ )
assert isinstance(shard, AcceptedField)
with pytest.raises(TypeError):
shard.configuration["index_location"] = "start" # pyright: ignore[reportIndexIssue]
@@ -1637,14 +1653,16 @@ def test_error_a_field_s_configuration_and_nested_fields_are_read_only() -> None
assert isinstance(shard.json, Mapping)
with pytest.raises(TypeError):
shard.json["name"] = "gzip" # pyright: ignore[reportIndexIssue]
- unclaimed, _ = resolve({"name": "acme.x", "configuration": {"a": [1]}}, CodecDefinition, CORE)
+ unclaimed, _ = resolve(
+ {"name": "acme.x", "configuration": {"a": [1]}}, CodecDefinition, CORE_CONTEXT
+ )
assert isinstance(unclaimed, UnclaimedField)
with pytest.raises(TypeError):
unclaimed.configuration["a"] = 2 # pyright: ignore[reportIndexIssue]
refused, _ = resolve(
{"name": "sharding_indexed", "configuration": {**whole, "index_location": "x"}},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
)
assert isinstance(refused, RefusedField)
with pytest.raises(TypeError):
@@ -1655,12 +1673,14 @@ def test_a_field_holding_fields_is_copied_and_pickled_whole() -> None:
"""A field pickles and deep-copies together with the fields it holds, and stays equal to itself, whether it is accepted, refused or unclaimed."""
whole = {**SHARD, "index_codecs": [LE, {"name": "crc32c"}]}
fields = [
- resolve({"name": "sharding_indexed", "configuration": whole}, CodecDefinition, CORE)[0],
- resolve({"name": "acme.x", "configuration": {"a": [1]}}, CodecDefinition, CORE)[0],
+ resolve(
+ {"name": "sharding_indexed", "configuration": whole}, CodecDefinition, CORE_CONTEXT
+ )[0],
+ resolve({"name": "acme.x", "configuration": {"a": [1]}}, CodecDefinition, CORE_CONTEXT)[0],
resolve(
{"name": "sharding_indexed", "configuration": {**whole, "index_location": "x"}},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
)[0],
]
for field in fields:
@@ -1697,8 +1717,8 @@ def test_a_field_its_caller_refined_reads_as_resolve_reads_it(field: object) ->
"""`resolve_refined` is `resolve` for a caller that has already refined the field. It gives the same field and the same problems, at the same locations, with the same `input`."""
refined, problems = refine_json(field, ("codecs", 0))
assert problems == ()
- expected = resolve(field, CodecDefinition, CORE, ("codecs", 0))
- found = resolve_refined(field, refined, problems, CodecDefinition, CORE, ("codecs", 0))
+ expected = resolve(field, CodecDefinition, CORE_CONTEXT, ("codecs", 0))
+ found = resolve_refined(field, refined, problems, CodecDefinition, CORE_CONTEXT, ("codecs", 0))
assert found == expected
assert [(p.loc, p.kind, p.input) for p in found[1]] == [
(p.loc, p.kind, p.input) for p in expected[1]
@@ -1711,7 +1731,7 @@ def test_error_a_field_its_caller_found_not_to_be_json_is_refused_unread() -> No
refined, problems = refine_json(field, ("codecs", 0))
assert refined is None
resolved, found = resolve_refined(
- field, refined, problems, CodecDefinition, CORE, ("codecs", 0)
+ field, refined, problems, CodecDefinition, CORE_CONTEXT, ("codecs", 0)
)
assert resolved == RefusedField(
json=UNSET, name="gzip", read_as=CodecDefinition, definition=GZIP_CODEC
@@ -1719,12 +1739,12 @@ def test_error_a_field_its_caller_found_not_to_be_json_is_refused_unread() -> No
assert [(p.loc, p.kind) for p in found] == [
(("codecs", 0, "configuration", "level"), "invalid_type")
]
- assert (resolved, found) == resolve(field, CodecDefinition, CORE, ("codecs", 0))
+ assert (resolved, found) == resolve(field, CodecDefinition, CORE_CONTEXT, ("codecs", 0))
def test_error_a_refined_field_is_not_read_in_a_scope_of_the_other_format() -> None:
with pytest.raises(TypeError, match="got a v2 scope"):
- resolve_refined("gzip", "gzip", (), CodecDefinition, CORE_V2)
+ resolve_refined("gzip", "gzip", (), CodecDefinition, CORE_V2_CONTEXT)
V2_IN_A_V3_SCOPE = (
@@ -1738,10 +1758,10 @@ def test_error_a_refined_field_is_not_read_in_a_scope_of_the_other_format() -> N
@pytest.mark.parametrize(
("field", "kind", "context", "message"),
[
- ({"id": "gzip"}, ZarrV2CodecDefinition, CORE, V2_IN_A_V3_SCOPE),
- (5, ZarrV2CodecDefinition, CORE, V2_IN_A_V3_SCOPE),
- ({"name": "gzip"}, CodecDefinition, CORE_V2, V3_IN_A_V2_SCOPE),
- (5, CodecDefinition, CORE_V2, V3_IN_A_V2_SCOPE),
+ ({"id": "gzip"}, ZarrV2CodecDefinition, CORE_CONTEXT, V2_IN_A_V3_SCOPE),
+ (5, ZarrV2CodecDefinition, CORE_CONTEXT, V2_IN_A_V3_SCOPE),
+ ({"name": "gzip"}, CodecDefinition, CORE_V2_CONTEXT, V3_IN_A_V2_SCOPE),
+ (5, CodecDefinition, CORE_V2_CONTEXT, V3_IN_A_V2_SCOPE),
],
ids=["v2-named", "v2-nameless", "v3-named", "v3-nameless"],
)
diff --git a/packages/zarr-metadata/tests/v3/test_every_definition.py b/packages/zarr-metadata/tests/v3/test_every_definition.py
index f881aa5b3c..6a9a743c50 100644
--- a/packages/zarr-metadata/tests/v3/test_every_definition.py
+++ b/packages/zarr-metadata/tests/v3/test_every_definition.py
@@ -28,8 +28,9 @@
from zarr_metadata.v3.codec.gzip import GZIP_CODEC
from zarr_metadata.v3.data_type.raw import RAW_BYTES_DATA_TYPE, RawBytesConfiguration
from zarr_metadata.v3.definition import (
- CORE,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
+ ZARR_PYTHON_CONTEXT,
ChunkGridDefinition,
ChunkKeyEncodingDefinition,
CodecDefinition,
@@ -47,7 +48,8 @@
"group_extra_field": GroupExtraFieldDefinition,
}
-# Fields that read without problems, per extension: at least one per definition in scope.
+# Fields that read without problems, per extension: at least one per definition in
+# the widest scope the package provides.
EXAMPLES: dict[str, tuple[object, ...]] = {
"codecs:blosc": (
{
@@ -89,6 +91,8 @@
},
},
),
+ "codecs:vlen-bytes": ("vlen-bytes", {"name": "vlen-bytes"}),
+ "codecs:vlen-utf8": ("vlen-utf8", {"name": "vlen-utf8"}),
"codecs:transpose": ({"name": "transpose", "configuration": {"order": [2, 1, 0]}},),
"codecs:zstd": ({"name": "zstd", "configuration": {"level": 3, "checksum": False}},),
"chunk_grid:regular": ({"name": "regular", "configuration": {"chunk_shape": [4, 4]}},),
@@ -143,6 +147,14 @@
},
),
"data_type:string": ("string",),
+ "data_type:fixed_length_utf32": (
+ {"name": "fixed_length_utf32", "configuration": {"length_bytes": 48}},
+ ),
+ "data_type:null_terminated_bytes": (
+ {"name": "null_terminated_bytes", "configuration": {"length_bytes": 1}},
+ ),
+ "data_type:variable_length_bytes": ("variable_length_bytes",),
+ "data_type:raw_bytes": ({"name": "raw_bytes", "configuration": {"length_bytes": 1}},),
"data_type:r*": ("r16", "r008"),
"group_extra_field:consolidated_metadata": (
{"consolidated_metadata": {"kind": "inline", "must_understand": False, "metadata": {}}},
@@ -161,7 +173,7 @@
def _read(key: str, field: object) -> tuple[type, list[tuple[tuple[str | int, ...], str]]]:
"""The type of the result of reading `field` in the scope (`AcceptedField`, `UnclaimedField` or `RefusedField`), and the location of each problem."""
- resolved, problems = resolve(field, KINDS[key.split(":")[0]], CORE_AND_EXTENSIONS)
+ resolved, problems = resolve(field, KINDS[key.split(":")[0]], ZARR_PYTHON_CONTEXT)
return type(resolved), [(found.loc, found.kind) for found in problems]
@@ -171,7 +183,7 @@ def _problems(key: str, field: object) -> list[tuple[tuple[str | int, ...], str]
def test_a_scope_s_definitions_pickle() -> None:
# This lets a scope be sent to another process and rebuilt there.
- data_types = pickle.loads(pickle.dumps(tuple(CORE_AND_EXTENSIONS.data_types.values())))
+ data_types = pickle.loads(pickle.dumps(tuple(CORE_AND_EXTENSIONS_CONTEXT.data_types.values())))
context = ZarrV3Context(data_types=data_types)
resolved, _ = resolve("complex64", DataTypeDefinition, context)
assert [(p.loc, p.kind) for p in fill_value_problems(resolved, [1, "x"])] == [
@@ -189,9 +201,9 @@ def _every(context: ZarrV3Context) -> set[Definition[Any]]:
def test_every_definition_in_scope_has_an_example() -> None:
- names = {definition.name for definition in _every(CORE_AND_EXTENSIONS)}
+ names = {definition.name for definition in _every(ZARR_PYTHON_CONTEXT)}
assert names == {key.split(":")[1] for key in EXAMPLES}
- assert _every(CORE) < _every(CORE_AND_EXTENSIONS)
+ assert _every(CORE_CONTEXT) < _every(CORE_AND_EXTENSIONS_CONTEXT) < _every(ZARR_PYTHON_CONTEXT)
@pytest.mark.parametrize(
@@ -202,12 +214,12 @@ def test_every_example_reads_and_its_simplest_spelling_is_stable(key: str, field
# reads the same way, and is its own simplest spelling.
assert _read(key, field) == (AcceptedField, [])
kind = KINDS[key.split(":")[0]]
- simplest, problems = canonicalize(field, kind, CORE_AND_EXTENSIONS)
+ simplest, problems = canonicalize(field, kind, ZARR_PYTHON_CONTEXT)
assert problems == ()
assert simplest is not None
- assert canonicalize(simplest, kind, CORE_AND_EXTENSIONS) == (simplest, ())
+ assert canonicalize(simplest, kind, ZARR_PYTHON_CONTEXT) == (simplest, ())
# A field that was already read gives the same spelling as its JSON, without reading it again.
- assert canonical_of(*resolve(field, kind, CORE_AND_EXTENSIONS)) == simplest
+ assert canonical_of(*resolve(field, kind, ZARR_PYTHON_CONTEXT)) == simplest
@pytest.mark.parametrize(
@@ -339,8 +351,8 @@ def test_the_simplest_spelling(field: dict[str, Any], simplest: object) -> None:
"v2": ChunkKeyEncodingDefinition,
}
kind = kinds.get(field["name"], CodecDefinition)
- assert canonicalize(field, kind, CORE_AND_EXTENSIONS) == (simplest, ())
- assert canonical_of(*resolve(field, kind, CORE_AND_EXTENSIONS)) == simplest
+ assert canonicalize(field, kind, CORE_AND_EXTENSIONS_CONTEXT) == (simplest, ())
+ assert canonical_of(*resolve(field, kind, CORE_AND_EXTENSIONS_CONTEXT)) == simplest
@pytest.mark.parametrize(
@@ -357,13 +369,13 @@ def test_the_simplest_spelling(field: dict[str, Any], simplest: object) -> None:
def test_raw_bits_read_as_r_star_with_the_size_their_name_carries(
field: object, bits: int, simplest: str
) -> None:
- resolved, problems = resolve(field, DataTypeDefinition, CORE_AND_EXTENSIONS)
+ resolved, problems = resolve(field, DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert problems == ()
assert isinstance(resolved, AcceptedField)
assert resolved.definition is RAW_BYTES_DATA_TYPE
assert resolved.json == field
assert configuration_of(resolved, RAW_BYTES_DATA_TYPE) == {"bits": bits}
- assert canonicalize(field, DataTypeDefinition, CORE_AND_EXTENSIONS) == (simplest, ())
+ assert canonicalize(field, DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT) == (simplest, ())
def test_a_reader_reads_raw_bits_its_own_way_by_defining_r_star() -> None:
@@ -371,7 +383,9 @@ def test_a_reader_reads_raw_bits_its_own_way_by_defining_r_star() -> None:
# package's own, it reads every raw-bits name. Filed back, the package's
# definition reads them again.
mine = DataTypeDefinition(name="r*", configuration=RawBytesConfiguration)
- context = CORE_AND_EXTENSIONS.update(data_types={**CORE_AND_EXTENSIONS.data_types, "r*": mine})
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
+ data_types={**CORE_AND_EXTENSIONS_CONTEXT.data_types, "r*": mine}
+ )
resolved, problems = resolve("r12", DataTypeDefinition, context)
assert problems == ()
assert isinstance(resolved, AcceptedField)
@@ -394,7 +408,7 @@ def test_a_reader_reads_raw_bits_its_own_way_by_defining_r_star() -> None:
def test_error_r_star_is_notation_and_no_name(field: object, at: tuple[str, ...]) -> None:
# `r*` is how the specification's table writes raw bits, not a name a
# document can use: `*` is not allowed in an extension name.
- resolved, problems = resolve(field, DataTypeDefinition, CORE_AND_EXTENSIONS)
+ resolved, problems = resolve(field, DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert type(resolved) is RefusedField
assert [(p.loc, p.kind) for p in problems] == [(at, "invalid_value")]
@@ -423,7 +437,7 @@ def test_error_r_star_is_notation_and_no_name(field: object, at: tuple[str, ...]
def test_an_unclaimed_field_keeps_its_configuration_in_its_kind_s_envelope(
field: object, kind: type[Definition[Any]], simplest: object
) -> None:
- assert canonicalize(field, kind, CORE) == (simplest, ())
+ assert canonicalize(field, kind, CORE_CONTEXT) == (simplest, ())
def _shard(codecs: list[object], index_codecs: list[object]) -> dict[str, object]:
@@ -492,10 +506,10 @@ def test_error_a_field_with_a_problem_has_no_simplest_spelling(
# unknown key, the stray member or the `must_understand`, or would
# write a value that is not valid. A field that was already read,
# together with its problems, has no simplest spelling either.
- simplest, problems = canonicalize(field, kind, CORE_AND_EXTENSIONS)
+ simplest, problems = canonicalize(field, kind, CORE_AND_EXTENSIONS_CONTEXT)
assert simplest is None
assert [(problem.loc, problem.kind) for problem in problems] == found
- assert canonical_of(*resolve(field, kind, CORE_AND_EXTENSIONS)) is None
+ assert canonical_of(*resolve(field, kind, CORE_AND_EXTENSIONS_CONTEXT)) is None
def test_error_a_canonical_that_does_not_hold_is_the_definitions_fault() -> None:
@@ -508,7 +522,9 @@ def test_error_a_canonical_that_does_not_hold_is_the_definitions_fault() -> None
canonicalize(
{"name": "acme.lying", "configuration": {"level": 1}},
CodecDefinition,
- CORE_AND_EXTENSIONS.update(codecs=[*CORE_AND_EXTENSIONS.codecs.values(), lying]),
+ CORE_AND_EXTENSIONS_CONTEXT.update(
+ codecs=[*CORE_AND_EXTENSIONS_CONTEXT.codecs.values(), lying]
+ ),
)
@@ -671,21 +687,24 @@ def test_a_bound_is_its_member_s_type_and_its_problem_holds_it(
) -> None:
# The bound is declared on the TypedDict, as pydantic reads a bound,
# not in a rule. The problem holds the bound and the value found.
- _, problems = resolve(field, kind, CORE_AND_EXTENSIONS)
+ _, problems = resolve(field, kind, CORE_AND_EXTENSIONS_CONTEXT)
assert [(p.loc, p.kind, p.input, dict(p.ctx)) for p in problems] == [
(loc, "invalid_value", value_at(field, loc), ctx)
]
STATIC_SIZE = ("bytes", "cast_value", "crc32c", "scale_offset", "transpose")
-DYNAMIC_SIZE = ("blosc", "gzip", "sharding_indexed", "zstd")
+DYNAMIC_SIZE = ("blosc", "gzip", "sharding_indexed", "vlen-bytes", "vlen-utf8", "zstd")
def test_every_codec_says_whether_its_size_is_static() -> None:
# This matches zarr-python's `is_fixed_size`. A compressor, and a shard
- # (whose empty inner chunks take no bytes), output a size that depends
- # on the values.
- sizes = {definition.name: definition.size for definition in CORE_AND_EXTENSIONS.codecs.values()}
+ # (whose empty inner chunks take no bytes), and a variable-length
+ # codec output a size that depends on the values.
+ sizes = {
+ definition.name: definition.size
+ for definition in CORE_AND_EXTENSIONS_CONTEXT.codecs.values()
+ }
assert sizes == dict.fromkeys(STATIC_SIZE, "static") | dict.fromkeys(DYNAMIC_SIZE, "dynamic")
diff --git a/packages/zarr-metadata/tests/v3/test_fill_values.py b/packages/zarr-metadata/tests/v3/test_fill_values.py
index 27436365b3..46e2152cfe 100644
--- a/packages/zarr-metadata/tests/v3/test_fill_values.py
+++ b/packages/zarr-metadata/tests/v3/test_fill_values.py
@@ -40,7 +40,7 @@
from zarr_metadata.v3.data_type._float import FloatWidth, complex_fill_value_rules, float_bits
from zarr_metadata.v3.data_type.struct import STRUCT_DATA_TYPE
from zarr_metadata.v3.definition import (
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
DataTypeDefinition,
)
@@ -86,7 +86,7 @@ def acme_point_fill_value_rules(
def _problems(data_type: JSONValue, value: object) -> list[tuple[tuple[str | int, ...], str]]:
- resolved, found = resolve(data_type, DataTypeDefinition, CORE_AND_EXTENSIONS)
+ resolved, found = resolve(data_type, DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert found == ()
return [(problem.loc, problem.kind) for problem in fill_value_problems(resolved, value)]
@@ -139,7 +139,7 @@ def _problems(data_type: JSONValue, value: object) -> list[tuple[tuple[str | int
],
)
def test_every_data_type_takes_its_fill_values(data_type: JSONValue, fill_value: object) -> None:
- resolved, _ = resolve(data_type, DataTypeDefinition, CORE_AND_EXTENSIONS)
+ resolved, _ = resolve(data_type, DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert fill_value_problems(resolved, fill_value) == ()
@@ -235,7 +235,7 @@ def test_a_fill_value_s_range_is_its_type_s(
) -> None:
# `Int8FillValue` is `Annotated[int, Interval(ge=-128, le=127)]`. The
# problem holds the range and the value that was found.
- resolved, _ = resolve(data_type, DataTypeDefinition, CORE_AND_EXTENSIONS)
+ resolved, _ = resolve(data_type, DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)
problems = fill_value_problems(resolved, fill_value)
assert [(p.loc, p.input, dict(p.ctx)) for p in problems] == [
(loc, value_at(fill_value, loc), ctx)
@@ -288,8 +288,8 @@ def test_error_an_array_document_s_fill_value_its_data_type_refuses() -> None:
def test_error_a_key_the_fill_value_shape_does_not_declare_hides_no_rule() -> None:
# The extra key is reported and hidden from the rules, which still check the other keys.
- context = CORE_AND_EXTENSIONS.update(
- data_types=[*CORE_AND_EXTENSIONS.data_types.values(), ACME_POINT]
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
+ data_types=[*CORE_AND_EXTENSIONS_CONTEXT.data_types.values(), ACME_POINT]
)
resolved, _ = resolve("acme.point", DataTypeDefinition, context)
found = fill_value_problems(resolved, {"x": 99, "extra": 1})
@@ -335,7 +335,7 @@ def _alike(left: object, right: object) -> bool:
def _read(data_type: JSONValue) -> AcceptedField[DataTypeDefinition[Any]]:
- resolved, found = resolve(data_type, DataTypeDefinition, CORE_AND_EXTENSIONS)
+ resolved, found = resolve(data_type, DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert found == ()
assert isinstance(resolved, AcceptedField)
return resolved
@@ -416,7 +416,7 @@ def test_a_fill_value_s_canonical_spelling_is_its_value_s(
def test_a_data_type_nothing_in_scope_claims_spells_a_fill_value_as_written() -> None:
- unclaimed, _ = resolve("acme.decimal", DataTypeDefinition, CORE_AND_EXTENSIONS)
+ unclaimed, _ = resolve("acme.decimal", DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)
values: list[JSONValue] = [-0.0, True, 1, [1.0], None]
for value in values:
assert _alike(canonical_fill_value(unclaimed, value), value)
@@ -453,7 +453,7 @@ def test_error_a_fill_value_with_a_problem_has_no_canonical_spelling() -> None:
assert canonical_fill_value(_read("float32"), "0x7fc0") is UNSET
assert canonical_fill_value(_read("int8"), 1.0) is UNSET
assert canonical_fill_value(_read("int8"), math.nan) is UNSET
- unclaimed, _ = resolve("acme.decimal", DataTypeDefinition, CORE_AND_EXTENSIONS)
+ unclaimed, _ = resolve("acme.decimal", DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert canonical_fill_value(unclaimed, None) is None
@@ -461,9 +461,9 @@ def test_error_a_canonical_spelling_that_raises_says_which_data_type_raised_it()
def refuses(configuration: EmptyConfiguration, nested: Nested, value: object) -> JSONValue:
raise ValueError("no")
- context = CORE_AND_EXTENSIONS.update(
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
data_types=[
- *CORE_AND_EXTENSIONS.data_types.values(),
+ *CORE_AND_EXTENSIONS_CONTEXT.data_types.values(),
dataclasses.replace(ACME_POINT, fill_value_canonical=refuses),
]
)
@@ -477,9 +477,9 @@ def test_error_a_canonical_spelling_that_is_not_json_is_refused() -> None:
def not_json(configuration: EmptyConfiguration, nested: Nested, value: object) -> JSONValue:
return cast("JSONValue", {1, 2})
- context = CORE_AND_EXTENSIONS.update(
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
data_types=[
- *CORE_AND_EXTENSIONS.data_types.values(),
+ *CORE_AND_EXTENSIONS_CONTEXT.data_types.values(),
dataclasses.replace(ACME_POINT, fill_value_canonical=not_json),
]
)
diff --git a/packages/zarr-metadata/tests/v3/test_grid_shapes.py b/packages/zarr-metadata/tests/v3/test_grid_shapes.py
index 5d79ea655e..6586624ce5 100644
--- a/packages/zarr-metadata/tests/v3/test_grid_shapes.py
+++ b/packages/zarr-metadata/tests/v3/test_grid_shapes.py
@@ -22,7 +22,7 @@
chunk_grid_lengths,
)
from zarr_metadata.v3.definition import (
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
ChunkGridDefinition,
)
@@ -39,7 +39,7 @@ def _rectilinear(*specs: JSONValue) -> JSONValue:
def _problems(grid: JSONValue, shape: tuple[int, ...]) -> list[tuple[tuple[str | int, ...], str]]:
- resolved, found = resolve(grid, ChunkGridDefinition, CORE_AND_EXTENSIONS)
+ resolved, found = resolve(grid, ChunkGridDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert found == ()
lengths, problems = chunk_grid_lengths(resolved, shape)
# A grid that does not fit the shape gives no lengths along any axis.
@@ -75,7 +75,7 @@ def _problems(grid: JSONValue, shape: tuple[int, ...]) -> list[tuple[tuple[str |
def test_every_chunk_grid_gives_the_lengths_of_its_chunks_over_a_shape_it_fits(
grid: JSONValue, shape: tuple[int, ...], lengths: tuple[set[int] | None, ...]
) -> None:
- resolved, _ = resolve(grid, ChunkGridDefinition, CORE_AND_EXTENSIONS)
+ resolved, _ = resolve(grid, ChunkGridDefinition, CORE_AND_EXTENSIONS_CONTEXT)
expected = tuple(None if axis is None else frozenset(axis) for axis in lengths)
assert chunk_grid_lengths(resolved, shape) == (expected, ())
@@ -103,8 +103,8 @@ def test_error_rectilinear_chunks_that_do_not_cover_their_dimension(spec: JSONVa
def test_a_grid_that_says_nothing_of_the_shape_fits_every_one_its_lengths_unknown() -> None:
lenient = ChunkGridDefinition(name="acme.grid", configuration=EmptyConfiguration)
- context = CORE_AND_EXTENSIONS.update(
- chunk_grids=[*CORE_AND_EXTENSIONS.chunk_grids.values(), lenient]
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
+ chunk_grids=[*CORE_AND_EXTENSIONS_CONTEXT.chunk_grids.values(), lenient]
)
resolved, _ = resolve("acme.grid", ChunkGridDefinition, context)
assert chunk_grid_lengths(resolved, (3, 4)) == ((None, None), ())
@@ -117,8 +117,8 @@ def test_error_a_grid_whose_chunk_lengths_have_another_number_of_axes() -> None:
configuration=EmptyConfiguration,
chunk_lengths=lambda c, n, s: (frozenset({1}),),
)
- context = CORE_AND_EXTENSIONS.update(
- chunk_grids=[*CORE_AND_EXTENSIONS.chunk_grids.values(), flat]
+ context = CORE_AND_EXTENSIONS_CONTEXT.update(
+ chunk_grids=[*CORE_AND_EXTENSIONS_CONTEXT.chunk_grids.values(), flat]
)
resolved, _ = resolve("acme.grid", ChunkGridDefinition, context)
with pytest.raises(ValueError, match="'acme.grid': its chunk_lengths gave 1 axes"):
@@ -162,7 +162,9 @@ def test_error_a_grid_whose_chunk_lengths_give_something_else_or_raise(
resolved, _ = resolve(
"acme.grid",
ChunkGridDefinition,
- CORE_AND_EXTENSIONS.update(chunk_grids=[*CORE_AND_EXTENSIONS.chunk_grids.values(), odd]),
+ CORE_AND_EXTENSIONS_CONTEXT.update(
+ chunk_grids=[*CORE_AND_EXTENSIONS_CONTEXT.chunk_grids.values(), odd]
+ ),
)
with pytest.raises((TypeError, KeyError), match=match) as raised:
chunk_grid_lengths(resolved, (3, 4), ("chunk_grid",))
diff --git a/packages/zarr-metadata/tests/v3/test_kinds.py b/packages/zarr-metadata/tests/v3/test_kinds.py
index 37fafd05f2..836f37e96c 100644
--- a/packages/zarr-metadata/tests/v3/test_kinds.py
+++ b/packages/zarr-metadata/tests/v3/test_kinds.py
@@ -17,9 +17,9 @@
from zarr_metadata.definition._definition import as_kind, kind_of
from zarr_metadata.definition._scope import kind_name
from zarr_metadata.json_schema import field_json_schema_v3
-from zarr_metadata.v2.definition import CORE_V2, ZarrV2Context, ZarrV2DataTypeDefinition
+from zarr_metadata.v2.definition import CORE_V2_CONTEXT, ZarrV2Context, ZarrV2DataTypeDefinition
from zarr_metadata.v3.definition import (
- CORE,
+ CORE_CONTEXT,
CodecDefinition,
DataTypeDefinition,
ZarrV3Context,
@@ -68,7 +68,7 @@ def test_error_a_class_that_declares_no_kind_is_of_none() -> None:
def test_error_the_json_schema_writer_writes_v3_fields_only() -> None:
"""`field_json_schema_v3` writes only the v3 envelope. For a kind of another format, it raises a `TypeError` that names this limit instead of writing a wrong schema."""
with pytest.raises(TypeError, match="Zarr v3"):
- field_json_schema_v3(ZarrV2DataTypeDefinition, context=CORE_V2) # pyright: ignore[reportArgumentType]
+ field_json_schema_v3(ZarrV2DataTypeDefinition, context=CORE_V2_CONTEXT) # pyright: ignore[reportArgumentType]
def test_a_v3_value_that_is_no_field_is_shown_in_the_message() -> None:
@@ -76,7 +76,7 @@ def test_a_v3_value_that_is_no_field_is_shown_in_the_message() -> None:
_, problems = resolve(
{"name": "sharding_indexed", "configuration": {"chunk_shape": [1], "codecs": [3]}},
CodecDefinition,
- CORE,
+ CORE_CONTEXT,
)
assert any(p.message.endswith("got 3") for p in problems), [p.message for p in problems]
@@ -93,8 +93,8 @@ def test_error_a_kind_declares_its_format() -> None:
def test_a_scope_has_the_format_of_its_class() -> None:
"""A scope's `zarr_format` is the format of every kind its class holds: 3 for a `ZarrV3Context` and 2 for a `ZarrV2Context`, including one that files nothing."""
- assert CORE.zarr_format == 3
- assert CORE_V2.zarr_format == 2
+ assert CORE_CONTEXT.zarr_format == 3
+ assert CORE_V2_CONTEXT.zarr_format == 2
assert ZarrV3Context().zarr_format == 3
assert ZarrV2Context().zarr_format == 2
@@ -106,17 +106,17 @@ def test_error_a_scope_files_one_format() -> None:
with pytest.raises(TypeError, match="v2 data type"):
ZarrV3Context(data_types=[UINT_DATA_TYPE_V2]) # pyright: ignore[reportArgumentType]
with pytest.raises(TypeError, match="v2 data type"):
- CORE.update(data_types=[*CORE.data_types.values(), UINT_DATA_TYPE_V2]) # pyright: ignore[reportArgumentType]
+ CORE_CONTEXT.update(data_types=[*CORE_CONTEXT.data_types.values(), UINT_DATA_TYPE_V2]) # pyright: ignore[reportArgumentType]
with pytest.raises(TypeError, match="format"):
- CORE.union(CORE_V2) # pyright: ignore[reportArgumentType]
+ CORE_CONTEXT.union(CORE_V2_CONTEXT) # pyright: ignore[reportArgumentType]
def test_error_a_field_is_read_in_a_scope_of_its_format() -> None:
"""`resolve` raises `TypeError` for a scope whose format differs from the kind's. In an empty scope of its format, it reads a field as unclaimed."""
with pytest.raises(TypeError, match="format"):
- resolve(" C
)
"""An array -> array codec that does not declare its output chunk."""
-CONTEXT = CORE_AND_EXTENSIONS.update(
- codecs=[*CORE_AND_EXTENSIONS.codecs.values(), ACME_TILE, ACME_SHUFFLE]
+CONTEXT = CORE_AND_EXTENSIONS_CONTEXT.update(
+ codecs=[*CORE_AND_EXTENSIONS_CONTEXT.codecs.values(), ACME_TILE, ACME_SHUFFLE]
)
UINT8 = resolve("uint8", DataTypeDefinition, CONTEXT)[0]
@@ -187,7 +187,7 @@ def _lengths(*axes: set[int]) -> tuple[frozenset[int], ...]:
CHUNK,
[CHUNK, Chunk(), Chunk(None, _of("float32").data_type)],
),
- ([_scale(offset=1), "vlen-utf8"], Chunk(), [Chunk(), Chunk()]),
+ ([_scale(offset=1), "acme.vlen"], Chunk(), [Chunk(), Chunk()]),
# Where the specs disagree or say nothing, a data type is not checked.
# The numpy time types say they accept any codec of 64-bit integers,
# and scale_offset does not say whether it accepts complex numbers.
@@ -330,7 +330,7 @@ def test_error_an_arithmetic_codec_handed_values_that_are_no_numbers(
) -> None:
# The codec comes before an array -> bytes codec that nothing in scope
# claims, which is not checked.
- assert _problems([codec, "vlen-utf8"], _of(data_type)) == [((0,), "invalid_value")]
+ assert _problems([codec, "acme.vlen"], _of(data_type)) == [((0,), "invalid_value")]
def test_error_a_codec_handed_on_what_another_could_not_take_is_judged_too() -> None:
@@ -343,7 +343,7 @@ def test_error_a_codec_handed_on_what_another_could_not_take_is_judged_too() ->
def test_error_a_cast_handed_complex_numbers() -> None:
- assert _problems([_cast("float32"), "vlen-utf8"], _of("complex64")) == [((0,), "invalid_value")]
+ assert _problems([_cast("float32"), "acme.vlen"], _of("complex64")) == [((0,), "invalid_value")]
@pytest.mark.parametrize(
diff --git a/packages/zarr-metadata/tests/v3/test_rule_locations.py b/packages/zarr-metadata/tests/v3/test_rule_locations.py
index 585a70ae61..499b934674 100644
--- a/packages/zarr-metadata/tests/v3/test_rule_locations.py
+++ b/packages/zarr-metadata/tests/v3/test_rule_locations.py
@@ -9,10 +9,10 @@
from zarr_metadata import ValidationProblem
from zarr_metadata.definition import RefusedField, resolve
-from zarr_metadata.v2.definition import CORE_V2, ZarrV2DataTypeDefinition
+from zarr_metadata.v2.definition import CORE_V2_CONTEXT, ZarrV2DataTypeDefinition
from zarr_metadata.v3.codec.bytes import BYTES_CODEC
from zarr_metadata.v3.data_type.raw import RAW_BYTES_DATA_TYPE
-from zarr_metadata.v3.definition import CORE, CodecDefinition, DataTypeDefinition
+from zarr_metadata.v3.definition import CORE_CONTEXT, CodecDefinition, DataTypeDefinition
if TYPE_CHECKING:
from collections.abc import Iterator, Mapping
@@ -48,13 +48,16 @@ def test_error_a_rule_of_a_name_that_encodes_its_configuration_is_a_wrong_value_
) -> None:
"""A name such as `r16` or ` None:
"""A codec that writes no configuration has an empty one the document does not hold. A member its rules find missing is the configuration itself, missing, and a problem with the configuration as a whole is the codec's."""
- context = CORE.update(
- codecs={**CORE.codecs, "bytes": dataclasses.replace(BYTES_CODEC, rules=_wants_an_endian)}
+ context = CORE_CONTEXT.update(
+ codecs={
+ **CORE_CONTEXT.codecs,
+ "bytes": dataclasses.replace(BYTES_CODEC, rules=_wants_an_endian),
+ }
)
_, problems = resolve(codec, CodecDefinition, context, ("codecs", 0))
assert [(p.loc, p.kind) for p in problems] == [
diff --git a/packages/zarr-metadata/tests/v3/test_scope.py b/packages/zarr-metadata/tests/v3/test_scope.py
index a99f1ac2df..97b382d3b0 100644
--- a/packages/zarr-metadata/tests/v3/test_scope.py
+++ b/packages/zarr-metadata/tests/v3/test_scope.py
@@ -29,8 +29,8 @@
from zarr_metadata.v3.data_type.bytes import BYTES_DATA_TYPE
from zarr_metadata.v3.data_type.raw import RAW_BYTES_DATA_TYPE
from zarr_metadata.v3.definition import (
- CORE,
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
+ CORE_CONTEXT,
ArrayExtraFieldDefinition,
ChunkGridDefinition,
ChunkKeyEncodingDefinition,
@@ -73,8 +73,8 @@ def _read(data: object, kind: type[Definition[Any]], context: ZarrV3Context) ->
ZarrV3Context(codecs=[BYTES_CODEC, GZIP_CODEC]),
True,
),
- (CORE, pickle.loads(pickle.dumps(CORE)), True),
- (CORE, CORE_AND_EXTENSIONS, False),
+ (CORE_CONTEXT, pickle.loads(pickle.dumps(CORE_CONTEXT)), True),
+ (CORE_CONTEXT, CORE_AND_EXTENSIONS_CONTEXT, False),
(ZarrV3Context(codecs=[GZIP_CODEC]), ZarrV3Context(codecs=[GZIP_CODEC, ZSTD_CODEC]), False),
(ZarrV3Context(), ZarrV3Context(), True),
],
@@ -91,7 +91,10 @@ def test_a_scope_is_equal_to_another_by_the_definitions_it_files(
def test_a_scope_is_a_set_member() -> None:
"""A scope is hashable, so it can be a dict key or a set member. It was not hashable when its tables were mapping proxies."""
- assert len({CORE, CORE_AND_EXTENSIONS, pickle.loads(pickle.dumps(CORE))}) == 2
+ assert (
+ len({CORE_CONTEXT, CORE_AND_EXTENSIONS_CONTEXT, pickle.loads(pickle.dumps(CORE_CONTEXT))})
+ == 2
+ )
def test_error_a_scope_conflict_says_each_disagreement() -> None:
@@ -114,14 +117,14 @@ def test_error_a_scope_conflict_says_each_disagreement() -> None:
@pytest.mark.parametrize(
("field", "claims"),
[
- (_read("gzip", CodecDefinition, CORE), {(CodecDefinition, "gzip"): GZIP_CODEC}),
- (_read("zstd", CodecDefinition, CORE), {(CodecDefinition, "zstd"): None}),
+ (_read("gzip", CodecDefinition, CORE_CONTEXT), {(CodecDefinition, "gzip"): GZIP_CODEC}),
+ (_read("zstd", CodecDefinition, CORE_CONTEXT), {(CodecDefinition, "zstd"): None}),
(
- _read("r16", DataTypeDefinition, CORE),
+ _read("r16", DataTypeDefinition, CORE_CONTEXT),
{(DataTypeDefinition, "r*"): RAW_BYTES_DATA_TYPE},
),
(
- _read(SHARD, CodecDefinition, CORE),
+ _read(SHARD, CodecDefinition, CORE_CONTEXT),
{
(CodecDefinition, "sharding_indexed"): SHARDING_INDEXED_CODEC,
(CodecDefinition, "bytes"): BYTES_CODEC,
@@ -129,7 +132,7 @@ def test_error_a_scope_conflict_says_each_disagreement() -> None:
},
),
(
- _read({"name": "gzip", "configuration": {"level": 12}}, CodecDefinition, CORE),
+ _read({"name": "gzip", "configuration": {"level": 12}}, CodecDefinition, CORE_CONTEXT),
{(CodecDefinition, "gzip"): GZIP_CODEC},
),
(RefusedField(json=3, name=None, read_as=CodecDefinition), {}),
@@ -146,7 +149,7 @@ def test_claims_of_says_what_a_reading_claimed_of_each_name(
def test_error_claims_of_refuses_one_name_read_two_ways() -> None:
"""Fields read in two scopes that give one name two different definitions have no single set of claims. `claims_of` raises a `ScopeConflictError` that names the key."""
fields = [
- *fields_of(_read("gzip", CodecDefinition, CORE), ("codecs", 0)),
+ *fields_of(_read("gzip", CodecDefinition, CORE_CONTEXT), ("codecs", 0)),
*fields_of(_read("gzip", CodecDefinition, ZarrV3Context(codecs=[MY_GZIP])), ("codecs", 1)),
]
with pytest.raises(ScopeConflictError) as raised:
@@ -188,61 +191,69 @@ def test_error_claims_of_refuses_one_name_read_two_ways() -> None:
@pytest.mark.parametrize(
("field", "other", "expected"),
[
- (_read(GZIP_FIELD, CodecDefinition, CORE), _read(GZIP_FIELD, CodecDefinition, CORE), True),
(
- _read({"name": "crc32c"}, CodecDefinition, CORE),
- _read("crc32c", CodecDefinition, CORE),
+ _read(GZIP_FIELD, CodecDefinition, CORE_CONTEXT),
+ _read(GZIP_FIELD, CodecDefinition, CORE_CONTEXT),
True,
),
(
- _read(ZSTD_FIELD, CodecDefinition, CORE_AND_EXTENSIONS),
- _read(ZSTD_FIELD, CodecDefinition, CORE),
+ _read({"name": "crc32c"}, CodecDefinition, CORE_CONTEXT),
+ _read("crc32c", CodecDefinition, CORE_CONTEXT),
True,
),
(
- _read(ZSTD_FIELD, CodecDefinition, CORE),
- _read(ZSTD_FIELD, CodecDefinition, CORE_AND_EXTENSIONS),
+ _read(ZSTD_FIELD, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT),
+ _read(ZSTD_FIELD, CodecDefinition, CORE_CONTEXT),
+ True,
+ ),
+ (
+ _read(ZSTD_FIELD, CodecDefinition, CORE_CONTEXT),
+ _read(ZSTD_FIELD, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT),
False,
),
(
- _read(GZIP_FIELD, CodecDefinition, CORE),
+ _read(GZIP_FIELD, CodecDefinition, CORE_CONTEXT),
_read(GZIP_FIELD, CodecDefinition, ZarrV3Context(codecs=[MY_GZIP])),
False,
),
(
- _read(NESTED_ZSTD, CodecDefinition, CORE_AND_EXTENSIONS),
- _read(NESTED_ZSTD, CodecDefinition, CORE),
+ _read(NESTED_ZSTD, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT),
+ _read(NESTED_ZSTD, CodecDefinition, CORE_CONTEXT),
True,
),
(
- _read(NESTED_ZSTD, CodecDefinition, CORE),
- _read(NESTED_ZSTD, CodecDefinition, CORE_AND_EXTENSIONS),
+ _read(NESTED_ZSTD, CodecDefinition, CORE_CONTEXT),
+ _read(NESTED_ZSTD, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT),
False,
),
(
- _read({"name": "gzip", "configuration": {"level": 12}}, CodecDefinition, CORE),
- _read("gzip", CodecDefinition, CORE),
+ _read({"name": "gzip", "configuration": {"level": 12}}, CodecDefinition, CORE_CONTEXT),
+ _read("gzip", CodecDefinition, CORE_CONTEXT),
False,
),
- (_read("zstd", CodecDefinition, CORE), _read("zstd", CodecDefinition, CORE), True),
(
- _read({"name": "zstd", "configuration": {"level": 1}}, CodecDefinition, CORE),
- _read({"name": "zstd", "configuration": {"level": 2}}, CodecDefinition, CORE),
+ _read("zstd", CodecDefinition, CORE_CONTEXT),
+ _read("zstd", CodecDefinition, CORE_CONTEXT),
+ True,
+ ),
+ (
+ _read({"name": "zstd", "configuration": {"level": 1}}, CodecDefinition, CORE_CONTEXT),
+ _read({"name": "zstd", "configuration": {"level": 2}}, CodecDefinition, CORE_CONTEXT),
False,
),
(
- _read("crc32c", CodecDefinition, CORE),
+ _read("crc32c", CodecDefinition, CORE_CONTEXT),
_read({"name": "crc32c"}, CodecDefinition, ZarrV3Context()),
True,
),
(
- _read(BLOSC_LEVEL_ONE, CodecDefinition, CORE),
+ _read(BLOSC_LEVEL_ONE, CodecDefinition, CORE_CONTEXT),
_read(BLOSC_LEVEL_TRUE, CodecDefinition, ZarrV3Context()),
False,
),
(
- _read(SHARD_HOLDING_REFUSED, CodecDefinition, CORE),
- _read(SHARD_HOLDING_REFUSED, CodecDefinition, CORE),
+ _read(SHARD_HOLDING_REFUSED, CodecDefinition, CORE_CONTEXT),
+ _read(SHARD_HOLDING_REFUSED, CodecDefinition, CORE_CONTEXT),
True,
),
],
@@ -279,7 +290,7 @@ def test_refines_orders_readings_by_information(
BLOSC_LEVEL_ONE,
SHARD_HOLDING_REFUSED,
]
-CONTEXTS = [ZarrV3Context(), CORE, CORE_AND_EXTENSIONS]
+CONTEXTS = [ZarrV3Context(), CORE_CONTEXT, CORE_AND_EXTENSIONS_CONTEXT]
READINGS = [
_read(document, CodecDefinition, context) for document in DOCUMENTS for context in CONTEXTS
]
@@ -301,15 +312,15 @@ def test_refines_is_a_partial_order_whose_bottom_is_equality(
@pytest.mark.parametrize(
("context", "claims", "gains", "conflicts"),
[
- (CORE, {(CodecDefinition, "gzip"): GZIP_CODEC}, (), ()),
+ (CORE_CONTEXT, {(CodecDefinition, "gzip"): GZIP_CODEC}, (), ()),
(
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
{(CodecDefinition, "zstd"): None},
((CodecDefinition, "zstd"),),
(),
),
(
- CORE,
+ CORE_CONTEXT,
{(CodecDefinition, "zstd"): ZSTD_CODEC},
(),
(Conflict((CodecDefinition, "zstd"), ZSTD_CODEC, None),),
@@ -320,8 +331,8 @@ def test_refines_is_a_partial_order_whose_bottom_is_equality(
(),
(Conflict((CodecDefinition, "gzip"), GZIP_CODEC, MY_GZIP),),
),
- (CORE, {(CodecDefinition, "acme.x"): None}, (), ()),
- (CORE, {(DataTypeDefinition, "r*"): RAW_BYTES_DATA_TYPE}, (), ()),
+ (CORE_CONTEXT, {(CodecDefinition, "acme.x"): None}, (), ()),
+ (CORE_CONTEXT, {(DataTypeDefinition, "r*"): RAW_BYTES_DATA_TYPE}, (), ()),
],
ids=["agrees", "gain", "loss", "conflict", "unclaimed-both", "raw-bits"],
)
@@ -341,13 +352,13 @@ def test_disagreements_says_where_a_scope_reads_claims_otherwise(
@pytest.mark.parametrize(
("contexts", "united"),
[
- ((CORE, CORE_AND_EXTENSIONS), CORE_AND_EXTENSIONS),
+ ((CORE_CONTEXT, CORE_AND_EXTENSIONS_CONTEXT), CORE_AND_EXTENSIONS_CONTEXT),
(
(ZarrV3Context(codecs=[GZIP_CODEC]), ZarrV3Context(codecs=[ZSTD_CODEC])),
ZarrV3Context(codecs=[GZIP_CODEC, ZSTD_CODEC]),
),
- ((CORE,), CORE),
- ((CORE, CORE), CORE),
+ ((CORE_CONTEXT,), CORE_CONTEXT),
+ ((CORE_CONTEXT, CORE_CONTEXT), CORE_CONTEXT),
(
(ZarrV3Context(codecs=[BYTES_CODEC]), ZarrV3Context(data_types=[BYTES_DATA_TYPE])),
ZarrV3Context(data_types=[BYTES_DATA_TYPE], codecs=[BYTES_CODEC]),
@@ -366,7 +377,7 @@ def test_union_is_the_least_scope_above_each(
def test_error_union_refuses_one_name_filed_two_ways() -> None:
"""Scopes that file different definitions under one name have no union. `union` raises a `ScopeConflictError` that names the key and both definitions."""
with pytest.raises(ScopeConflictError) as raised:
- CORE.union(ZarrV3Context(codecs=[MY_GZIP]))
+ CORE_CONTEXT.union(ZarrV3Context(codecs=[MY_GZIP]))
(conflict,) = raised.value.conflicts
assert (conflict.key, conflict.claimed, conflict.found) == (
(CodecDefinition, "gzip"),
@@ -398,21 +409,25 @@ def test_a_gain_is_judged_by_what_the_definition_reads_not_by_spelling() -> None
resolve,
)
from zarr_metadata.v3.definition import (
- CORE,
+ CORE_CONTEXT,
ChunkKeyEncodingDefinition,
)
nothing = ZarrV3Context()
spelled_out, _ = resolve(
- {"name": "default", "configuration": {"separator": "/"}}, ChunkKeyEncodingDefinition, CORE
+ {"name": "default", "configuration": {"separator": "/"}},
+ ChunkKeyEncodingDefinition,
+ CORE_CONTEXT,
)
- bare, _ = resolve({"name": "default"}, ChunkKeyEncodingDefinition, CORE)
+ bare, _ = resolve({"name": "default"}, ChunkKeyEncodingDefinition, CORE_CONTEXT)
unclaimed, _ = resolve({"name": "default"}, ChunkKeyEncodingDefinition, nothing)
assert spelled_out == bare
assert refines(bare, unclaimed)
assert refines(spelled_out, unclaimed)
other, _ = resolve(
- {"name": "default", "configuration": {"separator": "."}}, ChunkKeyEncodingDefinition, CORE
+ {"name": "default", "configuration": {"separator": "."}},
+ ChunkKeyEncodingDefinition,
+ CORE_CONTEXT,
)
assert not refines(other, unclaimed)
shard = {
@@ -424,7 +439,7 @@ def test_a_gain_is_judged_by_what_the_definition_reads_not_by_spelling() -> None
"index_location": "end",
},
}
- read, _ = resolve(shard, CodecDefinition, CORE)
+ read, _ = resolve(shard, CodecDefinition, CORE_CONTEXT)
unread, _ = resolve({**shard, "must_understand": True}, CodecDefinition, nothing)
assert refines(read, unread)
@@ -440,7 +455,7 @@ def test_a_scope_conflict_error_pickles_and_copies_with_its_conflicts() -> None:
import copy
with pytest.raises(ScopeConflictError) as raised:
- CORE.union(ZarrV3Context(codecs=[OTHER_GZIP]))
+ CORE_CONTEXT.union(ZarrV3Context(codecs=[OTHER_GZIP]))
error = raised.value
error.add_note("seen in a union")
for again in (pickle.loads(pickle.dumps(error)), copy.copy(error), copy.deepcopy(error)):
@@ -457,16 +472,18 @@ def test_a_conflict_names_the_field_as_the_document_writes_it_and_tells_the_defi
import dataclasses
from zarr_metadata.model import ZarrV3ArrayMetadata
- from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS
+ from zarr_metadata.v3.definition import CORE_AND_EXTENSIONS_CONTEXT
document = dict(
ZarrV3ArrayMetadata.create_default(shape=(2,), data_type="r16", fill_value=[0, 0]).to_json()
)
- model = ZarrV3ArrayMetadata(document, CORE_AND_EXTENSIONS)
+ model = ZarrV3ArrayMetadata(document, CORE_AND_EXTENSIONS_CONTEXT)
other = dataclasses.replace(RAW_BYTES_DATA_TYPE, configuration=EmptyConfiguration)
with pytest.raises(ScopeConflictError) as raised:
model.refined_in(
- CORE_AND_EXTENSIONS.update(data_types={**CORE_AND_EXTENSIONS.data_types, "r*": other})
+ CORE_AND_EXTENSIONS_CONTEXT.update(
+ data_types={**CORE_AND_EXTENSIONS_CONTEXT.data_types, "r*": other}
+ )
)
message = str(raised.value)
assert message.startswith("data type 'r16'")
@@ -489,11 +506,11 @@ def test_each_slot_is_a_read_only_view_by_name() -> None:
"array_extra_fields": ArrayExtraFieldDefinition,
"group_extra_fields": GroupExtraFieldDefinition,
}
- assert CORE.codecs["gzip"] is GZIP_CODEC
- assert CORE.data_types["r*"] is RAW_BYTES_DATA_TYPE
- assert dict(CORE.storage_transformers) == {}
+ assert CORE_CONTEXT.codecs["gzip"] is GZIP_CODEC
+ assert CORE_CONTEXT.data_types["r*"] is RAW_BYTES_DATA_TYPE
+ assert dict(CORE_CONTEXT.storage_transformers) == {}
with pytest.raises(TypeError):
- CORE.codecs["gzip"] = MY_GZIP # pyright: ignore[reportIndexIssue]
+ CORE_CONTEXT.codecs["gzip"] = MY_GZIP # pyright: ignore[reportIndexIssue]
def test_error_a_slot_refuses_a_definition_of_another_kind() -> None:
diff --git a/packages/zarr-metadata/tests/v3/test_sharding.py b/packages/zarr-metadata/tests/v3/test_sharding.py
index c74b173c50..911f20b55f 100644
--- a/packages/zarr-metadata/tests/v3/test_sharding.py
+++ b/packages/zarr-metadata/tests/v3/test_sharding.py
@@ -25,7 +25,7 @@
read_pipeline,
)
from zarr_metadata.v3.definition import (
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
Chunk,
CodecDefinition,
DataTypeDefinition,
@@ -42,7 +42,7 @@
def _dt(name: JSONValue) -> ResolvedField[DataTypeDefinition[Any]]:
- return resolve(name, DataTypeDefinition, CORE_AND_EXTENSIONS)[0]
+ return resolve(name, DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)[0]
FLOAT32 = _dt("float32")
@@ -81,7 +81,7 @@ def _read(
) -> tuple[tuple[Stage, ...], tuple[ValidationProblem, ...]]:
"""The stages, and every problem: first each codec's own problems at its location, then the pipeline's."""
read = [
- resolve(codec, CodecDefinition, CORE_AND_EXTENSIONS, (index,))
+ resolve(codec, CodecDefinition, CORE_AND_EXTENSIONS_CONTEXT, (index,))
for index, codec in enumerate(codecs)
]
stages, found = read_pipeline([resolved for resolved, _ in read], chunk)
diff --git a/packages/zarr-metadata/tests/v3/test_storage.py b/packages/zarr-metadata/tests/v3/test_storage.py
index fadcf36049..58d367119e 100644
--- a/packages/zarr-metadata/tests/v3/test_storage.py
+++ b/packages/zarr-metadata/tests/v3/test_storage.py
@@ -18,7 +18,7 @@
resolve,
)
from zarr_metadata.v3.definition import (
- CORE_AND_EXTENSIONS,
+ CORE_AND_EXTENSIONS_CONTEXT,
DataTypeDefinition,
StorageClass,
storage_of,
@@ -76,7 +76,7 @@ def _struct(*field_types: JSONValue) -> JSONValue:
def test_every_data_type_says_how_its_values_are_stored(
data_type: JSONValue, storage: StorageClass | None
) -> None:
- resolved, _ = resolve(data_type, DataTypeDefinition, CORE_AND_EXTENSIONS)
+ resolved, _ = resolve(data_type, DataTypeDefinition, CORE_AND_EXTENSIONS_CONTEXT)
assert storage_of(resolved) == storage
@@ -85,7 +85,9 @@ def test_a_data_type_that_says_nothing_of_its_storage_leaves_it_unknown() -> Non
resolved, _ = resolve(
"acme.quiet",
DataTypeDefinition,
- CORE_AND_EXTENSIONS.update(data_types=[*CORE_AND_EXTENSIONS.data_types.values(), quiet]),
+ CORE_AND_EXTENSIONS_CONTEXT.update(
+ data_types=[*CORE_AND_EXTENSIONS_CONTEXT.data_types.values(), quiet]
+ ),
)
assert storage_of(resolved) is None
@@ -99,7 +101,9 @@ def test_error_a_storage_that_gives_something_else() -> None:
resolved, _ = resolve(
"acme.lying",
DataTypeDefinition,
- CORE_AND_EXTENSIONS.update(data_types=[*CORE_AND_EXTENSIONS.data_types.values(), lying]),
+ CORE_AND_EXTENSIONS_CONTEXT.update(
+ data_types=[*CORE_AND_EXTENSIONS_CONTEXT.data_types.values(), lying]
+ ),
)
with pytest.raises(TypeError, match="'acme.lying': its storage gives one of"):
storage_of(resolved)
@@ -115,7 +119,9 @@ def storage(configuration: EmptyConfiguration, nested: Nested) -> StorageClass:
resolved, _ = resolve(
"acme.raising",
DataTypeDefinition,
- CORE_AND_EXTENSIONS.update(data_types=[*CORE_AND_EXTENSIONS.data_types.values(), raising]),
+ CORE_AND_EXTENSIONS_CONTEXT.update(
+ data_types=[*CORE_AND_EXTENSIONS_CONTEXT.data_types.values(), raising]
+ ),
)
with pytest.raises(KeyError) as raised:
storage_of(resolved)
diff --git a/packages/zarr-metadata/tests/v3/test_zarr_python_scope.py b/packages/zarr-metadata/tests/v3/test_zarr_python_scope.py
new file mode 100644
index 0000000000..99b9274dad
--- /dev/null
+++ b/packages/zarr-metadata/tests/v3/test_zarr_python_scope.py
@@ -0,0 +1,226 @@
+"""The `ZARR_PYTHON_CONTEXT` scope, and the definitions it adds for what zarr-python writes.
+
+`fixed_length_utf32`, `vlen-utf8` and `vlen-bytes` are registered in
+zarr-extensions, so they are in `CORE_AND_EXTENSIONS_CONTEXT`.
+`null_terminated_bytes`, `variable_length_bytes` and `raw_bytes` are not,
+so only `ZARR_PYTHON_CONTEXT` reads them.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+from zarr_metadata.definition import (
+ AcceptedField,
+ RefusedField,
+ UnclaimedField,
+ canonical_fill_value,
+ fill_value_problems,
+ resolve,
+)
+from zarr_metadata.model import read_array_metadata_v3
+from zarr_metadata.v3.codec.vlen_bytes import VLEN_BYTES_CODEC
+from zarr_metadata.v3.codec.vlen_utf8 import VLEN_UTF8_CODEC
+from zarr_metadata.v3.data_type.fixed_length_utf32 import FIXED_LENGTH_UTF32_DATA_TYPE
+from zarr_metadata.v3.data_type.null_terminated_bytes import NULL_TERMINATED_BYTES_DATA_TYPE
+from zarr_metadata.v3.data_type.variable_length_bytes import VARIABLE_LENGTH_BYTES_DATA_TYPE
+from zarr_metadata.v3.data_type.zarr_python_raw_bytes import ZARR_PYTHON_RAW_BYTES_DATA_TYPE
+from zarr_metadata.v3.definition import (
+ CORE_AND_EXTENSIONS_CONTEXT,
+ ZARR_PYTHON_CONTEXT,
+ DataTypeDefinition,
+ ZarrV3Context,
+)
+
+
+def _document(data_type: object, fill_value: object, codecs: list[object]) -> dict[str, Any]:
+ return {
+ "zarr_format": 3,
+ "node_type": "array",
+ "shape": [2],
+ "data_type": data_type,
+ "chunk_grid": {"name": "regular", "configuration": {"chunk_shape": [2]}},
+ "chunk_key_encoding": {"name": "default"},
+ "fill_value": fill_value,
+ "codecs": codecs,
+ "attributes": {},
+ }
+
+
+_LITTLE = {"name": "bytes", "configuration": {"endian": "little"}}
+_UTF32 = {"name": "fixed_length_utf32", "configuration": {"length_bytes": 12}}
+_NULL_TERMINATED = {"name": "null_terminated_bytes", "configuration": {"length_bytes": 4}}
+_RAW = {"name": "raw_bytes", "configuration": {"length_bytes": 4}}
+
+
+# The data type, fill value and serializer zarr-python 3.4.2 writes for ` None:
+ # In `ZARR_PYTHON_CONTEXT` every field is read by a definition and has no problem.
+ reading = read_array_metadata_v3(document, context=ZARR_PYTHON_CONTEXT)
+ assert reading.problems == ()
+ assert all(isinstance(field, AcceptedField) for _, field in reading.fields())
+ # `CORE_AND_EXTENSIONS_CONTEXT` reads the registered extensions, and leaves
+ # zarr-python's own data types unclaimed.
+ data_type = read_array_metadata_v3(document, context=CORE_AND_EXTENSIONS_CONTEXT).data_type
+ assert isinstance(data_type, AcceptedField) is in_core_and_extensions
+ assert isinstance(data_type, UnclaimedField) is not in_core_and_extensions
+
+
+def test_core_and_extensions_reads_the_registered_extensions() -> None:
+ """zarr-extensions registers `fixed_length_utf32`, `vlen-utf8` and `vlen-bytes`."""
+ assert (
+ CORE_AND_EXTENSIONS_CONTEXT.data_types[FIXED_LENGTH_UTF32_DATA_TYPE.name]
+ is FIXED_LENGTH_UTF32_DATA_TYPE
+ )
+ assert CORE_AND_EXTENSIONS_CONTEXT.codecs[VLEN_UTF8_CODEC.name] is VLEN_UTF8_CODEC
+ assert CORE_AND_EXTENSIONS_CONTEXT.codecs[VLEN_BYTES_CODEC.name] is VLEN_BYTES_CODEC
+
+
+def test_zarr_python_is_core_and_extensions_with_zarr_python_s_own_data_types() -> None:
+ own = CORE_AND_EXTENSIONS_CONTEXT.update(
+ data_types=[
+ *CORE_AND_EXTENSIONS_CONTEXT.data_types.values(),
+ NULL_TERMINATED_BYTES_DATA_TYPE,
+ VARIABLE_LENGTH_BYTES_DATA_TYPE,
+ ZARR_PYTHON_RAW_BYTES_DATA_TYPE,
+ ]
+ )
+ assert own == ZARR_PYTHON_CONTEXT
+
+
+@pytest.mark.parametrize(
+ ("data_type", "value", "canonical"),
+ [
+ (_UTF32, "", ""),
+ (_UTF32, "abc", "abc"),
+ # Trailing U+0000 code points are padding, so they are the same value.
+ (_UTF32, "a\x00\x00", "a"),
+ # A value is padded with null bytes, so trailing ones are the same value.
+ (_NULL_TERMINATED, "QQAA", "QQ=="),
+ (_NULL_TERMINATED, "QR==", "QQ=="),
+ (_NULL_TERMINATED, "", ""),
+ # The padding does not count against the size: b"A\x00\x00" fits in 2 bytes.
+ ({"name": "null_terminated_bytes", "configuration": {"length_bytes": 2}}, "QQAA", "QQ=="),
+ ("variable_length_bytes", "QR==", "QQ=="),
+ # zarr-python pads a shorter value with null bytes, and writes it padded.
+ (_RAW, "QQ==", "QQAAAA=="),
+ (_RAW, "QQAAAA==", "QQAAAA=="),
+ ("variable_length_bytes", "QQAA", "QQAA"),
+ ],
+)
+def test_the_canonical_fill_value(data_type: object, value: str, canonical: str) -> None:
+ resolved, problems = resolve(data_type, DataTypeDefinition, ZARR_PYTHON_CONTEXT)
+ assert problems == ()
+ assert fill_value_problems(resolved, value) == ()
+ assert canonical_fill_value(resolved, value) == canonical
+
+
+@pytest.mark.parametrize(
+ "data_type",
+ [
+ {"name": "fixed_length_utf32", "configuration": {"length_bytes": 0}},
+ {"name": "fixed_length_utf32", "configuration": {"length_bytes": 6}},
+ {"name": "null_terminated_bytes", "configuration": {"length_bytes": 0}},
+ {"name": "raw_bytes", "configuration": {"length_bytes": 0}},
+ ],
+ ids=["utf32-zero", "utf32-not-a-multiple-of-4", "null-terminated-zero", "raw-zero"],
+)
+def test_error_a_size_that_holds_no_whole_value(data_type: object) -> None:
+ resolved, problems = resolve(data_type, DataTypeDefinition, ZARR_PYTHON_CONTEXT)
+ assert isinstance(resolved, RefusedField)
+ assert [(p.loc, p.kind) for p in problems] == [
+ (("configuration", "length_bytes"), "invalid_value")
+ ]
+
+
+@pytest.mark.parametrize(
+ ("data_type", "value"),
+ [
+ (_UTF32, "abcd"),
+ # The spec counts trailing U+0000 too, although the value is "abc".
+ (_UTF32, "abc\x00"),
+ (_NULL_TERMINATED, "YWJjZGU="),
+ (_RAW, "YWJjZGU="),
+ ],
+ ids=[
+ "utf32-four-code-points",
+ "utf32-padding-past-capacity",
+ "null-terminated-five-bytes",
+ "raw-five-bytes",
+ ],
+)
+def test_error_a_fill_value_longer_than_the_type(data_type: object, value: str) -> None:
+ resolved, _ = resolve(data_type, DataTypeDefinition, ZARR_PYTHON_CONTEXT)
+ assert [(p.loc, p.kind) for p in fill_value_problems(resolved, value)] == [
+ ((), "invalid_value")
+ ]
+
+
+@pytest.mark.parametrize("data_type", [_NULL_TERMINATED, "variable_length_bytes", _RAW])
+def test_error_a_fill_value_that_is_not_base64(data_type: object) -> None:
+ resolved, _ = resolve(data_type, DataTypeDefinition, ZARR_PYTHON_CONTEXT)
+ assert [(p.loc, p.kind) for p in fill_value_problems(resolved, "YWI")] == [
+ ((), "invalid_value")
+ ]
+
+
+@pytest.mark.parametrize(
+ ("data_type", "codec"),
+ [
+ (_UTF32, "vlen-utf8"),
+ (_UTF32, "vlen-bytes"),
+ ("bytes", "vlen-utf8"),
+ ("variable_length_bytes", "vlen-utf8"),
+ ("string", "vlen-bytes"),
+ ],
+)
+def test_error_a_variable_length_codec_for_a_data_type_it_does_not_encode(
+ data_type: object, codec: str
+) -> None:
+ document = _document(data_type, "", [codec])
+ reading = read_array_metadata_v3(document, context=ZARR_PYTHON_CONTEXT)
+ assert [(p.loc, p.kind) for p in reading.problems] == [(("codecs", 0), "invalid_value")]
+
+
+def test_error_a_bytes_codec_without_an_endian_for_fixed_length_utf32() -> None:
+ reading = read_array_metadata_v3(_document(_UTF32, "", ["bytes"]), context=ZARR_PYTHON_CONTEXT)
+ assert [(p.loc, p.kind) for p in reading.problems] == [
+ (("codecs", 0, "configuration"), "missing_key")
+ ]
+
+
+def test_an_empty_scope_reads_none_of_them() -> None:
+ for name in (
+ "fixed_length_utf32",
+ "null_terminated_bytes",
+ "variable_length_bytes",
+ "raw_bytes",
+ ):
+ assert ZarrV3Context().claimant(DataTypeDefinition, name) is None
diff --git a/packages/zarr-metadata/uv.lock b/packages/zarr-metadata/uv.lock
index 9c21f5a4c9..1679e33309 100644
--- a/packages/zarr-metadata/uv.lock
+++ b/packages/zarr-metadata/uv.lock
@@ -1233,6 +1233,11 @@ dependencies = [
{ name = "typing-extensions" },
]
+[package.optional-dependencies]
+testing = [
+ { name = "hypothesis" },
+]
+
[package.dev-dependencies]
docs = [
{ name = "griffe-inherited-docstrings" },
@@ -1254,8 +1259,10 @@ test = [
[package.metadata]
requires-dist = [
{ name = "annotated-types", specifier = ">=0.6" },
+ { name = "hypothesis", marker = "extra == 'testing'", specifier = ">=6.85" },
{ name = "typing-extensions", specifier = ">=4.16" },
]
+provides-extras = ["testing"]
[package.metadata.requires-dev]
docs = [
diff --git a/pyproject.toml b/pyproject.toml
index 2554180f38..c95ecb6ee8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -84,7 +84,7 @@ remote = [
gpu = [
"cupy-cuda12x; sys_platform != 'darwin'",
]
-cast-value-rs = ["cast-value-rs>=0.4.2"]
+cast-value-rs = ["cast-value-rs>=0.5.1"]
cli = ["typer"]
optional = ["universal-pathlib"]
@@ -121,7 +121,6 @@ test = [
"pytest-xdist==3.8.0",
"pytest-benchmark==5.3.0",
"pytest-codspeed==5.0.3",
- "tomlkit==0.15.2",
"uv==0.13.5",
]
remote-tests = [
@@ -463,6 +462,7 @@ markers = [
"gpu: mark a test as requiring CuPy and GPU",
"s3: mark a test as requiring a (mock) S3 backend via moto",
"slow_hypothesis: slow hypothesis tests",
+ "examples: runs a script in examples/; skipped unless --run-examples is passed",
]
[tool.repo-review]
diff --git a/src/zarr/codecs/cast_value.py b/src/zarr/codecs/cast_value.py
index 629b4e7a27..3d465070b8 100644
--- a/src/zarr/codecs/cast_value.py
+++ b/src/zarr/codecs/cast_value.py
@@ -5,7 +5,7 @@
rounding, out-of-range handling, and explicit scalar mappings.
Requires the optional ``cast-value-rs`` package for the actual casting
-logic. Install it with: ``pip install 'cast-value-rs>=0.4.2'``.
+logic. Install it with: ``pip install 'cast-value-rs>=0.5.1'``.
"""
from __future__ import annotations
@@ -125,10 +125,12 @@ def parse_scalar_map(obj: ScalarMapJSON | ScalarMap) -> ScalarMap:
# Backend: cast-value-rs
# ---------------------------------------------------------------------------
-# Versions below this silently transpose input arrays that are not row-major -
+# Versions below 0.4.2 silently transpose input arrays that are not row-major -
# the layout `transpose` hands to the next codec - writing corrupted data with
-# no error. Keep in sync with the `cast-value-rs` extra in pyproject.toml.
-CAST_VALUE_RS_MIN_VERSION: Final = "0.4.2"
+# no error. Versions below 0.5.0 give wrong results for some casts at integer
+# and float range limits. 0.5.0 was never published to PyPI, so the floor is
+# 0.5.1. Keep in sync with the `cast-value-rs` extra in pyproject.toml.
+CAST_VALUE_RS_MIN_VERSION: Final = "0.5.1"
_INSTALL_HINT: Final = f"Install it with: pip install 'cast-value-rs>={CAST_VALUE_RS_MIN_VERSION}'"
@@ -144,8 +146,8 @@ def _check_backend_version() -> str | None:
if Version(installed) < Version(CAST_VALUE_RS_MIN_VERSION):
return (
f"The cast_value codec requires cast-value-rs >= {CAST_VALUE_RS_MIN_VERSION}, "
- f"but version {installed} is installed. Earlier versions silently corrupt data "
- f"when the input array is not row-major. {_INSTALL_HINT}"
+ f"but version {installed} is installed. Earlier versions can write wrong "
+ f"values without raising an error. {_INSTALL_HINT}"
)
return None
diff --git a/src/zarr/codecs/numcodecs/_codecs.py b/src/zarr/codecs/numcodecs/_codecs.py
index c980743166..5cd1611447 100644
--- a/src/zarr/codecs/numcodecs/_codecs.py
+++ b/src/zarr/codecs/numcodecs/_codecs.py
@@ -164,7 +164,12 @@ def _encode_sync(self, chunk_data: NDBuffer, chunk_spec: ArraySpec) -> NDBuffer:
# would be encoded in transposed element order (gh-3558)
chunk_ndarray = np.ascontiguousarray(chunk_data.as_ndarray_like())
out = self._codec.encode(chunk_ndarray)
- return chunk_spec.prototype.nd_buffer.from_ndarray_like(out)
+ # numcodecs filters return a flattened array; reshape it to the
+ # output shape so downstream codecs receive the chunk shape that
+ # resolve_metadata promised them (gh-4553)
+ return chunk_spec.prototype.nd_buffer.from_ndarray_like(
+ out.reshape(self.resolve_metadata(chunk_spec).shape)
+ )
async def _encode_single(self, chunk_data: NDBuffer, chunk_spec: ArraySpec) -> NDBuffer:
return await asyncio.to_thread(self._encode_sync, chunk_data, chunk_spec)
diff --git a/tests/conftest.py b/tests/conftest.py
index 3e87a724e0..89eb90a1d8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -262,15 +262,29 @@ def pytest_addoption(parser: Any) -> None:
default=False,
help="run slow hypothesis tests",
)
+ parser.addoption(
+ "--run-examples",
+ action="store_true",
+ default=False,
+ help="run the scripts in examples/",
+ )
+
+
+# Tests with one of these markers are skipped unless the matching option is passed.
+_OPT_IN_MARKERS = {
+ "slow_hypothesis": "--run-slow-hypothesis",
+ "examples": "--run-examples",
+}
def pytest_collection_modifyitems(config: Any, items: Any) -> None:
- if config.getoption("--run-slow-hypothesis"):
- return
- skip_slow_hyp = pytest.mark.skip(reason="need --run-slow-hypothesis option to run")
- for item in items:
- if "slow_hypothesis" in item.keywords:
- item.add_marker(skip_slow_hyp)
+ for marker, option in _OPT_IN_MARKERS.items():
+ if config.getoption(option):
+ continue
+ skip = pytest.mark.skip(reason=f"need {option} option to run")
+ for item in items:
+ if marker in item.keywords:
+ item.add_marker(skip)
settings.register_profile(
diff --git a/tests/test_codecs/test_cast_value.py b/tests/test_codecs/test_cast_value.py
index c2e78770d9..d6c703767f 100644
--- a/tests/test_codecs/test_cast_value.py
+++ b/tests/test_codecs/test_cast_value.py
@@ -9,7 +9,7 @@
import zarr
from tests.conftest import Expect, ExpectFail
from zarr.codecs import BytesCodec, TransposeCodec
-from zarr.codecs.cast_value import CastValue
+from zarr.codecs.cast_value import CAST_VALUE_RS_MIN_VERSION, CastValue
from zarr.storage import MemoryStore
try:
@@ -490,15 +490,14 @@ def test_parse_scalar_map(case: Expect[Any, Any]) -> None:
@pytest.mark.parametrize(
"case",
[
- Expect(input="0.4.2", output=None, id="exactly-minimum"),
- Expect(input="0.4.3", output=None, id="newer-patch"),
- Expect(input="0.5.0", output=None, id="newer-minor"),
- Expect(input="1.0.0", output=None, id="newer-major"),
- Expect(input="0.4.2.post1", output=None, id="post-release"),
+ Expect(input=CAST_VALUE_RS_MIN_VERSION, output=None, id="exactly-minimum"),
+ Expect(input=f"{CAST_VALUE_RS_MIN_VERSION}.post1", output=None, id="post-release"),
+ Expect(
+ input=f"{CAST_VALUE_RS_MIN_VERSION}.dev1",
+ output=f"{CAST_VALUE_RS_MIN_VERSION}.dev1",
+ id="pre-release-of-minimum",
+ ),
Expect(input="0.4.0", output="0.4.0", id="known-corrupting"),
- Expect(input="0.4.1", output="0.4.1", id="one-patch-below"),
- Expect(input="0.3.0", output="0.3.0", id="older-minor"),
- Expect(input="0.4.2.dev1", output="0.4.2.dev1", id="pre-release-of-minimum"),
],
ids=lambda c: c.id,
)
diff --git a/tests/test_codecs/test_numcodecs.py b/tests/test_codecs/test_numcodecs.py
index d78b73f34c..bf45017757 100644
--- a/tests/test_codecs/test_numcodecs.py
+++ b/tests/test_codecs/test_numcodecs.py
@@ -11,6 +11,7 @@
from zarr import config, create_array, open_array
from zarr.abc.numcodec import _is_numcodec, _is_numcodec_cls
from zarr.codecs import numcodecs as _numcodecs
+from zarr.codecs.transpose import TransposeCodec
from zarr.errors import UnknownCodecError
from zarr.registry import get_codec_class, get_numcodec
@@ -192,6 +193,45 @@ def test_generic_filter_f_contiguous(
np.testing.assert_array_equal(data, a[:, :])
+def test_generic_filter_flattened_output_reshaped() -> None:
+ # gh-4553: numcodecs filters return a flattened array; `_encode_sync` must
+ # reshape it back to the chunk shape, so downstream codecs receive chunks
+ # with the shape the pipeline promised them
+ data = np.arange(256, dtype="int64").reshape(16, 16)
+
+ a = create_array(
+ {},
+ shape=data.shape,
+ chunks=(16, 16),
+ dtype=data.dtype,
+ fill_value=0,
+ filters=[_numcodecs.Delta(dtype="int64"), TransposeCodec(order=(1, 0))],
+ )
+
+ a[:, :] = data
+ with codec_conf():
+ b = open_array(a.store, mode="r")
+ np.testing.assert_array_equal(data, b[:, :])
+
+
+def test_generic_filter_packedbits_output_shape() -> None:
+ # PackBits changes the size of the chunk, so `_encode_sync` must reshape its
+ # output to the shape from `resolve_metadata`, not the input chunk shape
+ data = np.tril(np.ones((16, 16), dtype=bool))
+
+ a = create_array(
+ {},
+ shape=data.shape,
+ chunks=(16, 16),
+ dtype=data.dtype,
+ fill_value=0,
+ filters=[_numcodecs.PackBits()],
+ )
+
+ a[:, :] = data
+ np.testing.assert_array_equal(data, a[:, :])
+
+
def test_generic_filter_bitround() -> None:
data = np.linspace(0, 1, 256, dtype="float32").reshape((16, 16))
diff --git a/tests/test_examples.py b/tests/test_examples.py
index a6634e8cc6..2421a3d0ca 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -1,87 +1,45 @@
from __future__ import annotations
-import re
import subprocess
-import sys
from pathlib import Path
-from typing import Final
import pytest
-import tomlkit
-from packaging.requirements import Requirement
-examples_dir = "examples"
-script_paths = tuple(Path(examples_dir).rglob("*.py"))
+ZARR_PROJECT_PATH = Path(__file__).parent.parent
+EXAMPLE_PATHS = tuple(sorted((ZARR_PROJECT_PATH / "examples").glob("*/*.py")))
-PEP_723_REGEX: Final = r"(?m)^# /// (?P[a-zA-Z0-9-]+)$\s(?P(^#(| .*)$\s)+)^# ///$"
-# This is the absolute path to the local Zarr installation. Moving this test to a different directory will break it.
-ZARR_PROJECT_PATH = Path(".").absolute()
+def test_example_paths() -> None:
+ """The example scripts are found, so the parametrized test below is not empty."""
+ assert len(EXAMPLE_PATHS) > 0
-def set_dep(script: str, dependency: str) -> str:
+@pytest.mark.examples
+@pytest.mark.parametrize("script_path", EXAMPLE_PATHS, ids=lambda path: path.stem)
+def test_example_runs(script_path: Path, tmp_path: Path) -> None:
"""
- Set a dependency in a PEP-723 script header.
- If the package is already in the list, it will be replaced.
- If the package is not already in the list, it will be added.
+ Each example runs against this checkout, in the environment its PEP 723 header defines.
- Source code modified from
- https://packaging.python.org/en/latest/specifications/inline-script-metadata/#reference-implementation
+ The test copies the script and uses `uv add` to point the copy's zarr requirement at
+ this checkout. `uv add` also resolves the header, so a header that cannot be
+ satisfied fails here.
"""
- match = re.search(PEP_723_REGEX, script)
-
- if match is None:
- raise ValueError(f"PEP-723 header not found in {script}")
-
- content = "".join(
- line[2:] if line.startswith("# ") else line[1:]
- for line in match.group("content").splitlines(keepends=True)
- )
-
- config = tomlkit.parse(content)
- for idx, dep in enumerate(tuple(config["dependencies"])):
- if Requirement(dep).name == Requirement(dependency).name:
- config["dependencies"][idx] = dependency
-
- new_content = "".join(
- f"# {line}" if line.strip() else f"#{line}"
- for line in tomlkit.dumps(config).splitlines(keepends=True)
- )
-
- start, end = match.span("content")
- return script[:start] + new_content + script[end:]
-
-
-def resave_script(source_path: Path, dest_path: Path) -> None:
- """
- Read a script from source_path and save it to dest_path after inserting the absolute path to the
- local Zarr project directory in the PEP-723 header.
- """
- source_text = source_path.read_text()
- dest_text = set_dep(source_text, f"zarr @ file:///{ZARR_PROJECT_PATH}")
- dest_path.write_text(dest_text)
-
-
-def test_script_paths() -> None:
- """
- Test that our test fixture is working properly and collecting script paths.
- """
- assert len(script_paths) > 0
-
-
-@pytest.mark.skipif(
- sys.platform == "win32", reason="This test fails for unknown reasons on Windows in CI."
-)
-@pytest.mark.parametrize("script_path", script_paths)
-def test_scripts_can_run(script_path: Path, tmp_path: Path) -> None:
dest_path = tmp_path / script_path.name
- # We resave the script after inserting the absolute path to the local Zarr project directory,
- # and then test its behavior.
- # This allows the example to be useful to users who don't have Zarr installed, but also testable.
- resave_script(script_path, dest_path)
- result = subprocess.run(
- ["uv", "run", "--refresh", str(dest_path)], capture_output=True, text=True, check=False
+ dest_path.write_text(script_path.read_text())
+
+ add = subprocess.run(
+ ["uv", "add", "--quiet", "--script", str(dest_path), str(ZARR_PROJECT_PATH)],
+ capture_output=True,
+ text=True,
+ check=False,
)
- assert result.returncode == 0, (
- f"Script at {script_path} failed to run. Output: {result.stdout} Error: {result.stderr}"
+ assert add.returncode == 0, f"uv add failed for {script_path}:\n{add.stderr}"
+
+ # `--reinstall-package` rebuilds zarr, so the run sees the current source.
+ run = subprocess.run(
+ ["uv", "run", "--reinstall-package", "zarr", str(dest_path)],
+ capture_output=True,
+ text=True,
+ check=False,
)
+ assert run.returncode == 0, f"{script_path} failed:\n{run.stdout}\n{run.stderr}"
diff --git a/uv.lock b/uv.lock
index f8094ec90e..05d88533ec 100644
--- a/uv.lock
+++ b/uv.lock
@@ -441,63 +441,91 @@ wheels = [
[[package]]
name = "cast-value-rs"
-version = "0.4.2"
+version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/56/a4/634d1917ac49ea69291471e87eee9c1aa45d71d6da9b2cd5e1bfe82729de/cast_value_rs-0.4.2.tar.gz", hash = "sha256:fd621b8dd4f7e93bbffdb119882d85e2731c2f0fb1d30de85f29e8e04c044822", size = 86862, upload-time = "2026-08-13T09:16:59.199Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/67/c7/9db59d415df2d1c9f905c4da951fe023c6259a162584ef68bd81c171d448/cast_value_rs-0.4.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6d8e1c6d7b0fec6b1060f83867d20ebd0cb1ac6fa2954cd82f418a31a96ac504", size = 494354, upload-time = "2026-08-13T09:14:41.627Z" },
- { url = "https://files.pythonhosted.org/packages/8f/53/10ece0b4ba4a0c156674c2db7e5d05ce9b04efe72e191417546e111d645c/cast_value_rs-0.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c953321ff7954e0fe1b25c1baf96f0a9cae0961b8a56ce63dc7b94e2ea7d85c", size = 458463, upload-time = "2026-08-13T09:14:43.371Z" },
- { url = "https://files.pythonhosted.org/packages/91/0b/e50ac2eb271eb6c34402b4abe317c795bc7d650d332d0bf422ae868aa2cd/cast_value_rs-0.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68492eef745691c80754cd027e216dbd5f7a4ce2e07fddfded3ee59bd947afb5", size = 487628, upload-time = "2026-08-13T09:14:44.86Z" },
- { url = "https://files.pythonhosted.org/packages/a9/0f/69befc7905af2e73a2739b26444787cd528331c041f37235f3853bf15f75/cast_value_rs-0.4.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbf145ed6f724fa7f5ddc9715f78e0d450ac141fc05299c83b2e7d627793ddaf", size = 555538, upload-time = "2026-08-13T09:14:46.305Z" },
- { url = "https://files.pythonhosted.org/packages/4a/1b/4105fabec3268139bda3555422808211ec29428e17fa56566c1af7957c04/cast_value_rs-0.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73fb23256dda16e4e9f49bfedebe0e068f00ece85091b526c102618f66144216", size = 650896, upload-time = "2026-08-13T09:14:48.049Z" },
- { url = "https://files.pythonhosted.org/packages/83/2a/452cf37675af02c3ac8bfa01fc962cb744a1392c51bccfb6d1faca423067/cast_value_rs-0.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8184135c78ebcabb904574a1b3bc5704c131140feae1b58d65c888fe08b46029", size = 561036, upload-time = "2026-08-13T09:14:49.431Z" },
- { url = "https://files.pythonhosted.org/packages/d4/87/edb215d17da304a6b36489fc51b8eb785c5b041c2fcc5f200f16adde3133/cast_value_rs-0.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:686ea86aad1ffebd0b6793549043108600593a85032457eb88a3a0513a4100f5", size = 549187, upload-time = "2026-08-13T09:14:50.801Z" },
- { url = "https://files.pythonhosted.org/packages/63/82/0f9963ef0bcb434cd1244cb2620123c2d71763d429d017ceb98fbb6faf48/cast_value_rs-0.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d6c425d2803e13408755a0022faa9efd0ab0becdf3573a1a8a7af64b1adee4ba", size = 591027, upload-time = "2026-08-13T09:14:52.281Z" },
- { url = "https://files.pythonhosted.org/packages/90/97/b489f8048491f2ece21bf99d5a1fea3bbfaf3cd0e9f555ac4a0352c11d81/cast_value_rs-0.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:92556f1bb9d6b222736d967228565ec745c640c29bc291d1cb730a1011d3dcac", size = 665201, upload-time = "2026-08-13T09:14:53.769Z" },
- { url = "https://files.pythonhosted.org/packages/ce/9f/fabffd9788f0b2d3f49ca4582b8859ab58b929b1b8837ec0be1263399279/cast_value_rs-0.4.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1b0881c23a8f5ff1dc6c70542179f2fc9191e0b7fa7a02f65316fb48904cd8d1", size = 831085, upload-time = "2026-08-13T09:14:55.244Z" },
- { url = "https://files.pythonhosted.org/packages/b5/66/ca653bf9b51eb624615623611db28ec9428a351259fcf5625c5520faf759/cast_value_rs-0.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8a67648bf47d9b740388efaf6f05185f22dea783e97eba895efdd1dbafe8e3cc", size = 788462, upload-time = "2026-08-13T09:14:56.666Z" },
- { url = "https://files.pythonhosted.org/packages/f1/54/3a9280f791c8765a54f7814a010d2f460c7d5f5483be3e5634f4d9b38d75/cast_value_rs-0.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:decf297bc576675fff2139885fb88f84844e27b99e5d28eaa01909db7a327c4b", size = 753055, upload-time = "2026-08-13T09:14:58.302Z" },
- { url = "https://files.pythonhosted.org/packages/06/26/09f4618dbab2de8ea427e817c349b8391b4d559316de899e9b54dde6143e/cast_value_rs-0.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:79a8c13af39bf65014dd6567dc5fa7c42dfda246e4335ab52814f3dd6fb42cac", size = 442068, upload-time = "2026-08-13T09:15:00.414Z" },
- { url = "https://files.pythonhosted.org/packages/fc/49/7def5933625ff3c98e7d20cbd7de0aa1e83098ffa425a9df087764657767/cast_value_rs-0.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:b8af7de3639271ad9e62aba4d15ddd22819c706bd7c7859663fdbb022397bdad", size = 397846, upload-time = "2026-08-13T09:15:02.022Z" },
- { url = "https://files.pythonhosted.org/packages/6f/fc/1f48ae9ea1f10caf3bbc31b288d08137835d8965188f6ddcccdb3c7d398e/cast_value_rs-0.4.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:297445abc64891a2e62af93e4f35bb280f874ac548646d0a6b04a86d45e636a5", size = 494394, upload-time = "2026-08-13T09:15:03.604Z" },
- { url = "https://files.pythonhosted.org/packages/7f/f3/d633218979cd8839fcfbade64a277c0ff9d8ff59d21bbd8a59f1467085b4/cast_value_rs-0.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8b6e0781b4ce61a559cc050d836a52157bd30b1f59d63fdeee79fbc19ab7e57", size = 458677, upload-time = "2026-08-13T09:15:04.947Z" },
- { url = "https://files.pythonhosted.org/packages/97/e4/96d72ccf5e00dd7af7bf55c96313df2ffba1f34a224f10430716232775c0/cast_value_rs-0.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8399dc89570c01537c07adc03fc2fc0e43a517be8a198e2f9e3f8a70dd003be0", size = 487289, upload-time = "2026-08-13T09:15:06.493Z" },
- { url = "https://files.pythonhosted.org/packages/93/30/18313de8c1d3826e9a6abd883b7a7981b19391f674dcd52ff2635d19f8ee/cast_value_rs-0.4.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:939417d72c9945ce24eb959e6b2cd32f7aa7d0a50a06a0fac484056b10b650dd", size = 555221, upload-time = "2026-08-13T09:15:08.28Z" },
- { url = "https://files.pythonhosted.org/packages/e9/1f/ed22b2e536ccbd4c5b234d869fc8f478347a45f7b940273023a0d6013134/cast_value_rs-0.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:101318e594ee998a5ccbc5ea7ed045cbd9fd22d45cac467c745fbd30309f1343", size = 650972, upload-time = "2026-08-13T09:15:10.103Z" },
- { url = "https://files.pythonhosted.org/packages/d7/d2/39f9ff9b678dfb01d206aec6be0f486137d5e9669ebadf0d1f356d2a7324/cast_value_rs-0.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d23bac26b2c2fe6a2368d10fbb072a71671f6e0d4d8cf9e893f62ed3530bbd1b", size = 560869, upload-time = "2026-08-13T09:15:11.602Z" },
- { url = "https://files.pythonhosted.org/packages/60/18/f63e8378db66b5a99e96c1422c68ba02a4e35de34355b90cf6d169d99ae6/cast_value_rs-0.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d799a20d7517974d61d7c4dd54e5d7ef439e726012c087bada31bc644b1535", size = 548885, upload-time = "2026-08-13T09:15:13.18Z" },
- { url = "https://files.pythonhosted.org/packages/98/af/12349007b670b11e16d3be51c29ab2398550b2653dae692a5d788d3dda87/cast_value_rs-0.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9f0f04815efcc47592474e91f61f24810139396696f0690f3d0bf0e6ff3bb4a", size = 590846, upload-time = "2026-08-13T09:15:14.67Z" },
- { url = "https://files.pythonhosted.org/packages/a3/cf/6a39df104923bc397ae58f90e21d1e87edd62d17f24fe3d6777e6c4d3e5f/cast_value_rs-0.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e69f81dab0b47c74c1f30c79198c412938b6c41510b8124dbd08fb36d13a8807", size = 665064, upload-time = "2026-08-13T09:15:16.527Z" },
- { url = "https://files.pythonhosted.org/packages/36/df/0a9ac5bae4e1d3b7b4a79795054d822c26bdbc3bedad71fe13e8ce666c4b/cast_value_rs-0.4.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:59e15ec495e8707447180e0111e65e083464da60236edd5c3387efedce3da997", size = 830909, upload-time = "2026-08-13T09:15:18.088Z" },
- { url = "https://files.pythonhosted.org/packages/2f/7d/9e99c969dd76087dbf731667982d8bb6b390d29c8f37c09dd330657b0487/cast_value_rs-0.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f9e787397588fabb28a146a28159150f44e8ef85a25abe364f989044f1b7159d", size = 788217, upload-time = "2026-08-13T09:15:19.706Z" },
- { url = "https://files.pythonhosted.org/packages/21/a9/5d7410d37be26bed1dc740c885a0c7fdb213b93e9b13ac76fb3282aa50ea/cast_value_rs-0.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c66ca6ad21758993830a9f4d24263065727bf767e4524d79ded0c0b61e3da9d", size = 752817, upload-time = "2026-08-13T09:15:21.197Z" },
- { url = "https://files.pythonhosted.org/packages/65/10/9c18cb01104bb211c9b0d9a956fae3f1a40f743c35bbc6763d0114ec36a7/cast_value_rs-0.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:26fe305d5bd35d21c5ee72c5651892895447e175ba3019a24713c565d965c0ef", size = 442025, upload-time = "2026-08-13T09:15:22.575Z" },
- { url = "https://files.pythonhosted.org/packages/64/49/42a0b48a71aca3ebb0abd0948fa9d484723c3562af30a8d6904b874abb2f/cast_value_rs-0.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:9ce6678fc4b1bdc459178225347acd8019f1d22bca1e13ab86cc2a3e4ecb2f46", size = 397871, upload-time = "2026-08-13T09:15:23.906Z" },
- { url = "https://files.pythonhosted.org/packages/be/e9/baf39cd3991962705ffd055fce071e23e7707f62df3fb44af2ffe0e84d11/cast_value_rs-0.4.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cefdcfd4d2b95e3b35fd98b3d3d865be9b3ef1bc1264db3bc1b8c50c5e8ac999", size = 495672, upload-time = "2026-08-13T09:15:25.168Z" },
- { url = "https://files.pythonhosted.org/packages/ed/9f/7ac6bf95d160f3fd89020aa952ffb4a49216b85a3fda02f84bc2c2612a2f/cast_value_rs-0.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbcec2cfd199c1e404f5bda71b273b4a7470762f4b55c1c21f09d60d2762e339", size = 458480, upload-time = "2026-08-13T09:15:26.647Z" },
- { url = "https://files.pythonhosted.org/packages/65/e5/46c2985ccbae41c8140f04f2acc9080761f10e8d2f8101c46083e632f812/cast_value_rs-0.4.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c224a9b30c2521cd06eac4bb1ac3860f41c902695e6f806344bc039d7a8a1d93", size = 486807, upload-time = "2026-08-13T09:15:28.129Z" },
- { url = "https://files.pythonhosted.org/packages/18/13/97851c45b275fbc9d42b5d5a340fd5ea430525b60c2eacfe6a36399d69ef/cast_value_rs-0.4.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1eaa7c3d5fe418a74928f78514f9baeaeb8e31b93db96b1761edd6144f0d2eff", size = 555335, upload-time = "2026-08-13T09:15:29.648Z" },
- { url = "https://files.pythonhosted.org/packages/7f/b3/c84c1e820786ddb062b162112b80e5dfc1b5b54b11125618f38c1e18d075/cast_value_rs-0.4.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bddec46b36f29964c8d4bbe82b8270575fbe1c0fb41c404bb3da1778f390a24", size = 650347, upload-time = "2026-08-13T09:15:31.044Z" },
- { url = "https://files.pythonhosted.org/packages/60/38/52b4cc31570384e0286e28a03164a357d53a8aba7421ef83e500843d5449/cast_value_rs-0.4.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab3fd77144d26e2da2a113f862c05ea1cf465df347daf5deb6cf1689253fddfa", size = 561498, upload-time = "2026-08-13T09:15:32.485Z" },
- { url = "https://files.pythonhosted.org/packages/41/c8/313e1d016b0adfccd1ead2bb28d2c94313ab62534bd6e11db19a480d3b55/cast_value_rs-0.4.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4affe8668a2f17764ed978c2dddb2b49554f36a71389e6928a8a5523c1a4173", size = 548676, upload-time = "2026-08-13T09:15:33.912Z" },
- { url = "https://files.pythonhosted.org/packages/81/8b/a11f8e514598ebfc084507b0db390ecbdc6bfcffbf91ae666e0de93e6c25/cast_value_rs-0.4.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a67316b00ca01a78865d222badef2b1cb6c60ad5ebd360bf4fa3b224e87b8ffa", size = 590803, upload-time = "2026-08-13T09:15:35.384Z" },
- { url = "https://files.pythonhosted.org/packages/94/1e/ed86da6fe69eff312cc83e348cf4715d4168ab2699783febd24bfd477cd8/cast_value_rs-0.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9946054c77d7c5df579e9e99ebebd4d8a220cb04a6af1b6467112dfe07eee72d", size = 664118, upload-time = "2026-08-13T09:15:36.797Z" },
- { url = "https://files.pythonhosted.org/packages/0b/01/58a3eadff1233bd98d5809e2f6b5dc6d1b8c4c770664145f57fd0bdce756/cast_value_rs-0.4.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:95b064458233a7ef0525481eccfd2d37e86620d877ab1535ae809d37355cf131", size = 830686, upload-time = "2026-08-13T09:15:38.645Z" },
- { url = "https://files.pythonhosted.org/packages/e6/40/a234d9d06013a0bee08f662ab083b3a55decfa2dbb315094fdf06693d425/cast_value_rs-0.4.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:d7de068a1dcc65d1aaf4e93228198b588776bd25b62ef42478ab7efe5245a6f3", size = 787835, upload-time = "2026-08-13T09:15:40.078Z" },
- { url = "https://files.pythonhosted.org/packages/d6/72/93f906345d03972de52e3e74dd4ed12b56aab53bec85cc96020690ba6dbe/cast_value_rs-0.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae0cbdd44290a74e34519eb990b33bf0f8e96f0fde95ffc799913ff1aab27719", size = 753039, upload-time = "2026-08-13T09:15:41.702Z" },
- { url = "https://files.pythonhosted.org/packages/e3/a5/47b6db4f68da086db3f3768775f545b9b7f660cab8f61039c91de1f813c4/cast_value_rs-0.4.2-cp314-cp314-win32.whl", hash = "sha256:d438517530d3a8d6bc810ab6dee831c48b45287435513fea1b0476b5d4fda07d", size = 387092, upload-time = "2026-08-13T09:15:43.218Z" },
- { url = "https://files.pythonhosted.org/packages/d3/9c/5f71a5e94875bb999646227a583792dbbe9a2b257df0d961e8a2da879f9e/cast_value_rs-0.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:7e7bb9faa4820b7eb0d99074fd61efff3a7392e72b6abe4b41c9d6b6d14fe457", size = 442126, upload-time = "2026-08-13T09:15:44.623Z" },
- { url = "https://files.pythonhosted.org/packages/95/1c/8e7546a044da8e92eafbb12594169301cc94e2a071fd3e34204595d0a05f/cast_value_rs-0.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:1f57e762a06102d61e940548a6ab841e05ad1f51f1c4e2aa26bc27a8c59a5abc", size = 398200, upload-time = "2026-08-13T09:15:46.117Z" },
- { url = "https://files.pythonhosted.org/packages/a0/09/527646deeb622e5a597e4271e9e06d9d7cd24d64de77b8cb4ce250128c18/cast_value_rs-0.4.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:230f6440db74d2d11e513060d0e72aa3f897546465bc778d30aaeac90f8e733e", size = 485197, upload-time = "2026-08-13T09:15:47.622Z" },
- { url = "https://files.pythonhosted.org/packages/b9/79/aaa713cb151a902439b670a4774ea76d0a5dadf1f03954f6a34af2a6cb1e/cast_value_rs-0.4.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37538170dc1fedbfd62dd56670e443b881474e7eda7f583a3c989c83a9ff15fc", size = 538669, upload-time = "2026-08-13T09:15:49.148Z" },
- { url = "https://files.pythonhosted.org/packages/d3/b1/2cf4f4f13f74caaa4dab4ddf4924fdeeffb39920d5ec30a80112aa041efa/cast_value_rs-0.4.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d00b9cef0d38d0c5631ba63270c38c5d7c85aa87c609644f204919f939f0c9", size = 649341, upload-time = "2026-08-13T09:15:50.614Z" },
- { url = "https://files.pythonhosted.org/packages/39/8b/1faa6b27cda3dd793635c7945118e49188b6714d6bf23bb072efd718e467/cast_value_rs-0.4.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89434eebb7a12f66e99382e08c32648f055e24d9fce66274adef6620af2aebbd", size = 555253, upload-time = "2026-08-13T09:15:52.252Z" },
- { url = "https://files.pythonhosted.org/packages/8b/c2/d31f177025b51a3fda70c5888b92f5fd30ec53bc2df2c046fc9a2717cd36/cast_value_rs-0.4.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:704171aec75f417f5a70a3eb5f107fbfc79183620093c9d9074290920a5981f1", size = 547964, upload-time = "2026-08-13T09:15:53.669Z" },
- { url = "https://files.pythonhosted.org/packages/64/99/521c2d6347ca39deef237909dfee61c6d2cb806dcc20d74636e209bc646e/cast_value_rs-0.4.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2add2599d299436ec161661a8e119a69742c763a0936e542db92b1ce9d6e0c04", size = 574778, upload-time = "2026-08-13T09:15:55.479Z" },
- { url = "https://files.pythonhosted.org/packages/f0/70/35c7cb37839454aeb93b9576b42daebd8249dd59e3834cd1be020b4b0660/cast_value_rs-0.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da5df79a926bdcc1f5494612f621027970a9702eb49d556068025166d4c0ace2", size = 662461, upload-time = "2026-08-13T09:15:56.97Z" },
- { url = "https://files.pythonhosted.org/packages/24/4f/a71bfaea1286bd12cc912733d650bd89a07c4430ee715202551c6e3eb0b0/cast_value_rs-0.4.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5552e3c80d43de7a8a821793cb57da0dd3720a377051a5bf86bcabe094e87777", size = 814743, upload-time = "2026-08-13T09:15:58.652Z" },
- { url = "https://files.pythonhosted.org/packages/94/a3/b04e0f42dafeae30604abca079cce303ebb69ba24f3c4debbf211ecc781c/cast_value_rs-0.4.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:73978c6dd2ce45a05a59ec7090df8b3f2a579ee86db9b1c7df21ede17f32189e", size = 782340, upload-time = "2026-08-13T09:16:00.135Z" },
- { url = "https://files.pythonhosted.org/packages/3a/6c/913de75054092de796509dc04b92fc6ac1bf83dc86901f1f482667dead16/cast_value_rs-0.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:45d630592afb18880d4df0d7afb28b8cfc0ba6aa742d8db115f616df284b453a", size = 754068, upload-time = "2026-08-13T09:16:01.572Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/a2/25/151a7ac3e68a7bfa76d91dcbe318828dfa227841cd3ca1809890c052b0d6/cast_value_rs-0.5.1.tar.gz", hash = "sha256:67220f38cf791489b93ecee1e33370428326a523a211760faa36dd6f9b60a07b", size = 91796, upload-time = "2026-10-31T08:45:39.516Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/01/3b5bd177228cfcaf72a08a42f0b2184d029845edabb337b4b32c825be5ac/cast_value_rs-0.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f6410d5f5ca3b1c5a5ddb9e54f0064d2a1486ec8cdb187d6df344cb8294bba51", size = 514726, upload-time = "2026-10-31T08:43:15.438Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/fe/b0863f6769c8396a247ba30a728c041c094634890c9fd698349711fb5498/cast_value_rs-0.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d10b5ff6fd0d07133f5143384837f692d4034a113947e8b0bc4309aa39ab66f1", size = 474619, upload-time = "2026-10-31T08:43:17.336Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/0b/3820f21a9f5c9d71cad9dba22274cee693962bc9a503a5abbe0d94c80704/cast_value_rs-0.5.1-cp312-cp312-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:a43ca46ebdff1aaa9260b61b4527db72040123010aab1ec281fe8dcfced2ad1f", size = 563294, upload-time = "2026-10-31T08:43:18.812Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f9/3bcb27ea7ee0766eaffee2e75461a416beafccafde35c6816d152eebd906/cast_value_rs-0.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b167288f74bc2aa1201ed9b3595c767cdb67abe289cd36c1c78b5f394d94eda3", size = 477658, upload-time = "2026-10-31T08:43:20.248Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/04/187ff973140d1cd77d0a8067f2ac96e4693d50f3cbd206ca8127019edef8/cast_value_rs-0.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:7aecf24a3444ea02e082f94a4c469ff527c2f580eb71cb702f091ebc3c19c5d2", size = 536492, upload-time = "2026-10-31T08:43:21.733Z" },
+ { url = "https://files.pythonhosted.org/packages/48/02/b2dbf5e4ed5caecc4f1686d5511bdff3769db5f28f262acf8f22d62feba8/cast_value_rs-0.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4351299e8fdeb83cbd34c0827a29fd8c8b160a3226cae774d3e682a0d9acc3e1", size = 632370, upload-time = "2026-10-31T08:43:23.687Z" },
+ { url = "https://files.pythonhosted.org/packages/db/44/f168d64613bb9a42595c4e55cedcf94b81fa4c47bb142c71970a7d6359e8/cast_value_rs-0.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a8bfb2dd64d8cf51cddff9abb396f0d5bac118a8065c69f9c0a5b223bcd3a2cd", size = 558721, upload-time = "2026-10-31T08:43:25.189Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/b3/383147f4e9e3dc04ab8e392c282653bedc871fb96344f6d773a23d6345f9/cast_value_rs-0.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:891fc3844dc25980a688dee92f7042d332ca6966ca952347cd335339b0ad1504", size = 525962, upload-time = "2026-10-31T08:43:26.784Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/d3/7e84f237a128047398cb786fc2ea1e4b0bc52f07cbdb6e8bf47d9d0df635/cast_value_rs-0.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f973626e9d6d31da4784dbc53d30c8c200724bf4f244d6dc890c79a479c01871", size = 654291, upload-time = "2026-10-31T08:43:28.259Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/5c/3e9442c4aa6d6b1e45e901bbd4f591df1729acc7842b02eb4c4a7c5429f6/cast_value_rs-0.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cd7c41110ffe110f5364a04e2b2a82a7dbaa57166e5b7c2893b9970d4e871c33", size = 812705, upload-time = "2026-10-31T08:43:29.696Z" },
+ { url = "https://files.pythonhosted.org/packages/43/7c/3e1b5742839ba9a97ba4e2cffb02758c8209b462f00474b08dc69537edc6/cast_value_rs-0.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2d56b5b7cfeeea8737772bf1ccb19835111cbf669d773591826ec53fb551dbeb", size = 766586, upload-time = "2026-10-31T08:43:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/e0/ffa2b2fbe125f9f0fd3f2c3cd4c818317d7523a2c02a9b6ab0d48d76644c/cast_value_rs-0.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b14d256361bb449562194c4f12c51f9b7745900412f37c84c1f759e6eab362fc", size = 730423, upload-time = "2026-10-31T08:43:32.782Z" },
+ { url = "https://files.pythonhosted.org/packages/40/7a/42c1ac8202610e48204a07f98f7055515c2b7d860c6fd8de5101e45c7974/cast_value_rs-0.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:ee3e305f27dd0f2c3334eb80c874bdd5262f0a707ad60b4ff2857a3c9a07b5a3", size = 401775, upload-time = "2026-10-31T08:43:34.402Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/e0/9b85c3da33c11f0ef38b03878bfe4b55ab06d4282bc3b2db0be94f30f2c6/cast_value_rs-0.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:6bc12f62e3a890243322999596d7001cf730cc59599538ad284f5e4508101bbc", size = 370508, upload-time = "2026-10-31T08:43:36.084Z" },
+ { url = "https://files.pythonhosted.org/packages/42/ce/e881f4ca411676ee6918ad74ba57639a5a0b0fa0ef43f3c6dc037c1dd5e2/cast_value_rs-0.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:32ff4e7e5c2bcbc3fe85314bd15553321ad43f4869e7015a45e05f1d131964d0", size = 514776, upload-time = "2026-10-31T08:43:37.558Z" },
+ { url = "https://files.pythonhosted.org/packages/80/cb/37300e5c5e2bf2396883aa9fdfd90e2c9cea6b33dfc1a69b04b4e723a973/cast_value_rs-0.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2689fe1558d5ecbd115bbd2bb97f3a1f2a458f3ebc656f268ceb94624b4dda26", size = 474544, upload-time = "2026-10-31T08:43:39.12Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/e5/c45635cb3da6b1ab9d06e57bd63e294362948d9ad6380a75570c4686ffb8/cast_value_rs-0.5.1-cp313-cp313-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:71e419e6f4799c187fffbe8c6a96bc1c07d17ccc63a8d77f1fcf74f01ce6f98e", size = 563316, upload-time = "2026-10-31T08:43:40.906Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/5d/da4920da8731987c016730cb527688d5eccfd0660dec1ad387ac5e97a996/cast_value_rs-0.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0fa4372466e59197be1c0ab17a909d16e9108fc5327ca38d8519e014fa6bf70", size = 476817, upload-time = "2026-10-31T08:43:42.322Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/b0/afabc7883058a18f6e3849a260e6c9faacbfa864d4ec78cadf2d7c8db49d/cast_value_rs-0.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2a3acfbcc7e4cf29ea45cbf2f481199dd19be6f101e5a519614708cfb8c9d9ce", size = 536511, upload-time = "2026-10-31T08:43:43.788Z" },
+ { url = "https://files.pythonhosted.org/packages/98/d5/0806712a64a7b117bbd24e7b2aface573f31a6b8977b0ae50d5a449d0621/cast_value_rs-0.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f7701b7d680689b901c2480289af095c3065c28731066a98d0bf4058ff1d7ec9", size = 631482, upload-time = "2026-10-31T08:43:45.193Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/81/b24b7c75eab58e0f653c62a037955868907f8d782963d88b8c3cd1ab8fba/cast_value_rs-0.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f895ffcf8c24870a92e8697619ef8b091d87159cb354ca3168ed72b092e81907", size = 558991, upload-time = "2026-10-31T08:43:46.766Z" },
+ { url = "https://files.pythonhosted.org/packages/14/b3/f7c034c10bc88ae99ae00c282690aeed534a978fa591fb5ba7d7947adca2/cast_value_rs-0.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c5acd38e2e8d0aa2873881efb624f076c6286b04af030e61aa124769fa75d06", size = 525925, upload-time = "2026-10-31T08:43:48.247Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/32/6bbbbbfe1352395243120ee77d364c54684613fa0e8c9e59b82571553816/cast_value_rs-0.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2373117423aee9df307b8ff87af9487faa6324124fc30eb358aa9f5630462f0f", size = 653682, upload-time = "2026-10-31T08:43:49.685Z" },
+ { url = "https://files.pythonhosted.org/packages/14/2c/81f025c5ff0acb2ffa5ea3dccc69e7a6ba62fc432376fc1df72b33ddde5e/cast_value_rs-0.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:fa1cda7c516ecb5219b9b109603ba09193bdb1ca9b3a64599e9f597dda576327", size = 812907, upload-time = "2026-10-31T08:43:51.151Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/cf/9baf257796769718ee29e2c0e372d44dc9bf1682edc3e9b2053fe2a0d094/cast_value_rs-0.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b619762eace293242b43286c3fedb8b83660effaac016afca400f7852f71c0da", size = 766871, upload-time = "2026-10-31T08:43:52.97Z" },
+ { url = "https://files.pythonhosted.org/packages/59/27/acbfb113c69afb86bac85b38ba872d95f7c61ecbea3ff350ab5e8e70c050/cast_value_rs-0.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18b3ddcf763d5208e2f8ac9db659837c67d77e22c44b7315e924b7351ed6e08c", size = 730422, upload-time = "2026-10-31T08:43:54.922Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e9/7a097d2844b3541b36946cdeb51430b2cf1f287026e054ac0240b46649ab/cast_value_rs-0.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:0fdc504d6f46b1f49b7c7fd1bf108b605d9d9ecb6054ba578d3f6a38843d6fce", size = 402047, upload-time = "2026-10-31T08:43:56.312Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/71/ad4dbde8cee35f86c660473f6bcb98641236c7b506a4b481bd4954a208b1/cast_value_rs-0.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:b0ef63e39dc55c5210d31e3fc2377668c12db86659af6212474338ab1168c0a8", size = 370621, upload-time = "2026-10-31T08:43:57.832Z" },
+ { url = "https://files.pythonhosted.org/packages/61/7b/4f3aac163a600c3a70be75d9b5652b0482c71117daac7c0276c040e60cf6/cast_value_rs-0.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:7f52ddc9536a2319caa0358088cdb552bd5c3ea0d576dd4bef5378dd87605f94", size = 519536, upload-time = "2026-10-31T08:43:59.456Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/fd/b2b7456241e86e485de36bf7f659be592fe2eaf93aa0df4d2f4b60b37de9/cast_value_rs-0.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4048177e1200023661ef37c0dacedb40d39465cae41f6c8e0da70a64cd98ddb2", size = 476470, upload-time = "2026-10-31T08:44:01.182Z" },
+ { url = "https://files.pythonhosted.org/packages/66/f3/74ce87a4064388862485d6d5394553a2764de3af5e1f7568cb19e19a0111/cast_value_rs-0.5.1-cp314-cp314-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:ef3fa404ed66e5f33c24cef003071f509b7e1f36a7f748182e62eda9e20941da", size = 563457, upload-time = "2026-10-31T08:44:02.813Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/1f/fc7a5e1b0e6af62ee5b48e57c50bcef62713900467b09bcb87a56fbf3fd2/cast_value_rs-0.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:946f69a6e586e0d1f8c395fc97e7a5556b64dbba87a8a34bef112dbbb9690373", size = 478965, upload-time = "2026-10-31T08:44:04.436Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/b1/39ce0bfd2ba25cbda8113eed86afa77f6eedd348d711a067352a73cba2e2/cast_value_rs-0.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:18460ef2f1883238083c6bbd50c2b4be41afc5384de0ad815a7b1483e8b2e491", size = 537191, upload-time = "2026-10-31T08:44:06.348Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/13/8c3f1d0a9593a15add6e0973226ab9e8ddeafad4aeb464bdac3811eae5c3/cast_value_rs-0.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:44d6d057327b96e1fef29ce10510a6ecc9612874d4df30e3e43e0540844b1bf2", size = 633461, upload-time = "2026-10-31T08:44:07.787Z" },
+ { url = "https://files.pythonhosted.org/packages/49/65/fb101465f33896a1454e5a959c9c50099e6d6a37db4d4d899cf924d766e1/cast_value_rs-0.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d3708705c38210d9ba5c6d75433532ca98744f36a5844b07e926a7511b7c0c3b", size = 559760, upload-time = "2026-10-31T08:44:09.529Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/24/9171671d9b14febdbc94810a61f33276bdc1dbc591c0c8d954d0f05a4086/cast_value_rs-0.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20bb849f145eb1d7cff568e84dd2970481c25fcdbae92d3bfb6f041750e92a48", size = 527072, upload-time = "2026-10-31T08:44:11.26Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/bc/f1e902a8b24b04564a45cea3471be06b4a1ba185f65553be4e26f7dd5a7e/cast_value_rs-0.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b6705f2c17704925a516392fbdc194fa1a3010829bd0800f205fd673d15b9751", size = 655678, upload-time = "2026-10-31T08:44:12.986Z" },
+ { url = "https://files.pythonhosted.org/packages/31/50/6ddedc98d93acd6d0fde9bfe6bf17ec5fe83485a00e0dc946b026fde2b31/cast_value_rs-0.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7b98cf7330ab7378f5497c215a7597e974e6224ebaaa46744eb6ec10c7cc6b55", size = 813526, upload-time = "2026-10-31T08:44:14.439Z" },
+ { url = "https://files.pythonhosted.org/packages/29/c1/b3ec914d461603d2d36dc1eb445710e58c90436b6efa1e9d99493adbbbe4/cast_value_rs-0.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:d4229f74c608b895ff7ebae51ea894384c4f8a639f1129df394ed86ac4f7fcf2", size = 767696, upload-time = "2026-10-31T08:44:16.427Z" },
+ { url = "https://files.pythonhosted.org/packages/85/ce/fb4f4ec09a80ebcd5c9e15ef6bb9943edf394653c0f97762d67eab5fda3d/cast_value_rs-0.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:60f4d3fdf33b44a294942938f49618d1908af8051ae80a2bb3ac7a81bf8cb563", size = 731352, upload-time = "2026-10-31T08:44:18.044Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/88/41ce6ec2663e40a28e2e9c8e35e08962a904909fed7ad45d1f6ae1f54c47/cast_value_rs-0.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:d9b5f2a2a8e0c7ca9dec0741e0d7a011fff2e1f7c36273ce4edd806ad7f5b5be", size = 404653, upload-time = "2026-10-31T08:44:19.421Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/27/80a353c489247f3fcb1555e2bb8fa87cfa273fdb12f6bafecf6fcf39313b/cast_value_rs-0.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:94ca8cd384413459399f3de50e383007090e46d55a89e554dc5f8cf2df4633e1", size = 371183, upload-time = "2026-10-31T08:44:20.766Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d7/a1a6f2ad4112592a342e0291accd7d7503d00f7f77afba7bfa746568440b/cast_value_rs-0.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:85511ddd1e54f5d065b2cec047cb90765006b104fab44ce2394364b91257d3ff", size = 491276, upload-time = "2026-10-31T08:44:22.216Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/d2/7a6f34c0a425f5b5264aa72982ae975394ff56af7667c4175da402538967/cast_value_rs-0.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e7588000b116674a94b62b09668afd4047ae1cfa6ca23fa217e2a0f9cc7c5d9", size = 659458, upload-time = "2026-10-31T08:44:23.938Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/95/14ebfec2e8bc9b5f4c4119f3bf341ac8d4af1e4f80cc469dc899627b3095/cast_value_rs-0.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5695711e9c5abe0bd1663e1cca65135ef6521c4aa9b0f66c3ea5dfa6a520d0d4", size = 578621, upload-time = "2026-10-31T08:44:25.344Z" },
+ { url = "https://files.pythonhosted.org/packages/31/bc/79e87e32286543f83cdcc3922dd7346fc5f59391a7f296d20efa77c1b091/cast_value_rs-0.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3394e3114361b4f7898dfc6e06da75373ae1a37ce042d40bd87a2e9e0aed5201", size = 552219, upload-time = "2026-10-31T08:44:27.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/61/2fdad5258778bff2dd557030f1712e7f9fc82be3b4504235111d57111123/cast_value_rs-0.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:57f08679dbca2f45c15ffd21de75da7891be7bfb7fa04ffe4cadcc10740dde49", size = 668370, upload-time = "2026-10-31T08:44:28.511Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c7/e3fd8da9d62a6fafb7ac35b54753bbade989463588769d24d65147d10573/cast_value_rs-0.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea5de15f91e9b59c7077ddee0903de11982440e3ed261995e0173f0d4dfb35db", size = 756835, upload-time = "2026-10-31T08:44:29.986Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/02/61bdbf5c06fce0dcdda04fbd8a2e6b224c5329c2b05089df2ec8b87206f7/cast_value_rs-0.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3bf1543670fd8a3e70b40bfe099848ad938e79c2583335be44328466e9452f60", size = 476694, upload-time = "2026-10-31T08:44:31.419Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/e3/1d842641d5f2ddd7d8debcd26cc372cb048bb5a0c032e011608c3f4cf8e9/cast_value_rs-0.5.1-cp315-cp315-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:d64f290dc2f440a67717cb5051552b509403f3d8ef5b9690876dc514acd13486", size = 563843, upload-time = "2026-10-31T08:44:33.063Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/0c/c3c1b448e97fe9684865bcd6534273cef3ae242d41706f8a05bfa8fab372/cast_value_rs-0.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7fafc0825b4f675fe55d9d22f480043da2b920ce474b4dc53162306c64bca3e4", size = 479009, upload-time = "2026-10-31T08:44:34.718Z" },
+ { url = "https://files.pythonhosted.org/packages/20/5f/91e9a654ac1fbd299ec6c6b6e0dd43f244438debfeb810e519cb7322c6da/cast_value_rs-0.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2d997464871e9c5ad2fa5a950de15390b134abd5a2071fd5e9fb2b27fe97abda", size = 537179, upload-time = "2026-10-31T08:44:36.188Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/8c/844d1a4d7bd0f8a8908b1f311d14a414c11fb2ce8445eb9a88621d528b0b/cast_value_rs-0.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ea69d3d3e1a0aeb428268bff34e2bd88b3ccf5d48200ef77e91e700b637c8380", size = 634858, upload-time = "2026-10-31T08:44:37.65Z" },
+ { url = "https://files.pythonhosted.org/packages/23/c1/93384916c505767f270240cb7f84bb838d486bf2a95e7a12a70267a0c03f/cast_value_rs-0.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9c6d156cc51ba7cc6083bc6c13751df9865b40fb22cb01de0e0de510c3b1c60b", size = 559196, upload-time = "2026-10-31T08:44:39.205Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e8/17541402fe90d8241ce4c8c228d81dba4dc2cf15f9a447d0a3d42f15d6db/cast_value_rs-0.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:63ff7854dd69c00e3d113c1cf80e137ab62b703e1d5a3933a6f317e48629aac2", size = 527247, upload-time = "2026-10-31T08:44:40.665Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/f2/7791c4cc6348daf07ae32be323a387c3abf3693e2d34540810ab0bfa2ee8/cast_value_rs-0.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:306f35ad3cd2cf6dfad817d47ff4f1d72c30857654da264170d5ab0f4cd16ba3", size = 656202, upload-time = "2026-10-31T08:44:42.272Z" },
+ { url = "https://files.pythonhosted.org/packages/20/0a/35f4cadb0012a1f03aa34b4c4dedbc040fa6f2a8ded51791aa54a911078b/cast_value_rs-0.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:01bb73fe09e64f6d7b98efed7e527870b7c5b506248728532c0bb6fb96eb870a", size = 813923, upload-time = "2026-10-31T08:44:43.751Z" },
+ { url = "https://files.pythonhosted.org/packages/23/b9/cfc4993e242375217aaec5c4732adc537280538f99d229c92c1cc4a0bccf/cast_value_rs-0.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:79115de49db59bb715dfb97dd6cb565ed937bb0f4ab65bf7a85c115aeb279865", size = 767624, upload-time = "2026-10-31T08:44:45.189Z" },
+ { url = "https://files.pythonhosted.org/packages/86/73/b4ad932c157b66d95e94e325aa79669667fe9fcee82cd2d4f148c04a9a63/cast_value_rs-0.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:fdb09db5d2585407125ada3b5bb9025cadb2fe7fc467e50839a6ded43add14de", size = 731376, upload-time = "2026-10-31T08:44:46.594Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e2/9e4153d8efa644059126a03ed1957174baf9b8453de6f1ddabc97436dc09/cast_value_rs-0.5.1-cp315-cp315-win32.whl", hash = "sha256:0176678830df81c30cc931624b0a352544bf40a89883b0651471f76389900114", size = 347664, upload-time = "2026-10-31T08:44:48.142Z" },
+ { url = "https://files.pythonhosted.org/packages/81/c8/54e5e9a13ab37bb26b9e769892ac79dd7bae30dca847353d5a9948bf5a21/cast_value_rs-0.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:50b335dd32f4ab9da85c0b07627bf2dfd9c123e6c281eb67c3ad812ecb96cc66", size = 404766, upload-time = "2026-10-31T08:44:49.632Z" },
+ { url = "https://files.pythonhosted.org/packages/31/9c/2b8d62dd4720aa96e88b5a5b499eba91dea6fb6d5f2c88ccca77563af4a1/cast_value_rs-0.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:39a2017df8ac0ac80fc6049920a569c7ef9ffb452958e51d7690003d241f3b41", size = 371573, upload-time = "2026-10-31T08:44:51.008Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/61/a1bc81db4af793e7268854065f1e238d548ebf107a12c276cf1296525647/cast_value_rs-0.5.1-cp315-cp315t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:95f1d08db5067bef51ac65b9addcec901132d4dc51bee089f5a50513c07c4e16", size = 594666, upload-time = "2026-10-31T08:44:52.621Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/bf/cd460b4f80d0c7ca531f3bda9596a9902b8847b986fed351210b9e1a3d63/cast_value_rs-0.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bc4f9995a6c9629c1b625c81309a22584a38738d8a4bf19d53d703e877d0a625", size = 491256, upload-time = "2026-10-31T08:44:53.958Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f9/82558521a5be44dca82f14d32ed39e268aaca7a8c55d855e07cbe1839d01/cast_value_rs-0.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:d1fa3b07d705a625ff1effb6cf7068290a04b6fb342ffdb2734666563c4a5842", size = 555869, upload-time = "2026-10-31T08:44:55.579Z" },
+ { url = "https://files.pythonhosted.org/packages/38/c7/0345a1d127f86672383abcf201f465915b1c7551726da61d0c7a7a39a075/cast_value_rs-0.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:82fb5958e1bddd1b6b59e5f320b7060a02c28ea4dfba959cf0199e92c8451fc5", size = 658025, upload-time = "2026-10-31T08:44:57.02Z" },
+ { url = "https://files.pythonhosted.org/packages/16/32/0884b5a9debc5d45fee87732aec96e720ec9e312b14f352c85d811d5ecb6/cast_value_rs-0.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:0aee9b55d721ab9aca8e9ca882e904d25d43562139d228ca8190605b19b77d4d", size = 578598, upload-time = "2026-10-31T08:44:58.622Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/01/da585bb0fb303495779123d1f681fcbc219b00e33c23b0055ad803e586da/cast_value_rs-0.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:76fcd3c6322bdbd24be9772c66c7aec314ad48e801c14857f8553ad42884733a", size = 552414, upload-time = "2026-10-31T08:45:00.145Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/71/28da1e78625ae61110f03688d92a4c77f1e1e5a72dfd266901f4ba79c3a0/cast_value_rs-0.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:eef6fa6aa0dbbf0a31a50bd3e4482f95a662a9a1bbcc2cb4d47adb68189ed94c", size = 668386, upload-time = "2026-10-31T08:45:01.72Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/03/5016bec95d999a884fb2f49746e8f2b705f522677701051111d1442cc7c2/cast_value_rs-0.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:c27411eeb713daf0afe0a3d8025fdc1cc32191d15df0c0ac09f229cf3cd97d1b", size = 831458, upload-time = "2026-10-31T08:45:03.37Z" },
+ { url = "https://files.pythonhosted.org/packages/44/db/cb3369d143c5eb5f998fd78e2772f985a3f73b30452a0be043ee83e62916/cast_value_rs-0.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:fdb1bdca4c472199a410ade27c3123b9c8aca4c5dd6a91597a5ca740a00d8eca", size = 796713, upload-time = "2026-10-31T08:45:04.845Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/2b/73ea6609be5583e648a6fcc723dc66296b0753bd98cbd10a3afca49c8da3/cast_value_rs-0.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:825d422a0e4f79a1103e8fae70f45a3244ca437ce317bb0d49bec6b5a4fccb6a", size = 757395, upload-time = "2026-10-31T08:45:06.343Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/4a/317620763de8d2061638cb30a10871100f2585e45a65a97bb54e1f0d1a29/cast_value_rs-0.5.1-pp312-pypy312_pp80-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:5a0eed236a5f65a5a4c84598b5cbfb77d6b295785684ef6912b47c2267429768", size = 587381, upload-time = "2026-10-31T08:45:25.09Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/bb/449c2fbcc5238cf24fd4c74d05d8d457287eee8ed7e6436dd8f730745f17/cast_value_rs-0.5.1-pp312-pypy312_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d072bc28e8f9fa25c2bd7fc73ed5566a2f752af38e38203ef142a9258353ff4", size = 485865, upload-time = "2026-10-31T08:45:26.455Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/cf/8113580800507c4c5a35a7876c4f2dccd2d05bbd84fd0820ad74489223c4/cast_value_rs-0.5.1-pp312-pypy312_pp80-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:55a474d9495588216625f960eb9ac5d281d5463222c5dc717745020f914444e8", size = 547762, upload-time = "2026-10-31T08:45:28.065Z" },
+ { url = "https://files.pythonhosted.org/packages/34/28/3119cdb31d40ea7fa5165a5bcb9e141db0d2a4737c393a09a473ab608a73/cast_value_rs-0.5.1-pp312-pypy312_pp80-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:57bbaa75c2b19e4e276cb26c98a94a08e80c657398be4dd28e149aa3e99af671", size = 650458, upload-time = "2026-10-31T08:45:30.123Z" },
+ { url = "https://files.pythonhosted.org/packages/34/04/40206336236ccbc59638a0e87383f1bf4c76a8feea89e48f0dac2b44d463/cast_value_rs-0.5.1-pp312-pypy312_pp80-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1f2fcb8e0487f5a0f9cbca9bb2d5ab15f227f77acaa7ea23b5dbda7e58179084", size = 568208, upload-time = "2026-10-31T08:45:31.545Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/31/8a26430d6a1e0f6b6f47b2c5faf30dc30a098d9db2e0734ad8fd44f4ed99/cast_value_rs-0.5.1-pp312-pypy312_pp80-musllinux_1_2_aarch64.whl", hash = "sha256:740e7fadb7d4884451d5b75ad4410ae2508c7a09f682797462c910abb285cc95", size = 663319, upload-time = "2026-10-31T08:45:33.133Z" },
+ { url = "https://files.pythonhosted.org/packages/73/d2/705418aa24017bd6bf574f96d42132fd61cb0a69dbf495129e7dfb1ca1d7/cast_value_rs-0.5.1-pp312-pypy312_pp80-musllinux_1_2_armv7l.whl", hash = "sha256:7ec196b6debda0ae9cbded8528951e0f8d2be57eb464028568733afd32aa5b2c", size = 823564, upload-time = "2026-10-31T08:45:34.543Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/e4/3c29de8fa19ddf9f97acfd9f44d93423da9596b5c2bcc27a9f22e2840026/cast_value_rs-0.5.1-pp312-pypy312_pp80-musllinux_1_2_i686.whl", hash = "sha256:8d69bddf06b53ca6f543d49ec64e99aeda569d208808f4b2b0829044a10e6299", size = 787628, upload-time = "2026-10-31T08:45:36.069Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/9f/f5bd9736239a12106e726de7b1eac0341d4b0a3e611e120452fd4c827999/cast_value_rs-0.5.1-pp312-pypy312_pp80-musllinux_1_2_x86_64.whl", hash = "sha256:a3b162f91f060845743ef8519e86643b3cfa92fd4404ab7e988c2a09c2b45432", size = 745735, upload-time = "2026-10-31T08:45:37.706Z" },
]
[[package]]
@@ -3435,15 +3463,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" },
]
-[[package]]
-name = "tomlkit"
-version = "0.15.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/08/72/fc01563df95c189a9f65850096bdc205bc2fe37da6c740cd64afaf18d07e/tomlkit-0.15.2.tar.gz", hash = "sha256:83e32755ba1d20d9340bd069ee97e9f6232ba2346b63ffcba425de01f7e327a6", size = 190522, upload-time = "2026-10-16T04:07:08.166Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/da/46/7d84138a60a8bd796825b2d38ebe92d0d16501ca57d501b7c5d677401bbc/tomlkit-0.15.2-py3-none-any.whl", hash = "sha256:05562412163a3037b94497385fb7f9204c637bc605af7a48199e41cf2272f437", size = 52225, upload-time = "2026-10-16T04:07:09.216Z" },
-]
-
[[package]]
name = "towncrier"
version = "26.9.0"
@@ -3846,7 +3865,6 @@ dev = [
{ name = "requests" },
{ name = "ruff" },
{ name = "s3fs" },
- { name = "tomlkit" },
{ name = "towncrier" },
{ name = "universal-pathlib" },
{ name = "uv" },
@@ -3888,7 +3906,6 @@ remote-tests = [
{ name = "pytest-xdist" },
{ name = "requests" },
{ name = "s3fs" },
- { name = "tomlkit" },
{ name = "uv" },
]
test = [
@@ -3903,13 +3920,12 @@ test = [
{ name = "pytest-cov" },
{ name = "pytest-reportlog" },
{ name = "pytest-xdist" },
- { name = "tomlkit" },
{ name = "uv" },
]
[package.metadata]
requires-dist = [
- { name = "cast-value-rs", marker = "extra == 'cast-value-rs'", specifier = ">=0.4.2" },
+ { name = "cast-value-rs", marker = "extra == 'cast-value-rs'", specifier = ">=0.5.1" },
{ name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'gpu'" },
{ name = "donfig", specifier = ">=0.8" },
{ name = "fsspec", marker = "extra == 'remote'", specifier = ">=2023.10.0" },
@@ -3956,7 +3972,6 @@ dev = [
{ name = "requests", specifier = "==2.34.2" },
{ name = "ruff", specifier = "==0.17.1" },
{ name = "s3fs", specifier = ">=2023.10.0" },
- { name = "tomlkit", specifier = "==0.15.2" },
{ name = "towncrier", specifier = "==26.9.0" },
{ name = "universal-pathlib" },
{ name = "uv", specifier = "==0.13.5" },
@@ -3996,7 +4011,6 @@ remote-tests = [
{ name = "pytest-xdist", specifier = "==3.8.0" },
{ name = "requests", specifier = "==2.34.2" },
{ name = "s3fs", specifier = ">=2023.10.0" },
- { name = "tomlkit", specifier = "==0.15.2" },
{ name = "uv", specifier = "==0.13.5" },
]
test = [
@@ -4011,6 +4025,5 @@ test = [
{ name = "pytest-cov", specifier = "==7.1.0" },
{ name = "pytest-reportlog", specifier = "==1.0.0" },
{ name = "pytest-xdist", specifier = "==3.8.0" },
- { name = "tomlkit", specifier = "==0.15.2" },
{ name = "uv", specifier = "==0.13.5" },
]