diff --git a/packages/zarr-metadata/changes/427.removal.md b/packages/zarr-metadata/changes/427.removal.md new file mode 100644 index 0000000000..11d0f8a5a5 --- /dev/null +++ b/packages/zarr-metadata/changes/427.removal.md @@ -0,0 +1 @@ +Replace `ZarrV3Context.update(**slots)` and `ZarrV2Context.update(**slots)` with `copy(*, update=None)`, after pydantic's `model_copy(update=...)`: `context.update(codecs=[...])` becomes `context.copy(update={"codecs": [...]})`, and still replaces each slot it is given whole. To add definitions to a scope and keep the ones it has, use `union`: `CORE_AND_EXTENSIONS.union(ZarrV3Context(data_types=[MY_DATA_TYPE]))`, which the docs now show in place of splicing a slot's definitions back in. diff --git a/packages/zarr-metadata/docs/context.md b/packages/zarr-metadata/docs/context.md index 4786effd33..08bb26eea6 100644 --- a/packages/zarr-metadata/docs/context.md +++ b/packages/zarr-metadata/docs/context.md @@ -73,10 +73,10 @@ In this example, our context object only contained one codec definition, for the After resolution the `gzip` codec was explicitly flagged as unrecognized. But we can actually check that codec declaration against a real codec definition by expanding our context to include the `GZIP_CODEC` definition: ```python test="true" session="context" -new_context = context.update(codecs={**context.codecs, "gzip": GZIP_CODEC}) +new_context = context.union(ZarrV3Context(codecs=[GZIP_CODEC])) ``` -`update` returns a new context with the `codecs` slot replaced. We pass the codecs the old context already has, plus `gzip`, so the new context claims both `bytes` and `gzip`. The old context is unchanged. +`union` returns a new context that files every definition of both, so the new context claims both `bytes` and `gzip`. The old context is unchanged. To replace a slot instead of adding to it, use `copy(update=...)`. Now read the same document in the new context. `read_array_metadata_v3` returns everything the read found, instead of raising, so we can look at each codec: diff --git a/packages/zarr-metadata/docs/parsing-validation.md b/packages/zarr-metadata/docs/parsing-validation.md index adfe8e51c2..eef3b92671 100644 --- a/packages/zarr-metadata/docs/parsing-validation.md +++ b/packages/zarr-metadata/docs/parsing-validation.md @@ -210,21 +210,21 @@ unclaimed = None if reading.metadata is None else reading.metadata.unclaimed_fie can_open = unclaimed is not None and len(unclaimed) == 0 ``` -To make an unknown data type or codec known, add its definition to the scope's slot and read the document again: +To make an unknown data type or codec known, add its definition to the scope with `union` and read the document again: ```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, CodecDefinition, ZarrV3Context # 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.union(ZarrV3Context(codecs=[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. +`union` keeps every definition the scope has, and raises `ScopeConflictError` if `MY_CODEC` has a name the scope already files with another definition. To replace a definition on purpose, use `copy`, which, like pydantic's `model_copy(update=...)`, replaces whole each slot it is given and keeps the others. A slot can be given as a mapping from each definition's name to it, so `CORE_AND_EXTENSIONS.copy(update={"codecs": {**CORE_AND_EXTENSIONS.codecs, "gzip": MY_GZIP}})` replaces the gzip definition with your own. Passing `copy(update={"codecs": [MY_CODEC]})` instead would leave `MY_CODEC` as the only codec. [^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/src/zarr_metadata/definition/__init__.py b/packages/zarr-metadata/src/zarr_metadata/definition/__init__.py index 6dd9c86173..5ccb3cb527 100644 --- a/packages/zarr-metadata/src/zarr_metadata/definition/__init__.py +++ b/packages/zarr-metadata/src/zarr_metadata/definition/__init__.py @@ -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, CodecDefinition, ZarrV3Context 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.union(ZarrV3Context(codecs=[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=`, @@ -231,11 +231,12 @@ def acme_lz4_rules( format and file the same definitions, and equal scopes have equal hashes. `context.union(*others)` returns the smallest scope that contains each of them, or raises a `ScopeConflictError` naming each name -filed with two different definitions. `update` replaces the -definitions in the slots it is given and keeps the others, and takes a -slot as definitions or as a mapping by name, so -`context.update(codecs={**context.codecs, "gzip": MY_GZIP})` replaces -one definition on purpose. A model's `refined_in` moves it to a scope +filed with two different definitions. It is how to add definitions to a +scope, keeping the ones it has. `context.copy(update={...})`, like +pydantic's `model_copy`, replaces whole each slot it is given and keeps +the others. It takes a slot as definitions or as a mapping by name, so +`context.copy(update={"codecs": {**context.codecs, "gzip": MY_GZIP}})` +replaces one definition on purpose. A model's `refined_in` moves it to a scope that claims more names and contradicts nothing. `refines` orders two models by how much they know. A name that was unclaimed and is now read by a definition is a gain. The reverse is a loss. One name read by two diff --git a/packages/zarr-metadata/src/zarr_metadata/v2/_context.py b/packages/zarr-metadata/src/zarr_metadata/v2/_context.py index b13aa90620..4d25546a67 100644 --- a/packages/zarr-metadata/src/zarr_metadata/v2/_context.py +++ b/packages/zarr-metadata/src/zarr_metadata/v2/_context.py @@ -51,7 +51,7 @@ def is_v2_kind(kind: type[Definition[Any]]) -> TypeIs[type[ZarrV2Kind]]: class ZarrV2ContextSlots(TypedDict, total=False, closed=True): - """The definitions in each slot of a v2 scope, as `ZarrV2Context` and its `update` take them. + """The definitions in each slot of a v2 scope, as `ZarrV2Context` and its `copy` take them. This is where a v2 scope's slots are declared: each key is a slot, and its value the definitions of the kind that slot holds, as an @@ -77,13 +77,13 @@ class ZarrV2Context: document holds, as `ZarrV2ContextSlots` declares them: `data_types`, read from `dtype`, and `codecs`, read from `compressor` and `filters`. Each slot is a read-only mapping from name to definition. The - constructor and `update` take, for each slot, an iterable of + constructor and `copy` take, for each slot, an iterable of definitions or such a mapping, so one scope's slot can be given to another. A definition of another kind than its slot holds, and a key that is not its definition's name, raise `TypeError`, and two different definitions under one name in a slot raise - `ScopeConflictError`. To replace a definition, or a whole slot, use - `update`. + `ScopeConflictError`. To add definitions, use `union`. To replace a + definition, or a whole slot, use `copy`. `ZarrV2Context()` is the scope that claims nothing. Two scopes are equal when they file the same definitions in each slot, and equal scopes have equal hashes. @@ -139,26 +139,28 @@ def claimant(self, kind: type[D], name: str) -> D | None: """ return claimant_in(self._tables, kind, name) - def update(self, **slots: Unpack[ZarrV2ContextSlots]) -> ZarrV2Context: - """This scope, with each slot given here holding only the definitions given for it. The other slots are kept. + def copy(self, *, update: ZarrV2ContextSlots | None = None) -> ZarrV2Context: + """This scope, with each slot in `update` holding only the definitions given for it, as pydantic's `model_copy(update=...)` replaces each field it is given. The other slots are kept. - A slot can be given as a mapping from each definition's name to - it, so a slot of this scope passes back in changed: - `context.update(codecs={**context.codecs, "gzip": MY_GZIP})` - replaces the gzip definition and keeps the other codecs, and - `context.update(codecs=[*context.codecs.values(), MY_CODEC])` adds - one, raising `ScopeConflictError` if the scope already files - another definition under its name. + A slot given here is replaced whole: `context.copy(update={"codecs": [MY_CODEC]})` + is a scope whose only codec is `MY_CODEC`. To add definitions to a + scope, use `union`: `context.union(ZarrV2Context(codecs=[MY_CODEC]))`. + A slot can be given as a mapping from each definition's name to it, + so a slot of this scope passes back in changed: + `context.copy(update={"codecs": {**context.codecs, "gzip": MY_GZIP}})` + replaces the gzip definition and keeps the other codecs. """ - return self._of(updated(self._tables, self.kinds, slots, 2)) + return self._of(updated(self._tables, self.kinds, {} if update is None else update, 2)) def union(self, *others: ZarrV2Context) -> ZarrV2Context: """The smallest scope that files everything this scope and each of `others` file. - Raises `ScopeConflictError` when two of them file different - definitions under one name in a slot. To replace a definition on - purpose, use `update`. Raises `TypeError` for a scope of another - format. + This is how to add definitions to a scope: + `CORE_V2.union(ZarrV2Context(codecs=[MY_CODEC]))` keeps every + definition of `CORE_V2` and adds `MY_CODEC`. Raises + `ScopeConflictError` when two of them file different definitions + under one name in a slot. To replace a definition on purpose, use + `copy`. Raises `TypeError` for a scope of another format. """ refuse_others(others, ZarrV2Context) return self._of( diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/_context.py b/packages/zarr-metadata/src/zarr_metadata/v3/_context.py index 84d9ff4c21..633242498d 100644 --- a/packages/zarr-metadata/src/zarr_metadata/v3/_context.py +++ b/packages/zarr-metadata/src/zarr_metadata/v3/_context.py @@ -67,7 +67,7 @@ def is_v3_kind(kind: type[Definition[Any]]) -> TypeIs[type[ZarrV3Kind]]: class ZarrV3ContextSlots(TypedDict, total=False, closed=True): - """The definitions in each slot of a v3 scope, as `ZarrV3Context` and its `update` take them. + """The definitions in each slot of a v3 scope, as `ZarrV3Context` and its `copy` take them. This is where a v3 scope's slots are declared: each key is a slot, and its value the definitions of the kind that slot holds, as an @@ -114,13 +114,13 @@ class ZarrV3Context: `group_extra_fields` hold the definitions of members the spec does not define, by key. Each slot is a read-only mapping from the name each definition is filed under (`r*` for raw bits) to the definition. The - constructor and `update` take, for each slot, an iterable of + constructor and `copy` take, for each slot, an iterable of definitions or such a mapping, so one scope's slot can be given to another. A definition of another kind than its slot holds, and a key that is not its definition's name, raise `TypeError`, and two different definitions under one name in a slot raise - `ScopeConflictError`. To replace a definition, or a whole slot, use - `update`. + `ScopeConflictError`. To add definitions, use `union`. To replace a + definition, or a whole slot, use `copy`. `ZarrV3Context()` is the scope that claims nothing. Two scopes are equal when they file the same definitions in each slot, and equal scopes have equal hashes. @@ -201,26 +201,28 @@ def claimant(self, kind: type[D], name: str) -> D | None: """ return claimant_in(self._tables, kind, name) - def update(self, **slots: Unpack[ZarrV3ContextSlots]) -> ZarrV3Context: - """This scope, with each slot given here holding only the definitions given for it. The other slots are kept. + def copy(self, *, update: ZarrV3ContextSlots | None = None) -> ZarrV3Context: + """This scope, with each slot in `update` holding only the definitions given for it, as pydantic's `model_copy(update=...)` replaces each field it is given. The other slots are kept. - A slot can be given as a mapping from each definition's name to - it, so a slot of this scope passes back in changed: - `context.update(codecs={**context.codecs, "gzip": MY_GZIP})` - replaces the gzip definition and keeps the other codecs, and - `context.update(codecs=[*context.codecs.values(), MY_CODEC])` adds - one, raising `ScopeConflictError` if the scope already files - another definition under its name. + A slot given here is replaced whole: `context.copy(update={"codecs": [MY_CODEC]})` + is a scope whose only codec is `MY_CODEC`. To add definitions to a + scope, use `union`: `context.union(ZarrV3Context(codecs=[MY_CODEC]))`. + A slot can be given as a mapping from each definition's name to it, + so a slot of this scope passes back in changed: + `context.copy(update={"codecs": {**context.codecs, "gzip": MY_GZIP}})` + replaces the gzip definition and keeps the other codecs. """ - return self._of(updated(self._tables, self.kinds, slots, 3)) + return self._of(updated(self._tables, self.kinds, {} if update is None else update, 3)) def union(self, *others: ZarrV3Context) -> ZarrV3Context: """The smallest scope that files everything this scope and each of `others` file. - Raises `ScopeConflictError` when two of them file different - definitions under one name in a slot. To replace a definition on - purpose, use `update`. Raises `TypeError` for a scope of another - format. + This is how to add definitions to a scope: + `CORE_AND_EXTENSIONS.union(ZarrV3Context(codecs=[MY_CODEC]))` keeps every + definition of `CORE_AND_EXTENSIONS` and adds `MY_CODEC`. Raises + `ScopeConflictError` when two of them file different definitions + under one name in a slot. To replace a definition on purpose, use + `copy`. Raises `TypeError` for a scope of another format. """ refuse_others(others, ZarrV3Context) return self._of( diff --git a/packages/zarr-metadata/src/zarr_metadata/v3/_core.py b/packages/zarr-metadata/src/zarr_metadata/v3/_core.py index 5a3f52f4e4..177d319364 100644 --- a/packages/zarr-metadata/src/zarr_metadata/v3/_core.py +++ b/packages/zarr-metadata/src/zarr_metadata/v3/_core.py @@ -80,17 +80,18 @@ ) """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( - data_types=[ - *CORE.data_types.values(), - BYTES_DATA_TYPE, - STRING_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], +CORE_AND_EXTENSIONS: Final = CORE.union( + ZarrV3Context( + data_types=[ + BYTES_DATA_TYPE, + STRING_DATA_TYPE, + NUMPY_DATETIME64_DATA_TYPE, + NUMPY_TIMEDELTA64_DATA_TYPE, + STRUCT_DATA_TYPE, + ], + chunk_grids=[RECTILINEAR_CHUNK_GRID], + codecs=[CAST_VALUE_CODEC, SCALE_OFFSET_CODEC, ZSTD_CODEC], + ) ) """A scope with what the specification defines, plus the extensions that `zarr-extensions` registers and this package defines.""" diff --git a/packages/zarr-metadata/tests/model/test_consolidating.py b/packages/zarr-metadata/tests/model/test_consolidating.py index 850888d223..28bb547110 100644 --- a/packages/zarr-metadata/tests/model/test_consolidating.py +++ b/packages/zarr-metadata/tests/model/test_consolidating.py @@ -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.copy(update={"codecs": {**CORE.codecs, "bytes": MY_BYTES}}) INLINE: dict[str, Any] = {"kind": "inline", "must_understand": False} @@ -310,7 +310,7 @@ 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.copy(update={"data_types": {**CORE.data_types, "r*": strict}}), ) with pytest.raises(MetadataValidationError) as raised: _consolidated(CORE, a=child) @@ -328,7 +328,7 @@ 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.copy(update={"codecs": {**CORE.codecs, "gzip": strict}}), ) with pytest.raises(MetadataValidationError) as raised: _consolidated(CORE, a=child) diff --git a/packages/zarr-metadata/tests/model/test_construction.py b/packages/zarr-metadata/tests/model/test_construction.py index 037266de42..d2ff32fff1 100644 --- a/packages/zarr-metadata/tests/model/test_construction.py +++ b/packages/zarr-metadata/tests/model/test_construction.py @@ -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.copy( + update={"data_types": {**CORE_AND_EXTENSIONS.data_types, "int8": counting}} ) model = ZarrV3ArrayMetadata.create_default(context=context, data_type="int8", fill_value=3) assert values == [3] @@ -456,10 +456,12 @@ def counting(configuration: GzipCodecConfiguration) -> GzipCodecConfiguration: asked.append(1) return configuration - context = CORE_AND_EXTENSIONS.update( - codecs={ - **CORE_AND_EXTENSIONS.codecs, - "gzip": dataclasses.replace(GZIP_CODEC, canonical=counting), + context = CORE_AND_EXTENSIONS.copy( + update={ + "codecs": { + **CORE_AND_EXTENSIONS.codecs, + "gzip": dataclasses.replace(GZIP_CODEC, canonical=counting), + } } ) document = { @@ -489,10 +491,12 @@ 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( - codecs={ - **CORE_AND_EXTENSIONS.codecs, - "gzip": dataclasses.replace(GZIP_CODEC, canonical=refuses), + context = CORE_AND_EXTENSIONS.copy( + update={ + "codecs": { + **CORE_AND_EXTENSIONS.codecs, + "gzip": dataclasses.replace(GZIP_CODEC, canonical=refuses), + } } ) document = { diff --git a/packages/zarr-metadata/tests/model/test_extension_points.py b/packages/zarr-metadata/tests/model/test_extension_points.py index 063338e160..79852afcb1 100644 --- a/packages/zarr-metadata/tests/model/test_extension_points.py +++ b/packages/zarr-metadata/tests/model/test_extension_points.py @@ -75,7 +75,7 @@ class LenientGzipConfiguration(TypedDict, closed=True): (_document(codecs=[BYTES, {"name": "zstd", "configuration": {"level": 99}}]), CORE), ( _document(codecs=[BYTES, {"name": "gzip", "configuration": {"level": 99}}]), - CORE.update(codecs={**CORE.codecs, "gzip": LENIENT_GZIP}), + CORE.copy(update={"codecs": {**CORE.codecs, "gzip": LENIENT_GZIP}}), ), (dict(ZarrV3ArrayMetadata.create_default(shape=(0, 4)).to_json()), CORE_AND_EXTENSIONS), ], @@ -173,7 +173,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.copy(update={"codecs": {**CORE.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..7f64befc93 100644 --- a/packages/zarr-metadata/tests/model/test_extra_fields.py +++ b/packages/zarr-metadata/tests/model/test_extra_fields.py @@ -51,9 +51,7 @@ 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.union(ZarrV3Context(array_extra_fields=[ACME_TILING, ACME_NOTE])) ARRAY: dict[str, Any] = dict(ZarrV3ArrayMetadata.create_default(shape=(4,)).to_json()) GROUP: dict[str, Any] = {"zarr_format": 3, "node_type": "group"} diff --git a/packages/zarr-metadata/tests/model/test_group.py b/packages/zarr-metadata/tests/model/test_group.py index 9f62600b74..eadd91ca96 100644 --- a/packages/zarr-metadata/tests/model/test_group.py +++ b/packages/zarr-metadata/tests/model/test_group.py @@ -52,6 +52,7 @@ CORE, CORE_AND_EXTENSIONS, CodecDefinition, + ZarrV3Context, ) from zarr_metadata.v3.group import ZarrV3GroupMetadataJSON @@ -639,7 +640,7 @@ 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.union(ZarrV3Context(codecs=[ACME_X])) child = ZarrV3ArrayMetadata.create_default( context=context, shape=(4,), codecs=(LITTLE, {"name": "acme.x"}) ) @@ -859,10 +860,12 @@ def counted( reads.append(configuration) yield from () - context = CORE_AND_EXTENSIONS.update( - codecs={ - **CORE_AND_EXTENSIONS.codecs, - "gzip": dataclasses.replace(GZIP_CODEC, rules=counted), + context = CORE_AND_EXTENSIONS.copy( + update={ + "codecs": { + **CORE_AND_EXTENSIONS.codecs, + "gzip": dataclasses.replace(GZIP_CODEC, rules=counted), + } } ) array = _array(codecs=[LITTLE, {"name": "gzip", "configuration": {"level": 5}}]) diff --git a/packages/zarr-metadata/tests/model/test_pair.py b/packages/zarr-metadata/tests/model/test_pair.py index 0f1b06341e..0f9ce84ae2 100644 --- a/packages/zarr-metadata/tests/model/test_pair.py +++ b/packages/zarr-metadata/tests/model/test_pair.py @@ -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.copy(update={"codecs": {**CORE.codecs, "bytes": MY_BYTES}}) SPELLED_OUT: dict[str, Any] = { **ARRAY, "data_type": {"name": "uint8"}, @@ -183,7 +183,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.union(ZarrV3Context(codecs=[local])), ) with pytest.raises((pickle.PicklingError, AttributeError)): pickle.dumps(model) @@ -415,8 +415,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.copy( + update={"codecs": {**CORE.codecs, "bytes": dataclasses.replace(BYTES_CODEC, rules=counted)}} ) ZarrV3GroupMetadata(CONSOLIDATED, context=context) assert calls == [] @@ -612,7 +612,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.union(ZarrV3Context(codecs=[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 0b55501409..dd2907a670 100644 --- a/packages/zarr-metadata/tests/model/test_pair_v2.py +++ b/packages/zarr-metadata/tests/model/test_pair_v2.py @@ -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.copy(update={"codecs": {**CORE_V2.codecs, "zlib": BARE_ZLIB}}) def _document(changes: dict[str, Any]) -> dict[str, Any]: 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 f5174b46de..327ed49512 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 @@ -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.copy(update={"codecs": {**CORE_V2.codecs, "zlib": bare}}) ).problems assert [p.loc for p in found] == [("compressor", "level")] diff --git a/packages/zarr-metadata/tests/model/test_unclaimed_fields.py b/packages/zarr-metadata/tests/model/test_unclaimed_fields.py index cef80f6c70..80ddd3ba45 100644 --- a/packages/zarr-metadata/tests/model/test_unclaimed_fields.py +++ b/packages/zarr-metadata/tests/model/test_unclaimed_fields.py @@ -21,6 +21,7 @@ ArrayExtraFieldDefinition, GroupExtraFieldDefinition, ZarrV3CodecField, + ZarrV3Context, ) ARRAY: dict[str, Any] = dict(ZarrV3ArrayMetadata.create_default(shape=(4,)).to_json()) @@ -36,19 +37,21 @@ class _MirrorConfiguration(TypedDict, closed=True): must_understand: NotRequired[bool] -MIRRORING = CORE.update( - array_extra_fields=[ - *CORE.array_extra_fields.values(), - ArrayExtraFieldDefinition(name="acme.mirror", configuration=_MirrorConfiguration), - ] +MIRRORING = CORE.union( + ZarrV3Context( + array_extra_fields=[ + ArrayExtraFieldDefinition(name="acme.mirror", configuration=_MirrorConfiguration) + ] + ) ) """`CORE`, with a definition of an `acme.mirror` member that holds a codec.""" -GROUP_MIRRORING = CORE.update( - group_extra_fields=[ - *CORE.group_extra_fields.values(), - GroupExtraFieldDefinition(name="acme.mirror", configuration=_MirrorConfiguration), - ] +GROUP_MIRRORING = CORE.union( + ZarrV3Context( + group_extra_fields=[ + GroupExtraFieldDefinition(name="acme.mirror", configuration=_MirrorConfiguration) + ] + ) ) """`CORE`, with a definition of an `acme.mirror` member of a group that holds a codec.""" diff --git a/packages/zarr-metadata/tests/test_contexts.py b/packages/zarr-metadata/tests/test_contexts.py index ef8f408727..9a0e89c81b 100644 --- a/packages/zarr-metadata/tests/test_contexts.py +++ b/packages/zarr-metadata/tests/test_contexts.py @@ -54,7 +54,7 @@ def test_the_scope_of_each_format_behaves_alike( gzip: Any, # noqa: ANN401 - either gzip definition zarr_format: int, ) -> None: - """Both scope classes implement `Context` in the same way: the format they read, `claimant`, `definitions_of`, a view per slot, `update` replacing a slot, `union`, equality and hashing by what each slot files, a short repr, and pickling and copying. A field is read in either one with `resolve`.""" + """Both scope classes implement `Context` in the same way: the format they read, `claimant`, `definitions_of`, a view per slot, `copy` replacing a slot, `union`, equality and hashing by what each slot files, a short repr, and pickling and copying. A field is read in either one with `resolve`.""" built = cls(codecs=[gzip]) assert built.zarr_format == zarr_format assert built.claimant(kind, "gzip") is gzip @@ -64,7 +64,7 @@ def test_the_scope_of_each_format_behaves_alike( assert hash(built) == hash(cls(codecs=[gzip])) assert built != cls() assert repr(built) == f"{cls.__name__}(<1 definitions>)" - replaced = built.update(codecs=[_other(gzip)]) + replaced = built.copy(update={"codecs": [_other(gzip)]}) assert replaced.claimant(kind, "gzip") is not gzip assert built.union(cls()) == built assert built.union() == built @@ -76,28 +76,44 @@ def test_the_scope_of_each_format_behaves_alike( @pytest.mark.parametrize(("cls", "core", "kind", "gzip", "zarr_format"), FORMATS) -def test_update_replaces_each_slot_it_is_given_and_keeps_the_rest( +def test_copy_replaces_each_slot_it_is_given_and_keeps_the_rest( cls: Any, # noqa: ANN401 - either scope class core: Any, # noqa: ANN401 - either core scope kind: Any, # noqa: ANN401 - either codec kind gzip: Any, # noqa: ANN401 - either gzip definition zarr_format: int, ) -> None: - """`update` returns the scope with each slot it is given replaced whole, and every other slot kept. A slot is given as definitions, or as a mapping from each definition's name to it, so a slot of a scope passes back in as it is, or changed by name. The scope it is called on is unchanged.""" - only = core.update(codecs=[gzip]) + """`copy(update=...)`, like pydantic's `model_copy`, returns the scope with each slot it is given replaced whole, and every other slot kept. A slot is given as definitions, or as a mapping from each definition's name to it, so a slot of a scope passes back in as it is, or changed by name. The scope it is called on is unchanged.""" + only = core.copy(update={"codecs": [gzip]}) assert dict(only.codecs) == {"gzip": gzip} assert only.data_types == core.data_types - assert core.update() == core - assert core.update(codecs=core.codecs) == core + assert core.copy() == core + assert core.copy(update={}) == core + assert core.copy(update={"codecs": core.codecs}) == core assert cls(codecs={"gzip": gzip}) == cls(codecs=[gzip]) other = _other(gzip) - replaced = core.update(codecs={**core.codecs, "gzip": other}) + replaced = core.copy(update={"codecs": {**core.codecs, "gzip": other}}) assert replaced.codecs["gzip"] is other assert {**replaced.codecs, "gzip": gzip} == dict(core.codecs) - assert dict(cls().update(codecs=[*cls().codecs.values(), gzip]).codecs) == {"gzip": gzip} assert core.codecs["gzip"] is gzip +@pytest.mark.parametrize(("cls", "core", "kind", "gzip", "zarr_format"), FORMATS) +def test_union_adds_definitions_and_keeps_every_one_the_scope_has( + cls: Any, # noqa: ANN401 - either scope class + core: Any, # noqa: ANN401 - either core scope + kind: Any, # noqa: ANN401 - either codec kind + gzip: Any, # noqa: ANN401 - either gzip definition + zarr_format: int, +) -> None: + """Adding definitions to a scope is a `union` with a scope of them: every definition the scope has is kept, in every slot. A definition the scope already files under its name is no conflict.""" + added = dataclasses.replace(gzip, name="acme.gzip") + extended = core.union(cls(codecs=[added, gzip])) + assert dict(extended.codecs) == {**core.codecs, "acme.gzip": added} + assert extended.data_types == core.data_types + assert dict(cls().union(cls(codecs=[gzip])).codecs) == {"gzip": gzip} + + @pytest.mark.parametrize(("cls", "core", "kind", "gzip", "zarr_format"), FORMATS) def test_error_a_slot_given_as_a_mapping_files_each_definition_under_its_name( cls: Any, # noqa: ANN401 - either scope class @@ -106,13 +122,13 @@ def test_error_a_slot_given_as_a_mapping_files_each_definition_under_its_name( gzip: Any, # noqa: ANN401 - either gzip definition zarr_format: int, ) -> None: - """A slot given as a mapping keys each definition by its own name, the name the scope files it under. Another key raises `TypeError` naming both, in the constructor and in `update`, rather than filing the definition under a name it does not read, or ignoring a misspelled key.""" + """A slot given as a mapping keys each definition by its own name, the name the scope files it under. Another key raises `TypeError` naming both, in the constructor and in `copy`, rather than filing the definition under a name it does not read, or ignoring a misspelled key.""" with pytest.raises( TypeError, match="codecs files each definition under its name, got 'gzip' under 'zstd'" ): cls(codecs={"zstd": gzip}) with pytest.raises(TypeError, match="got 'gzip' under 'gzipp'"): - core.update(codecs={**core.codecs, "gzipp": _other(gzip)}) + core.copy(update={"codecs": {**core.codecs, "gzipp": _other(gzip)}}) @pytest.mark.parametrize(("cls", "core", "kind", "gzip", "zarr_format"), FORMATS) @@ -123,11 +139,11 @@ def test_error_a_scope_has_only_the_slots_it_declares( gzip: Any, # noqa: ANN401 - either gzip definition zarr_format: int, ) -> None: - """A slot the scope does not declare raises `TypeError` naming it, in the constructor and in `update`, as a type checker reports it.""" + """A slot the scope does not declare raises `TypeError` naming it, in the constructor and in `copy`, as a type checker reports it.""" with pytest.raises(TypeError, match=f"a v{zarr_format} scope has no slot 'codec'"): cls(codec=[gzip]) with pytest.raises(TypeError, match=f"a v{zarr_format} scope has no slot 'codec'"): - core.update(codec=[gzip]) + core.copy(update={"codec": [gzip]}) @pytest.mark.parametrize(("cls", "core", "kind", "gzip", "zarr_format"), FORMATS) diff --git a/packages/zarr-metadata/tests/test_problem_data.py b/packages/zarr-metadata/tests/test_problem_data.py index c2ecafacb6..8d8f289ddd 100644 --- a/packages/zarr-metadata/tests/test_problem_data.py +++ b/packages/zarr-metadata/tests/test_problem_data.py @@ -316,10 +316,12 @@ 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( - data_types={ - **CORE_AND_EXTENSIONS.data_types, - "r*": DataTypeDefinition(name="r*", configuration=WideBits), + context = CORE_AND_EXTENSIONS.copy( + update={ + "data_types": { + **CORE_AND_EXTENSIONS.data_types, + "r*": DataTypeDefinition(name="r*", configuration=WideBits), + } } ) _, problems = resolve("r24", DataTypeDefinition, context, ("data_type",)) diff --git a/packages/zarr-metadata/tests/v3/test_definitions.py b/packages/zarr-metadata/tests/v3/test_definitions.py index 90f3f809ac..ce5e6e2542 100644 --- a/packages/zarr-metadata/tests/v3/test_definitions.py +++ b/packages/zarr-metadata/tests/v3/test_definitions.py @@ -201,18 +201,19 @@ def acme_paired_rules( rules=acme_paired_rules, ) -CONTEXT = CORE_AND_EXTENSIONS.update( - codecs=[ - *CORE_AND_EXTENSIONS.codecs.values(), - ACME_STACK, - ACME_LEVEL, - ACME_TOTAL, - ACME_TREE, - ACME_FALLBACK, - ACME_ROUTES, - ACME_BOUNDED, - ACME_PAIRED, - ] +CONTEXT = CORE_AND_EXTENSIONS.union( + ZarrV3Context( + codecs=[ + ACME_STACK, + ACME_LEVEL, + ACME_TOTAL, + ACME_TREE, + ACME_FALLBACK, + ACME_ROUTES, + ACME_BOUNDED, + ACME_PAIRED, + ] + ) ) @@ -774,7 +775,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.union(ZarrV3Context(codecs=[lying])) def test_error_a_rule_that_yields_something_else() -> None: @@ -1126,7 +1127,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.copy( + update={"codecs": {**CORE_AND_EXTENSIONS.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 @@ -1573,8 +1576,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.copy( + update={"codecs": {**CORE.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 +1601,13 @@ 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.copy( + update={ + "codecs": { + **CORE.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 +1618,10 @@ 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.copy( + update={ + "codecs": {**CORE.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: diff --git a/packages/zarr-metadata/tests/v3/test_every_definition.py b/packages/zarr-metadata/tests/v3/test_every_definition.py index f881aa5b3c..4dc5c48289 100644 --- a/packages/zarr-metadata/tests/v3/test_every_definition.py +++ b/packages/zarr-metadata/tests/v3/test_every_definition.py @@ -371,7 +371,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.copy( + update={"data_types": {**CORE_AND_EXTENSIONS.data_types, "r*": mine}} + ) resolved, problems = resolve("r12", DataTypeDefinition, context) assert problems == () assert isinstance(resolved, AcceptedField) @@ -380,7 +382,7 @@ def test_a_reader_reads_raw_bits_its_own_way_by_defining_r_star() -> None: again, _ = resolve( "r12", DataTypeDefinition, - context.update(data_types={**context.data_types, "r*": RAW_BYTES_DATA_TYPE}), + context.copy(update={"data_types": {**context.data_types, "r*": RAW_BYTES_DATA_TYPE}}), ) assert isinstance(again, RefusedField) assert again.definition is RAW_BYTES_DATA_TYPE @@ -508,7 +510,7 @@ 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.union(ZarrV3Context(codecs=[lying])), ) diff --git a/packages/zarr-metadata/tests/v3/test_fill_values.py b/packages/zarr-metadata/tests/v3/test_fill_values.py index 27436365b3..0b464d29f1 100644 --- a/packages/zarr-metadata/tests/v3/test_fill_values.py +++ b/packages/zarr-metadata/tests/v3/test_fill_values.py @@ -42,6 +42,7 @@ from zarr_metadata.v3.definition import ( CORE_AND_EXTENSIONS, DataTypeDefinition, + ZarrV3Context, ) if TYPE_CHECKING: @@ -288,9 +289,7 @@ 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.union(ZarrV3Context(data_types=[ACME_POINT])) resolved, _ = resolve("acme.point", DataTypeDefinition, context) found = fill_value_problems(resolved, {"x": 99, "extra": 1}) assert [(problem.loc, problem.kind) for problem in found] == [ @@ -461,11 +460,8 @@ 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( - data_types=[ - *CORE_AND_EXTENSIONS.data_types.values(), - dataclasses.replace(ACME_POINT, fill_value_canonical=refuses), - ] + context = CORE_AND_EXTENSIONS.union( + ZarrV3Context(data_types=[dataclasses.replace(ACME_POINT, fill_value_canonical=refuses)]) ) resolved, _ = resolve("acme.point", DataTypeDefinition, context) with pytest.raises(ValueError, match="no") as raised: @@ -477,11 +473,8 @@ 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( - data_types=[ - *CORE_AND_EXTENSIONS.data_types.values(), - dataclasses.replace(ACME_POINT, fill_value_canonical=not_json), - ] + context = CORE_AND_EXTENSIONS.union( + ZarrV3Context(data_types=[dataclasses.replace(ACME_POINT, fill_value_canonical=not_json)]) ) resolved, _ = resolve("acme.point", DataTypeDefinition, context) with pytest.raises(TypeError, match="'acme.point': its fill_value_canonical gives 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..394e63ec21 100644 --- a/packages/zarr-metadata/tests/v3/test_grid_shapes.py +++ b/packages/zarr-metadata/tests/v3/test_grid_shapes.py @@ -24,6 +24,7 @@ from zarr_metadata.v3.definition import ( CORE_AND_EXTENSIONS, ChunkGridDefinition, + ZarrV3Context, ) if TYPE_CHECKING: @@ -103,9 +104,7 @@ 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.union(ZarrV3Context(chunk_grids=[lenient])) resolved, _ = resolve("acme.grid", ChunkGridDefinition, context) assert chunk_grid_lengths(resolved, (3, 4)) == ((None, None), ()) @@ -117,9 +116,7 @@ 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.union(ZarrV3Context(chunk_grids=[flat])) resolved, _ = resolve("acme.grid", ChunkGridDefinition, context) with pytest.raises(ValueError, match="'acme.grid': its chunk_lengths gave 1 axes"): chunk_grid_lengths(resolved, (3, 4)) @@ -162,7 +159,7 @@ 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.union(ZarrV3Context(chunk_grids=[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..99b1467f30 100644 --- a/packages/zarr-metadata/tests/v3/test_kinds.py +++ b/packages/zarr-metadata/tests/v3/test_kinds.py @@ -106,7 +106,7 @@ 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.union(ZarrV3Context(data_types=[UINT_DATA_TYPE_V2])) # pyright: ignore[reportArgumentType] with pytest.raises(TypeError, match="format"): CORE.union(CORE_V2) # pyright: ignore[reportArgumentType] diff --git a/packages/zarr-metadata/tests/v3/test_pipelines.py b/packages/zarr-metadata/tests/v3/test_pipelines.py index 8361221e66..59d008d5c5 100644 --- a/packages/zarr-metadata/tests/v3/test_pipelines.py +++ b/packages/zarr-metadata/tests/v3/test_pipelines.py @@ -32,6 +32,7 @@ CodecDefinition, DataTypeDefinition, ZarrV3CodecField, + ZarrV3Context, ZarrV3DataTypeField, ) @@ -64,9 +65,7 @@ def _tiled(configuration: EmptyConfiguration, nested: Nested, chunk: Chunk) -> 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.union(ZarrV3Context(codecs=[ACME_TILE, ACME_SHUFFLE])) UINT8 = resolve("uint8", DataTypeDefinition, CONTEXT)[0] @@ -357,9 +356,7 @@ def test_error_a_transition_that_gives_something_else() -> None: size="static", transition=lambda configuration, nested, chunk: "a chunk", # pyright: ignore[reportArgumentType] ) - codec = resolve( - "acme.lying", CodecDefinition, CONTEXT.update(codecs=[*CONTEXT.codecs.values(), lying]) - )[0] + codec = resolve("acme.lying", CodecDefinition, CONTEXT.union(ZarrV3Context(codecs=[lying])))[0] with pytest.raises(TypeError, match="'acme.lying': its transition gives a Chunk"): read_pipeline([codec], CHUNK) @@ -376,7 +373,7 @@ def transition(configuration: EmptyConfiguration, nested: Nested, chunk: Chunk) transition=transition, ) codec = resolve( - "acme.raising", CodecDefinition, CONTEXT.update(codecs=[*CONTEXT.codecs.values(), raising]) + "acme.raising", CodecDefinition, CONTEXT.union(ZarrV3Context(codecs=[raising])) )[0] with pytest.raises(KeyError) as raised: read_pipeline([codec], CHUNK, ("codecs",)) @@ -408,9 +405,7 @@ def transition(configuration: EmptyConfiguration, nested: Nested, chunk: Chunk) size="static", transition=transition, ) - codec = resolve( - "acme.odd", CodecDefinition, CONTEXT.update(codecs=[*CONTEXT.codecs.values(), odd]) - )[0] + codec = resolve("acme.odd", CodecDefinition, CONTEXT.union(ZarrV3Context(codecs=[odd])))[0] with pytest.raises(TypeError, match=match) as raised: read_pipeline([codec], CHUNK, ("codecs",)) assert raised.value.__notes__ == [ @@ -491,9 +486,7 @@ def _holder( pipelines=pipelines, # pyright: ignore[reportArgumentType] ) field = {"name": "acme.holder", "configuration": {"codecs": [LITTLE], "types": types}} - return resolve( - field, CodecDefinition, CONTEXT.update(codecs=[*CONTEXT.codecs.values(), holder]) - )[0] + return resolve(field, CodecDefinition, CONTEXT.union(ZarrV3Context(codecs=[holder])))[0] @pytest.mark.parametrize("given", ["a chunk", {"codecs": "a chunk"}, {("codecs",): Chunk()}]) diff --git a/packages/zarr-metadata/tests/v3/test_scope.py b/packages/zarr-metadata/tests/v3/test_scope.py index a99f1ac2df..37d7e57cfe 100644 --- a/packages/zarr-metadata/tests/v3/test_scope.py +++ b/packages/zarr-metadata/tests/v3/test_scope.py @@ -466,7 +466,9 @@ def test_a_conflict_names_the_field_as_the_document_writes_it_and_tells_the_defi 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.copy( + update={"data_types": {**CORE_AND_EXTENSIONS.data_types, "r*": other}} + ) ) message = str(raised.value) assert message.startswith("data type 'r16'") diff --git a/packages/zarr-metadata/tests/v3/test_storage.py b/packages/zarr-metadata/tests/v3/test_storage.py index fadcf36049..ece38c697e 100644 --- a/packages/zarr-metadata/tests/v3/test_storage.py +++ b/packages/zarr-metadata/tests/v3/test_storage.py @@ -21,6 +21,7 @@ CORE_AND_EXTENSIONS, DataTypeDefinition, StorageClass, + ZarrV3Context, storage_of, ) @@ -85,7 +86,7 @@ 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.union(ZarrV3Context(data_types=[quiet])), ) assert storage_of(resolved) is None @@ -99,7 +100,7 @@ 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.union(ZarrV3Context(data_types=[lying])), ) with pytest.raises(TypeError, match="'acme.lying': its storage gives one of"): storage_of(resolved) @@ -115,7 +116,7 @@ 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.union(ZarrV3Context(data_types=[raising])), ) with pytest.raises(KeyError) as raised: storage_of(resolved)