Skip to content

Commit 89fa080

Browse files
committed
feat(experimental): scope ArrayIO's extension resolution with a Context
Extends zarr.core.context.Context — the extensions-in-scope value the structured data type resolution already uses — with codecs and codec_pipeline, and threads an optional context through the resolution functions: parse_dtype and the data-type getters, codec normalization (_resolve_codec, _parse_*_codec, parse_codecs), metadata from_dict on both formats, parse_array_metadata, create_codec_pipeline, and build_array_metadata. A kind the context leaves None resolves from the default registries exactly as before; a kind it sets is a closed world, and a name outside it fails to resolve, naming the scope. AsyncArrayIO and ArrayIO take the context in build, open and their constructors, making them the first public takers, and AsyncArray passes one through to the AsyncArrayIO it wraps. Refs zarr-developers#4217 Assisted-by: ClaudeCode:claude-fable-5
1 parent 63c6ed7 commit 89fa080

9 files changed

Lines changed: 253 additions & 51 deletions

File tree

‎changes/420.feature.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a `context` to `zarr.experimental.ArrayIO` and `zarr.experimental.AsyncArrayIO`: a value defining the data types, codecs and codec pipeline in scope for one array. Extension names in a stored document or in creation keyword arguments resolve through the context instead of the default registries; a kind the context leaves unset falls back to the defaults, while a kind it sets is a closed world, and a name outside it fails to resolve with an error naming the scope.

‎src/zarr/core/array.py‎

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
from zarr.abc.store import Store
171171
from zarr.codecs.sharding import IndexLocation, ShardingCodec
172172
from zarr.core.buffer import Buffer
173+
from zarr.core.context import Context
173174
from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar
174175
from zarr.storage import StoreLike
175176
from zarr.types import AnyArray, AnyAsyncArray, ArrayV2, ArrayV3, AsyncArrayV2, AsyncArrayV3
@@ -215,9 +216,13 @@ def _chunk_sizes_from_shape(
215216
return tuple(result)
216217

217218

218-
def parse_array_metadata(data: Any, path: str | None = None) -> ArrayMetadata:
219+
def parse_array_metadata(
220+
data: Any, path: str | None = None, *, context: Context | None = None
221+
) -> ArrayMetadata:
219222
"""Array metadata from a metadata object or a metadata document, naming the array at
220-
`path` in warnings about how an invalid document was read.
223+
`path` in warnings about how an invalid document was read. With a `context`, a
224+
document's extension names resolve through it instead of the default registries;
225+
a metadata object is already resolved and passes through any context.
221226
222227
`ArrayV2Metadata` accepts a chunk size of 0, as it always has, though only an
223228
invalid document holds one: such metadata is read as the documents it would store
@@ -230,7 +235,7 @@ def parse_array_metadata(data: Any, path: str | None = None) -> ArrayMetadata:
230235
if isinstance(data, dict):
231236
zarr_format = data.get("zarr_format")
232237
if zarr_format == 3:
233-
meta_out = ArrayV3Metadata.from_dict(data, path=path)
238+
meta_out = ArrayV3Metadata.from_dict(data, path=path, context=context)
234239
if len(meta_out.storage_transformers) > 0:
235240
msg = (
236241
f"Array metadata contains storage transformers: {meta_out.storage_transformers}."
@@ -239,16 +244,19 @@ def parse_array_metadata(data: Any, path: str | None = None) -> ArrayMetadata:
239244
raise ValueError(msg)
240245
return meta_out
241246
elif zarr_format == 2:
242-
return ArrayV2Metadata.from_dict(data, path=path)
247+
return ArrayV2Metadata.from_dict(data, path=path, context=context)
243248
else:
244249
raise ValueError(f"Invalid zarr_format: {zarr_format}. Expected 2 or 3")
245250
raise TypeError # pragma: no cover
246251

247252

248-
def create_codec_pipeline(metadata: ArrayMetadata, *, store: Store | None = None) -> CodecPipeline:
253+
def create_codec_pipeline(
254+
metadata: ArrayMetadata, *, store: Store | None = None, context: Context | None = None
255+
) -> CodecPipeline:
256+
pipeline_class = get_pipeline_class() if context is None else context.pipeline_class()
249257
if store is not None:
250258
try:
251-
return get_pipeline_class().from_array_metadata_and_store(
259+
return pipeline_class.from_array_metadata_and_store(
252260
array_metadata=metadata, store=store
253261
)
254262
except NotImplementedError:
@@ -261,7 +269,7 @@ def create_codec_pipeline(metadata: ArrayMetadata, *, store: Store | None = None
261269
# "disables partial reads" warning) fire here; `evolve_from_array_spec`
262270
# re-splits the same already-warned-about chain via
263271
# `codecs_from_list_unchecked`, so it does not re-emit them.
264-
pipeline = get_pipeline_class().from_codecs(metadata.codecs)
272+
pipeline = pipeline_class.from_codecs(metadata.codecs)
265273

266274
# Use the regular chunk shape if available, otherwise use a
267275
# placeholder. The ChunkTransform is shape-agnostic — the actual
@@ -280,7 +288,7 @@ def create_codec_pipeline(metadata: ArrayMetadata, *, store: Store | None = None
280288
return pipeline.evolve_from_array_spec(chunk_spec)
281289
elif isinstance(metadata, ArrayV2Metadata):
282290
v2_codec = V2Codec(filters=metadata.filters, compressor=metadata.compressor)
283-
pipeline = get_pipeline_class().from_codecs([v2_codec])
291+
pipeline = pipeline_class.from_codecs([v2_codec])
284292
chunk_spec = ArraySpec(
285293
shape=metadata.chunks,
286294
dtype=metadata.dtype,
@@ -403,6 +411,7 @@ def __init__(
403411
metadata: ArrayV2Metadata | ArrayV2MetadataDict,
404412
store_path: StorePath,
405413
config: ArrayConfigLike | None = None,
414+
context: Context | None = None,
406415
) -> None: ...
407416

408417
@overload
@@ -411,15 +420,17 @@ def __init__(
411420
metadata: ArrayV3Metadata | ArrayMetadataJSON_V3,
412421
store_path: StorePath,
413422
config: ArrayConfigLike | None = None,
423+
context: Context | None = None,
414424
) -> None: ...
415425

416426
def __init__(
417427
self,
418428
metadata: ArrayMetadata | ArrayMetadataDict,
419429
store_path: StorePath,
420430
config: ArrayConfigLike | None = None,
431+
context: Context | None = None,
421432
) -> None:
422-
object.__setattr__(self, "_io", AsyncArrayIO(metadata, store_path, config))
433+
object.__setattr__(self, "_io", AsyncArrayIO(metadata, store_path, config, context))
423434

424435
@classmethod
425436
def _from_io(cls, io: AsyncArrayIO) -> AnyAsyncArray:
@@ -4599,21 +4610,25 @@ def build_array_metadata(
45994610
attributes: dict[str, JSON] | None = None,
46004611
chunk_key_encoding: ChunkKeyEncodingLike | None = None,
46014612
dimension_names: DimensionNamesLike = None,
4613+
context: Context | None = None,
46024614
) -> ArrayV2Metadata | ArrayV3Metadata:
46034615
"""Build an array metadata document from creation-style keyword arguments.
46044616
46054617
This is the pure half of `init_array`: it normalizes the same keyword
46064618
arguments into an in-memory metadata object without touching any store.
46074619
The parameters have the same meaning as in
4608-
[`zarr.api.asynchronous.create_array`][].
4620+
[`zarr.api.asynchronous.create_array`][]. With a `context`, the data type
4621+
and Zarr format 3 codec names resolve through it instead of the default
4622+
registries; Zarr format 2 filters and compressors are numcodecs objects,
4623+
which no context governs.
46094624
"""
46104625

46114626
if zarr_format is None:
46124627
zarr_format = _default_zarr_format()
46134628

46144629
from zarr.codecs.sharding import ShardingCodec
46154630

4616-
zdtype = parse_dtype(dtype, zarr_format=zarr_format)
4631+
zdtype = parse_dtype(dtype, zarr_format=zarr_format, context=context)
46174632
shape_parsed = parse_shapelike(shape)
46184633
item_size = zdtype.item_size if isinstance(zdtype, HasItemSize) else 1
46194634
chunk_key_encoding_parsed = _parse_chunk_key_encoding(
@@ -4689,6 +4704,7 @@ def build_array_metadata(
46894704
filters=filters,
46904705
serializer=serializer,
46914706
dtype=zdtype,
4707+
context=context,
46924708
)
46934709
sub_codecs = cast("tuple[Codec, ...]", (*array_array, array_bytes, *bytes_bytes))
46944710
# A stored chunk grid passed as chunks= / shards= (e.g. by the "keep"
@@ -5312,6 +5328,7 @@ def _parse_chunk_encoding_v3(
53125328
filters: FiltersLike,
53135329
serializer: SerializerLike,
53145330
dtype: ZDType[TBaseDType, TBaseScalar],
5331+
context: Context | None = None,
53155332
) -> tuple[tuple[ArrayArrayCodec, ...], ArrayBytesCodec, tuple[BytesBytesCodec, ...]]:
53165333
"""
53175334
Generate chunk encoding classes for v3 arrays with optional defaults.
@@ -5328,15 +5345,17 @@ def _parse_chunk_encoding_v3(
53285345
maybe_array_array = (filters,)
53295346
else:
53305347
maybe_array_array = cast("Iterable[Codec | dict[str, JSON]]", filters)
5331-
out_array_array = tuple(_parse_array_array_codec(c) for c in maybe_array_array)
5348+
out_array_array = tuple(
5349+
_parse_array_array_codec(c, context=context) for c in maybe_array_array
5350+
)
53325351

53335352
if serializer == "auto":
53345353
out_array_bytes = default_serializer_v3(dtype)
53355354
else:
53365355
# TODO: ensure that the serializer is compatible with the ndarray produced by the
53375356
# array-array codecs. For example, if a sequence of array-array codecs produces an
53385357
# array with a single-byte data type, then the serializer should not specify endiannesss.
5339-
out_array_bytes = _parse_array_bytes_codec(serializer)
5358+
out_array_bytes = _parse_array_bytes_codec(serializer, context=context)
53405359

53415360
if compressors is None:
53425361
out_bytes_bytes: tuple[BytesBytesCodec, ...] = ()
@@ -5349,7 +5368,9 @@ def _parse_chunk_encoding_v3(
53495368
else:
53505369
maybe_bytes_bytes = cast("Iterable[Codec | dict[str, JSON]]", compressors)
53515370

5352-
out_bytes_bytes = tuple(_parse_bytes_bytes_codec(c) for c in maybe_bytes_bytes)
5371+
out_bytes_bytes = tuple(
5372+
_parse_bytes_bytes_codec(c, context=context) for c in maybe_bytes_bytes
5373+
)
53535374

53545375
# TODO: ensure that the serializer is compatible with the ndarray produced by the
53555376
# array-array codecs. For example, if a sequence of array-array codecs produces an
@@ -6207,11 +6228,18 @@ class AsyncArrayIO:
62076228
opened from a stored document that needed a repair stores that repair
62086229
before its first chunk write. Call `store_metadata` first to make that
62096230
write explicit.
6231+
6232+
A `context` (see [`zarr.core.context.Context`][]) defines the data types,
6233+
codecs and codec pipeline in scope for this array: extension names in a
6234+
stored document or in `build`'s keyword arguments resolve through it, and
6235+
a name outside it fails to resolve even when the default registries know
6236+
it. Without one, the default registries are used.
62106237
"""
62116238

62126239
_metadata: ArrayMetadata
62136240
_store_path: StorePath
62146241
_config: ArrayConfig
6242+
_context: Context | None
62156243
_codec_pipeline: CodecPipeline
62166244
_chunk_grid: ChunkGrid
62176245

@@ -6220,14 +6248,16 @@ def __init__(
62206248
metadata: ArrayMetadata | ArrayMetadataDict,
62216249
store_path: StorePath,
62226250
config: ArrayConfigLike | None = None,
6251+
context: Context | None = None,
62236252
) -> None:
6224-
metadata_parsed = parse_array_metadata(metadata, str(store_path))
6253+
metadata_parsed = parse_array_metadata(metadata, str(store_path), context=context)
62256254
self._metadata = metadata_parsed
62266255
self._store_path = store_path
62276256
self._config = parse_array_config(config)
6257+
self._context = context
62286258
self._chunk_grid = ChunkGrid.from_metadata(metadata_parsed)
62296259
self._codec_pipeline = create_codec_pipeline(
6230-
metadata=metadata_parsed, store=store_path.store
6260+
metadata=metadata_parsed, store=store_path.store, context=context
62316261
)
62326262

62336263
@classmethod
@@ -6250,12 +6280,15 @@ async def build(
62506280
chunk_key_encoding: ChunkKeyEncodingLike | None = None,
62516281
dimension_names: DimensionNamesLike = None,
62526282
config: ArrayConfigLike | None = None,
6283+
context: Context | None = None,
62536284
) -> AsyncArrayIO:
62546285
"""Declare an array bound to a location in a store, without storing anything.
62556286
62566287
The keyword arguments have the same meaning as in
62576288
[`zarr.api.asynchronous.create_array`][]. The returned instance holds
6258-
the normalized metadata in memory; `store_metadata` stores it.
6289+
the normalized metadata in memory; `store_metadata` stores it. With a
6290+
`context`, the data type and codec names resolve through it instead of
6291+
the default registries.
62596292
"""
62606293
metadata = build_array_metadata(
62616294
shape=shape,
@@ -6271,9 +6304,10 @@ async def build(
62716304
attributes=attributes,
62726305
chunk_key_encoding=chunk_key_encoding,
62736306
dimension_names=dimension_names,
6307+
context=context,
62746308
)
62756309
store_path = await make_store_path(store, path=name)
6276-
return cls(metadata, store_path, config)
6310+
return cls(metadata, store_path, config, context=context)
62776311

62786312
@classmethod
62796313
async def open(
@@ -6283,19 +6317,24 @@ async def open(
62836317
name: str | None = None,
62846318
zarr_format: ZarrFormat | None = None,
62856319
config: ArrayConfigLike | None = None,
6320+
context: Context | None = None,
62866321
) -> AsyncArrayIO:
62876322
"""Read the metadata of an existing array and bind to it.
62886323
62896324
This is the one classmethod constructor that performs IO: the array's
62906325
stored metadata document is read from the store. `zarr_format=None`
62916326
(the default) reads Zarr format 3 and falls back to format 2, as
6292-
`open` does in `zarrs`; pass 2 or 3 to require one format.
6327+
`open` does in `zarrs`; pass 2 or 3 to require one format. With a
6328+
`context`, the document's data type and codec names resolve through it
6329+
instead of the default registries.
62936330
"""
62946331
store_path = await make_store_path(store, path=name)
62956332
metadata = parse_array_metadata(
6296-
await get_array_metadata(store_path, zarr_format=zarr_format), str(store_path)
6333+
await get_array_metadata(store_path, zarr_format=zarr_format),
6334+
str(store_path),
6335+
context=context,
62976336
)
6298-
return cls(metadata, store_path, config)
6337+
return cls(metadata, store_path, config, context=context)
62996338

63006339
@property
63016340
def metadata(self) -> ArrayMetadata:
@@ -6350,7 +6389,9 @@ async def verify(self) -> None:
63506389
"""
63516390
bound = self._metadata
63526391
stored = parse_array_metadata(
6353-
await get_array_metadata(self._store_path, zarr_format=None), str(self._store_path)
6392+
await get_array_metadata(self._store_path, zarr_format=None),
6393+
str(self._store_path),
6394+
context=self._context,
63546395
)
63556396
if stored == bound:
63566397
return
@@ -6515,8 +6556,9 @@ def __init__(
65156556
metadata: ArrayMetadata | ArrayMetadataDict,
65166557
store_path: StorePath,
65176558
config: ArrayConfigLike | None = None,
6559+
context: Context | None = None,
65186560
) -> None:
6519-
self._async_io = AsyncArrayIO(metadata, store_path, config)
6561+
self._async_io = AsyncArrayIO(metadata, store_path, config, context)
65206562

65216563
@classmethod
65226564
def _wrap(cls, async_io: AsyncArrayIO) -> ArrayIO:
@@ -6544,6 +6586,7 @@ def build(
65446586
chunk_key_encoding: ChunkKeyEncodingLike | None = None,
65456587
dimension_names: DimensionNamesLike = None,
65466588
config: ArrayConfigLike | None = None,
6589+
context: Context | None = None,
65476590
) -> ArrayIO:
65486591
"""Declare an array bound to a location in a store, without storing anything.
65496592
@@ -6568,6 +6611,7 @@ def build(
65686611
chunk_key_encoding=chunk_key_encoding,
65696612
dimension_names=dimension_names,
65706613
config=config,
6614+
context=context,
65716615
)
65726616
)
65736617
)
@@ -6580,13 +6624,18 @@ def open(
65806624
name: str | None = None,
65816625
zarr_format: ZarrFormat | None = None,
65826626
config: ArrayConfigLike | None = None,
6627+
context: Context | None = None,
65836628
) -> ArrayIO:
65846629
"""Read the metadata of an existing array and bind to it.
65856630
65866631
See `AsyncArrayIO.open`.
65876632
"""
65886633
return cls._wrap(
6589-
sync(AsyncArrayIO.open(store, name=name, zarr_format=zarr_format, config=config))
6634+
sync(
6635+
AsyncArrayIO.open(
6636+
store, name=name, zarr_format=zarr_format, config=config, context=context
6637+
)
6638+
)
65906639
)
65916640

65926641
@property

0 commit comments

Comments
 (0)