Skip to content

Commit 41a528f

Browse files
committed
refactor(zarr-metadata): make every public type name parse against a naming grammar
Three grammars now cover the public surface, enforced by a conformance test that walks every public module's __all__: - core document/model names: ZarrV{2,3} + entity + optional role suffix (JSON / JSONPartial / Partial / StoreKey) - extension-entity names: registered entity + exactly one role suffix (CodecMetadata, DataTypeName, FillValue, ...) - a closed standalone-vocabulary allowlist for role-less scalar and diagnostic types, with a staleness guard The three .z-file document types were the only names that fit no grammar and are renamed: ZArrayMetadata -> ZarrV2ZArrayJSON, ZGroupMetadata -> ZarrV2ZGroupJSON, ZAttrsMetadata -> ZarrV2ZAttrsJSON. The leading V2 of V2ChunkKeyEncodingMetadata is that encoding's registered entity name, not a format version; its module docstring now says so. Assisted-by: ClaudeCode:claude-fable-5
1 parent f8cd675 commit 41a528f

11 files changed

Lines changed: 160 additions & 40 deletions

File tree

packages/zarr-metadata/changes/4119.removal.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ entity names are reserved for the `zarr_metadata.model` dataclasses:
1717
- `DataTypeMetadataV2``ZarrV2DataTypeMetadata`
1818
- `ArrayOrderV2``ZarrV2ArrayOrder`
1919
- `ArrayDimensionSeparatorV2``ZarrV2ArrayDimensionSeparator`
20+
- `ZArrayMetadata``ZarrV2ZArrayJSON` (the strict on-disk `.zarray` document)
21+
- `ZGroupMetadata``ZarrV2ZGroupJSON` (the strict on-disk `.zgroup` document)
22+
- `ZAttrsMetadata``ZarrV2ZAttrsJSON` (the `.zattrs` document)
2023

2124
The old names are removed, not aliased. The `zarr_metadata.pydantic` field
2225
types take the bare entity names (`ZarrV3ArrayMetadata`, ...), matching the
@@ -30,3 +33,9 @@ snake_case functions put it last (`ARRAY_METADATA_STORE_KEY_V2`,
3033
whose bare name is taken by (or reserved for) a `zarr_metadata.model`
3134
dataclass; raw field-level types the models hold verbatim
3235
(`ZarrV2CodecMetadata`, `ZarrV3ExtensionField`) keep their bare names.
36+
Extension-entity types put the registered entity name first and end in
37+
exactly one role suffix (`BloscCodecMetadata`, `Uint8DataTypeName`) — the
38+
`V2` in `V2ChunkKeyEncodingMetadata` is that encoding's entity name, not a
39+
format version, which is always spelled `ZarrV2`/`ZarrV3`. Every public
40+
type name is checked against this grammar by
41+
`tests/test_public_api.py::test_public_type_names_comply_with_naming_grammar`.

packages/zarr-metadata/src/zarr_metadata/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,20 @@
2222
from zarr_metadata.v2.array import (
2323
ARRAY_DIMENSION_SEPARATOR_V2,
2424
ARRAY_ORDER_V2,
25-
ZArrayMetadata,
2625
ZarrV2ArrayDimensionSeparator,
2726
ZarrV2ArrayMetadataJSON,
2827
ZarrV2ArrayMetadataJSONPartial,
2928
ZarrV2ArrayOrder,
3029
ZarrV2DataTypeMetadata,
30+
ZarrV2ZArrayJSON,
3131
)
32-
from zarr_metadata.v2.attributes import ZAttrsMetadata
32+
from zarr_metadata.v2.attributes import ZarrV2ZAttrsJSON
3333
from zarr_metadata.v2.codec import ZarrV2CodecMetadata
3434
from zarr_metadata.v2.consolidated import ZarrV2ConsolidatedMetadataJSON
3535
from zarr_metadata.v2.group import (
3636
ZarrV2GroupMetadataJSON,
3737
ZarrV2GroupMetadataJSONPartial,
38-
ZGroupMetadata,
38+
ZarrV2ZGroupJSON,
3939
)
4040
from zarr_metadata.v3._common import ZarrV3MetadataFieldJSON
4141
from zarr_metadata.v3.array import (
@@ -338,9 +338,6 @@
338338
"V2ChunkKeyEncodingName",
339339
"V2ChunkKeyEncodingSeparator",
340340
"ValidationProblem",
341-
"ZArrayMetadata",
342-
"ZAttrsMetadata",
343-
"ZGroupMetadata",
344341
"ZarrV2ArrayDimensionSeparator",
345342
"ZarrV2ArrayMetadata",
346343
"ZarrV2ArrayMetadataJSON",
@@ -355,6 +352,9 @@
355352
"ZarrV2GroupMetadataJSON",
356353
"ZarrV2GroupMetadataJSONPartial",
357354
"ZarrV2GroupMetadataPartial",
355+
"ZarrV2ZArrayJSON",
356+
"ZarrV2ZAttrsJSON",
357+
"ZarrV2ZGroupJSON",
358358
"ZarrV3ArrayMetadata",
359359
"ZarrV3ArrayMetadataJSON",
360360
"ZarrV3ArrayMetadataJSONPartial",
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
"""Zarr v2 metadata types."""
22

33
from zarr_metadata.v2.array import (
4-
ZArrayMetadata,
54
ZarrV2ArrayDimensionSeparator,
65
ZarrV2ArrayMetadataJSON,
76
ZarrV2ArrayOrder,
87
ZarrV2DataTypeMetadata,
8+
ZarrV2ZArrayJSON,
99
)
10-
from zarr_metadata.v2.attributes import ZAttrsMetadata
10+
from zarr_metadata.v2.attributes import ZarrV2ZAttrsJSON
1111
from zarr_metadata.v2.codec import ZarrV2CodecMetadata
1212
from zarr_metadata.v2.consolidated import ZarrV2ConsolidatedMetadataJSON
13-
from zarr_metadata.v2.group import ZarrV2GroupMetadataJSON, ZGroupMetadata
13+
from zarr_metadata.v2.group import ZarrV2GroupMetadataJSON, ZarrV2ZGroupJSON
1414

1515
__all__ = [
16-
"ZArrayMetadata",
17-
"ZAttrsMetadata",
18-
"ZGroupMetadata",
1916
"ZarrV2ArrayDimensionSeparator",
2017
"ZarrV2ArrayMetadataJSON",
2118
"ZarrV2ArrayOrder",
2219
"ZarrV2CodecMetadata",
2320
"ZarrV2ConsolidatedMetadataJSON",
2421
"ZarrV2DataTypeMetadata",
2522
"ZarrV2GroupMetadataJSON",
23+
"ZarrV2ZArrayJSON",
24+
"ZarrV2ZAttrsJSON",
25+
"ZarrV2ZGroupJSON",
2626
]

packages/zarr-metadata/src/zarr_metadata/v2/array.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@
5555
"""Tuple of permitted values for the `dimension_separator` field of v2 array metadata."""
5656

5757

58-
class ZArrayMetadata(TypedDict):
58+
class ZarrV2ZArrayJSON(TypedDict):
5959
"""
6060
On-disk `.zarray` file content.
6161
6262
Strict shape of the JSON document persisted at `<path>/.zarray` for
6363
a v2 array. User attributes live in a sibling `.zattrs` file and are
64-
NOT part of this type; see `ZAttrsMetadata`.
64+
NOT part of this type; see `ZarrV2ZAttrsJSON`.
6565
6666
See https://zarr-specs.readthedocs.io/en/latest/v2/v2.0.html
6767
"""
@@ -87,7 +87,7 @@ class ZarrV2ArrayMetadataJSON(TypedDict):
8787
`attributes` field so a single TypedDict represents the complete
8888
in-memory state of a v2 array node. Consumers that read or write a
8989
real `.zarray` file should split / merge `attributes` accordingly,
90-
or use `ZArrayMetadata` (strict on-disk) plus `ZAttrsMetadata` directly.
90+
or use `ZarrV2ZArrayJSON` (strict on-disk) plus `ZarrV2ZAttrsJSON` directly.
9191
9292
See https://zarr-specs.readthedocs.io/en/latest/v2/v2.0.html
9393
"""
@@ -152,10 +152,10 @@ class ZarrV2ArrayMetadataJSONPartial(TypedDict, total=False):
152152
__all__ = [
153153
"ARRAY_DIMENSION_SEPARATOR_V2",
154154
"ARRAY_ORDER_V2",
155-
"ZArrayMetadata",
156155
"ZarrV2ArrayDimensionSeparator",
157156
"ZarrV2ArrayMetadataJSON",
158157
"ZarrV2ArrayMetadataJSONPartial",
159158
"ZarrV2ArrayOrder",
160159
"ZarrV2DataTypeMetadata",
160+
"ZarrV2ZArrayJSON",
161161
]

packages/zarr-metadata/src/zarr_metadata/v2/attributes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77

88
from zarr_metadata._common import JSONValue
99

10-
ZAttrsMetadata = Mapping[str, JSONValue]
10+
ZarrV2ZAttrsJSON = Mapping[str, JSONValue]
1111
"""On-disk `.zattrs` file content.
1212
1313
A JSON object holding user-defined attributes for a v2 array or group.
1414
Spec-defined keys for arrays / groups live in sibling `.zarray` / `.zgroup`
15-
files (modeled by `ZArrayMetadata` / `ZGroupMetadata`). This type does not
15+
files (modeled by `ZarrV2ZArrayJSON` / `ZarrV2ZGroupJSON`). This type does not
1616
constrain the keys or values of the attributes mapping.
1717
"""
1818

1919

2020
__all__ = [
21-
"ZAttrsMetadata",
21+
"ZarrV2ZAttrsJSON",
2222
]

packages/zarr-metadata/src/zarr_metadata/v2/consolidated.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010

1111
from typing_extensions import TypedDict
1212

13-
from zarr_metadata.v2.array import ZArrayMetadata
14-
from zarr_metadata.v2.attributes import ZAttrsMetadata
15-
from zarr_metadata.v2.group import ZGroupMetadata
13+
from zarr_metadata.v2.array import ZarrV2ZArrayJSON
14+
from zarr_metadata.v2.attributes import ZarrV2ZAttrsJSON
15+
from zarr_metadata.v2.group import ZarrV2ZGroupJSON
1616

1717

1818
class ZarrV2ConsolidatedMetadataJSON(TypedDict):
@@ -24,17 +24,17 @@ class ZarrV2ConsolidatedMetadataJSON(TypedDict):
2424
that path. The keys include the filename suffix, not just the node
2525
path; the value's shape is determined by which file the key points at:
2626
27-
- `<path>/.zarray` -> `ZArrayMetadata`
28-
- `<path>/.zgroup` -> `ZGroupMetadata`
29-
- `<path>/.zattrs` -> `ZAttrsMetadata`
27+
- `<path>/.zarray` -> `ZarrV2ZArrayJSON`
28+
- `<path>/.zgroup` -> `ZarrV2ZGroupJSON`
29+
- `<path>/.zattrs` -> `ZarrV2ZAttrsJSON`
3030
3131
The TypedDict cannot discriminate the value shape on the key suffix
3232
at the type level; consumers should narrow at runtime by inspecting
3333
`key.endswith(".zarray")` etc.
3434
"""
3535

3636
zarr_consolidated_format: int
37-
metadata: Mapping[str, ZArrayMetadata | ZGroupMetadata | ZAttrsMetadata]
37+
metadata: Mapping[str, ZarrV2ZArrayJSON | ZarrV2ZGroupJSON | ZarrV2ZAttrsJSON]
3838

3939

4040
__all__ = [

packages/zarr-metadata/src/zarr_metadata/v2/group.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
from zarr_metadata._common import JSONValue
1212

1313

14-
class ZGroupMetadata(TypedDict):
14+
class ZarrV2ZGroupJSON(TypedDict):
1515
"""
1616
On-disk `.zgroup` file content.
1717
1818
Strict shape of the JSON document persisted at `<path>/.zgroup` for
1919
a v2 group. The spec defines exactly one field. User attributes live
2020
in a sibling `.zattrs` file and are NOT part of this type; see
21-
`ZAttrsMetadata`.
21+
`ZarrV2ZAttrsJSON`.
2222
2323
See https://zarr-specs.readthedocs.io/en/latest/v2/v2.0.html
2424
"""
@@ -34,8 +34,8 @@ class ZarrV2GroupMetadataJSON(TypedDict):
3434
and `.zattrs` (user attributes). On disk these are persisted as two
3535
separate files; this type folds them so a single TypedDict represents
3636
the complete in-memory state of a v2 group node. Consumers that read
37-
or write the real on-disk files should use `ZGroupMetadata` (strict
38-
`.zgroup`) plus `ZAttrsMetadata` directly.
37+
or write the real on-disk files should use `ZarrV2ZGroupJSON` (strict
38+
`.zgroup`) plus `ZarrV2ZAttrsJSON` directly.
3939
4040
See https://zarr-specs.readthedocs.io/en/latest/v2/v2.0.html
4141
"""
@@ -75,7 +75,7 @@ class ZarrV2GroupMetadataJSONPartial(TypedDict, total=False):
7575

7676

7777
__all__ = [
78-
"ZGroupMetadata",
7978
"ZarrV2GroupMetadataJSON",
8079
"ZarrV2GroupMetadataJSONPartial",
80+
"ZarrV2ZGroupJSON",
8181
]

packages/zarr-metadata/src/zarr_metadata/v3/chunk_key_encoding/v2.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
Intended only to allow existing v2 arrays to be converted to v3 without
55
having to rename chunks. Not recommended for new arrays.
66
7+
Naming note: these are Zarr **v3** types. The leading `V2` in
8+
`V2ChunkKeyEncodingMetadata` (and friends) is the encoding's registered
9+
*entity name* (`"v2"`), not the format-version marker that `ZarrV2...`
10+
names carry — this package's version-prefixed names always spell it
11+
`ZarrV2` / `ZarrV3`.
12+
713
See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-key-encoding
814
"""
915

packages/zarr-metadata/tests/test_public_api.py

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Test that the curated front-door names are accessible from the top-level zarr_metadata package."""
22

3+
import importlib
4+
import pkgutil
35
import re
46
from typing import get_args
57

@@ -23,12 +25,12 @@ def _group_rank(s: str) -> int:
2325
# Category A — metadata-document types
2426
"ZarrV2ArrayMetadataJSON",
2527
"ZarrV2ArrayMetadataJSONPartial",
26-
"ZArrayMetadata",
28+
"ZarrV2ZArrayJSON",
2729
"ZarrV2GroupMetadataJSON",
2830
"ZarrV2GroupMetadataJSONPartial",
29-
"ZGroupMetadata",
31+
"ZarrV2ZGroupJSON",
3032
"ZarrV2ConsolidatedMetadataJSON",
31-
"ZAttrsMetadata",
33+
"ZarrV2ZAttrsJSON",
3234
"ZarrV2CodecMetadata",
3335
"ZarrV3ArrayMetadataJSON",
3436
"ZarrV3ArrayMetadataJSONPartial",
@@ -217,6 +219,109 @@ def test_all_is_grouped_and_unique() -> None:
217219
assert len(zm.__all__) == len(set(zm.__all__))
218220

219221

222+
# --- naming grammar ---------------------------------------------------------
223+
224+
# Core document/model names: the format version comes first (`ZarrV2` /
225+
# `ZarrV3`), then the CamelCase entity, then an optional role suffix
226+
# (`JSON`, `JSONPartial`, `Partial`, `StoreKey`) — validated loosely here
227+
# because `JSON` decomposes into single-letter words under any strict
228+
# word-splitting regex.
229+
_CORE_NAME = re.compile(r"^ZarrV[23](?:[A-Z][a-z0-9]*)+$")
230+
231+
# Zarr v3 extension-entity names: the registered entity comes first (`Blosc`,
232+
# `Uint8`, ... — `V2` here is the *entity name* of the v2-compatibility chunk
233+
# key encoding, not a format-version marker, which is always spelled
234+
# `ZarrV2`/`ZarrV3`), followed by exactly one role suffix.
235+
_EXTENSION_ROLES = (
236+
"CodecConfiguration",
237+
"CodecMetadata",
238+
"CodecName",
239+
"CodecObject",
240+
"ChunkGridConfiguration",
241+
"ChunkGridMetadata",
242+
"ChunkGridName",
243+
"ChunkGridObject",
244+
"ChunkKeyEncodingConfiguration",
245+
"ChunkKeyEncodingMetadata",
246+
"ChunkKeyEncodingName",
247+
"ChunkKeyEncodingObject",
248+
"ChunkKeyEncodingSeparator",
249+
"DataTypeName",
250+
"FillValue",
251+
"Configuration",
252+
"Component",
253+
)
254+
_EXTENSION_NAME = re.compile(r"^(?:[A-Z][a-z0-9]*)+?(?:" + "|".join(_EXTENSION_ROLES) + r")$")
255+
256+
# Standalone vocabulary: scalar Literal aliases, structural helper shapes, and
257+
# the validation diagnostics. Closed by hand — a new name belongs here only if
258+
# it is genuinely role-less; anything document- or entity-shaped must fit the
259+
# grammars above instead.
260+
_STANDALONE_VOCAB = frozenset(
261+
{
262+
"Base64Bytes",
263+
"BloscCName",
264+
"BloscShuffle",
265+
"CastOutOfRangeMode",
266+
"CastRoundingMode",
267+
"Endianness",
268+
"HexFloat16",
269+
"HexFloat32",
270+
"HexFloat64",
271+
"JSONValue",
272+
"MetadataValidationError",
273+
"NumpyDatetime64",
274+
"NumpyTimeUnit",
275+
"NumpyTimedelta64",
276+
"ProblemKind",
277+
"RectilinearDimSpec",
278+
"ScalarMap",
279+
"ScalarMapEntry",
280+
"ShardingIndexLocation",
281+
"Struct",
282+
"StructField",
283+
"ValidationProblem",
284+
}
285+
)
286+
287+
288+
def _public_type_names() -> set[tuple[str, str]]:
289+
"""Every (module, CamelCase name) pair exported via a public `__all__`."""
290+
module_names = {"zarr_metadata"}
291+
for info in pkgutil.walk_packages(zm.__path__, prefix="zarr_metadata."):
292+
if not any(part.startswith("_") for part in info.name.split(".")[1:]):
293+
module_names.add(info.name)
294+
out: set[tuple[str, str]] = set()
295+
for module_name in module_names:
296+
module = importlib.import_module(module_name)
297+
for name in getattr(module, "__all__", ()):
298+
if name.startswith("_") or name.isupper() or name.islower():
299+
continue
300+
out.add((module_name, name))
301+
return out
302+
303+
304+
def test_public_type_names_comply_with_naming_grammar() -> None:
305+
"""Every public type name parses against the package naming grammar:
306+
version-first core names, entity-plus-role extension names, or the closed
307+
standalone vocabulary."""
308+
exported = _public_type_names()
309+
violations = [
310+
f"{module}.{name}"
311+
for module, name in sorted(exported)
312+
if name not in _STANDALONE_VOCAB
313+
and not _CORE_NAME.match(name)
314+
and not _EXTENSION_NAME.match(name)
315+
]
316+
assert not violations, f"names outside the naming grammar: {violations}"
317+
318+
319+
def test_standalone_vocab_is_not_stale() -> None:
320+
"""Every allowlisted vocabulary name is still actually exported."""
321+
exported_names = {name for _, name in _public_type_names()}
322+
assert exported_names >= _STANDALONE_VOCAB
323+
324+
220325
def test_promoted_pairs_drift() -> None:
221326
pairs = [
222327
(zm.ENDIANNESS, zm.Endianness),

packages/zarr-metadata/tests/v2/array/test_fixtures.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Decode v2 array metadata fixtures via pydantic.
22
33
Each `*.json` file in this directory is a representative on-disk
4-
`.zarray` that should validate cleanly as `ZArrayMetadata` (the strict
4+
`.zarray` that should validate cleanly as `ZarrV2ZArrayJSON` (the strict
55
on-disk shape). User attributes live in sibling `.zattrs` files and are
66
not part of these fixtures.
77
@@ -17,11 +17,11 @@
1717
import pytest
1818
from pydantic import TypeAdapter
1919

20-
from zarr_metadata.v2.array import ZArrayMetadata
20+
from zarr_metadata.v2.array import ZarrV2ZArrayJSON
2121

2222
FIXTURES_DIR = Path(__file__).parent
2323
FIXTURES = sorted(FIXTURES_DIR.glob("*.json"))
24-
ADAPTER = TypeAdapter(ZArrayMetadata)
24+
ADAPTER = TypeAdapter(ZarrV2ZArrayJSON)
2525

2626

2727
@pytest.mark.parametrize("fixture", FIXTURES, ids=lambda p: p.stem)

0 commit comments

Comments
 (0)