Skip to content

Commit f794ca4

Browse files
author
NIK-TIGER-BILL
committed
fix: accept unknown metadata fields with must_understand=false
Fixes #3523 Signed-off-by: NIK-TIGER-BILL <nik.tiger.bill@github.com>
1 parent b9d3964 commit f794ca4

2 files changed

Lines changed: 77 additions & 0 deletions

File tree

src/zarr/core/group.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
)
4949
from zarr.core.config import config
5050
from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata
51+
from zarr.core.metadata.v3 import (
52+
AllowedExtraField,
53+
check_allowed_extra_field,
54+
parse_extra_fields,
55+
)
5156
from zarr.core.metadata.io import save_metadata
5257
from zarr.core.sync import SyncMixin, sync
5358
from zarr.errors import (
@@ -354,6 +359,7 @@ class GroupMetadata(Metadata):
354359
zarr_format: ZarrFormat = 3
355360
consolidated_metadata: ConsolidatedMetadata | None = None
356361
node_type: Literal["group"] = field(default="group", init=False)
362+
extra_fields: dict[str, AllowedExtraField] = field(default_factory=dict)
357363

358364
def to_buffer_dict(self, prototype: BufferPrototype) -> dict[str, Buffer]:
359365
json_indent = config.get("json_indent")
@@ -408,13 +414,16 @@ def __init__(
408414
attributes: dict[str, Any] | None = None,
409415
zarr_format: ZarrFormat = 3,
410416
consolidated_metadata: ConsolidatedMetadata | None = None,
417+
extra_fields: Mapping[str, AllowedExtraField] | None = None,
411418
) -> None:
412419
attributes_parsed = parse_attributes(attributes)
413420
zarr_format_parsed = parse_zarr_format(zarr_format)
421+
extra_fields_parsed = parse_extra_fields(extra_fields)
414422

415423
object.__setattr__(self, "attributes", attributes_parsed)
416424
object.__setattr__(self, "zarr_format", zarr_format_parsed)
417425
object.__setattr__(self, "consolidated_metadata", consolidated_metadata)
426+
object.__setattr__(self, "extra_fields", extra_fields_parsed)
418427

419428
@classmethod
420429
def from_dict(cls, data: dict[str, Any]) -> GroupMetadata:
@@ -431,11 +440,34 @@ def from_dict(cls, data: dict[str, Any]) -> GroupMetadata:
431440
# extra key in the metadata.
432441
expected = {x.name for x in fields(cls)}
433442
data = {k: v for k, v in data.items() if k in expected}
443+
else:
444+
# zarr v3: extra fields must have must_understand=False
445+
expected = {x.name for x in fields(cls)}
446+
extra_fields: dict[str, AllowedExtraField] = {}
447+
invalid_extra_fields: dict[str, Any] = {}
448+
for key in list(data.keys()):
449+
if key not in expected:
450+
val = data.pop(key)
451+
if check_allowed_extra_field(val):
452+
extra_fields[key] = val
453+
else:
454+
invalid_extra_fields[key] = val
455+
if len(invalid_extra_fields) > 0:
456+
msg = (
457+
"Got a Zarr V3 group metadata document with the following disallowed extra fields:"
458+
f"{sorted(invalid_extra_fields.keys())}."
459+
'Extra fields are not allowed unless they are a dict with a "must_understand" key'
460+
"which is assigned the value `False`."
461+
)
462+
raise MetadataValidationError(msg)
463+
data["extra_fields"] = extra_fields
434464

435465
return cls(**data)
436466

437467
def to_dict(self) -> dict[str, Any]:
438468
result = asdict(replace(self, consolidated_metadata=None))
469+
extra_fields = result.pop("extra_fields", {})
470+
result = result | extra_fields
439471
if self.consolidated_metadata is not None:
440472
result["consolidated_metadata"] = self.consolidated_metadata.to_dict()
441473
else:

tests/test_group.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,51 @@ def test_from_dict_extra_fields(self):
16011601
expected = GroupMetadata(attributes={"key": "value"}, zarr_format=2)
16021602
assert result == expected
16031603

1604+
def test_from_dict_extra_fields_v3_allowed(self):
1605+
data = {
1606+
"attributes": {"key": "value"},
1607+
"zarr_format": 3,
1608+
"node_type": "group",
1609+
"my_ext": {"must_understand": False, "data": [1, 2, 3]},
1610+
}
1611+
result = GroupMetadata.from_dict(data)
1612+
expected = GroupMetadata(
1613+
attributes={"key": "value"},
1614+
zarr_format=3,
1615+
extra_fields={"my_ext": {"must_understand": False, "data": [1, 2, 3]}},
1616+
)
1617+
assert result == expected
1618+
1619+
def test_from_dict_extra_fields_v3_must_understand_true(self):
1620+
data = {
1621+
"attributes": {"key": "value"},
1622+
"zarr_format": 3,
1623+
"node_type": "group",
1624+
"my_ext": {"must_understand": True},
1625+
}
1626+
with pytest.raises(MetadataValidationError, match="disallowed extra fields"):
1627+
GroupMetadata.from_dict(data)
1628+
1629+
def test_from_dict_extra_fields_v3_non_dict(self):
1630+
data = {
1631+
"attributes": {"key": "value"},
1632+
"zarr_format": 3,
1633+
"node_type": "group",
1634+
"my_ext": 42,
1635+
}
1636+
with pytest.raises(MetadataValidationError, match="disallowed extra fields"):
1637+
GroupMetadata.from_dict(data)
1638+
1639+
def test_to_dict_extra_fields_v3(self):
1640+
metadata = GroupMetadata(
1641+
attributes={"key": "value"},
1642+
zarr_format=3,
1643+
extra_fields={"my_ext": {"must_understand": False, "data": [1, 2, 3]}},
1644+
)
1645+
result = metadata.to_dict()
1646+
assert result["my_ext"] == {"must_understand": False, "data": [1, 2, 3]}
1647+
assert "extra_fields" not in result
1648+
16041649

16051650
class TestInfo:
16061651
def test_info(self):

0 commit comments

Comments
 (0)