Skip to content

Commit 28768dd

Browse files
authored
Merge branch 'main' into dependabot/uv/packages/zarr-http-server/uv-7a16775f7e
2 parents 8732aac + 9b7b93b commit 28768dd

20 files changed

Lines changed: 1127 additions & 95 deletions

‎changes/4166.doc.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Clarify how Zarr format 3 codec pipeline roles map to `filters`, `serializer`, and `compressors` when creating arrays.

‎changes/4404.feature.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reading data encoded with the `pickle` codec now requires an explicit opt-in, because unpickling data from an untrusted store can execute arbitrary code ([GHSA-jqc8-qjfv-5m6r](https://github.com/zarr-developers/numcodecs/security/advisories/GHSA-jqc8-qjfv-5m6r)). Enable it with `zarr.config.set({"array.allow_unsafe_pickle_decode": True})` or the environment variable `ZARR_ARRAY__ALLOW_UNSAFE_PICKLE_DECODE=True`. Without the opt-in, reading such data raises `zarr.errors.PickleCodecDisabledError`. numcodecs has its own opt-in (`numcodecs.pickles.ALLOW_UNSAFE_PICKLE_DECODE`), which must also be enabled. Writing with the `pickle` codec is unchanged.

‎changes/4408.bugfix.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`zarr.open_array` and `zarr.open` (sync and async) now apply the `config` argument when opening an existing array. Previously `config` was only used when a new array was created, so `zarr.open_array(store, config={"read_missing_chunks": False})` silently ran an existing array with the global defaults. `config` is now a named parameter of both functions and of `Array.open`/`AsyncArray.open`. `zarr.open` raises a `TypeError` naming the problem when `config` is given for a group.

‎changes/4450.bugfix.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed several ways `ZipStore` could lose data or fail. A store created with `mode="w"` or `mode="x"` now keeps its entries when it is used again after `close()`, `move()`, or unpickling, instead of truncating the archive. Every method now opens the archive on first use, under a lock, so `get`, `get_partial_values`, `set_if_not_exists`, and `clear` no longer raise `AttributeError` on a store that was never opened, and threads sharing a store no longer open the archive twice. An unpickled store now opens its archive on first use rather than when it is unpickled, so writing a dask array to a `ZipStore` no longer produces an unreadable file. Where reopening an archive would silently drop its entries, the store now raises instead: `RuntimeError` after a failed `close()`, `zipfile.BadZipFile` if the file is not a complete zip archive, and `io.UnsupportedOperation` for a file object that is not readable and seekable. The type of the `mode` parameter now includes `"x"`. Builds on [#3593](https://github.com/zarr-developers/zarr-python/pull/3593) by [@oelhammouchi](https://github.com/oelhammouchi).

‎changes/4539.bugfix.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The minimum supported numcodecs version is now 0.16.6, and 0.17.0 is excluded. Those versions leave `Pickle.decode` enabled by default, which allows arbitrary code execution when reading an untrusted Zarr v2 array ([GHSA-jqc8-qjfv-5m6r](https://github.com/zarr-developers/numcodecs/security/advisories/GHSA-jqc8-qjfv-5m6r)).

‎docs/user-guide/arrays.md‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,13 +194,61 @@ arr_f = arr.with_config({"order": "F"})
194194
print(arr_f.config)
195195
```
196196

197+
## Zarr format 3 codec pipeline
198+
199+
Zarr format 3 stores a single ordered list of codecs in the
200+
[`codecs`](https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#array-metadata-codecs)
201+
field of array metadata. The
202+
[Zarr format 3 specification](https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-encoding)
203+
classifies each codec by the input and output of its encode transform, as
204+
"array -> array", "array -> bytes", or "bytes -> bytes", and requires the list to
205+
contain zero or more array -> array codecs, followed by exactly one
206+
array -> bytes codec, followed by zero or more bytes -> bytes codecs.
207+
208+
Zarr-Python's array creation functions expose that list through three
209+
parameters: `filters`, `serializer`, and `compressors`. These names are not
210+
defined in the Zarr format 3 specification. They are Zarr-Python aliases for
211+
the specification's codec kinds:
212+
213+
- `filters`: the array -> array codecs, given as
214+
[`zarr.abc.codec.ArrayArrayCodec`][] instances. These transform chunk arrays
215+
into chunk arrays before serialization.
216+
- `serializer`: the array -> bytes codec, given as one
217+
[`zarr.abc.codec.ArrayBytesCodec`][] instance. This transforms a chunk array
218+
into bytes. Every Zarr format 3 array needs exactly one array -> bytes codec,
219+
either supplied explicitly or chosen by default.
220+
- `compressors`: the bytes -> bytes codecs, given as
221+
[`zarr.abc.codec.BytesBytesCodec`][] instances. These transform bytes into
222+
bytes after serialization.
223+
224+
The `compressors` parameter is only for bytes-to-bytes codecs. If a codec is an
225+
`ArrayBytesCodec`, pass it with `serializer`, not `compressors`. For example, the
226+
built-in [`zarr.codecs.BytesCodec`][] can be supplied explicitly as the serializer:
227+
228+
```python exec="true" session="arrays" source="above" result="ansi"
229+
serializer = zarr.codecs.BytesCodec(endian="little")
230+
z_explicit_serializer = zarr.create_array(
231+
store="data/example-explicit-serializer.zarr",
232+
shape=(100,),
233+
chunks=(10,),
234+
dtype="int32",
235+
serializer=serializer,
236+
compressors=None,
237+
)
238+
print(z_explicit_serializer.serializer)
239+
print(f"Compressors: {z_explicit_serializer.compressors}")
240+
```
241+
242+
The same rule applies to third-party Zarr format 3 codecs: if the codec is
243+
documented as an `ArrayBytesCodec`, provide an instance as `serializer=...`.
244+
197245
## Compressors
198246

199247
A number of different compressors can be used with Zarr. Zarr includes Blosc,
200248
Zstandard and Gzip compressors. Additional compressors are available through
201249
a separate package called [NumCodecs](https://numcodecs.readthedocs.io/en/stable/) which provides various
202250
compressor libraries including LZ4, Zlib, BZ2 and LZMA.
203-
Different compressors can be provided via the `compressors` keyword
251+
Different bytes-to-bytes compressors can be provided via the `compressors` keyword
204252
argument accepted by all array creation functions. For example:
205253

206254
```python exec="true" session="arrays" source="above" result="ansi"

‎docs/user-guide/config.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Configuration options include the following:
4040
- Whether empty chunks are written to storage `array.write_empty_chunks`
4141
- Enable experimental rectilinear chunks `array.rectilinear_chunks`
4242
- Whether missing chunks are filled with the array's fill value on read `array.read_missing_chunks` (default `True`). Set to `False` to raise a [`ChunkNotFoundError`][zarr.errors.ChunkNotFoundError] instead.
43+
- Whether data encoded with the `pickle` codec can be read `array.allow_unsafe_pickle_decode` (default `False`). Unpickling can execute arbitrary code, so only enable this for data from a source you trust, e.g. with `zarr.config.set({'array.allow_unsafe_pickle_decode': True})` or the environment variable `ZARR_ARRAY__ALLOW_UNSAFE_PICKLE_DECODE=True`. While it is disabled, reading such data raises a [`PickleCodecDisabledError`][zarr.errors.PickleCodecDisabledError]. Writing with the `pickle` codec is not affected. numcodecs has a separate opt-in for the same reason (`numcodecs.pickles.ALLOW_UNSAFE_PICKLE_DECODE = True`, or `NUMCODECS_ALLOW_UNSAFE_PICKLE_DECODE=1` before importing numcodecs); both must be enabled to read pickled data.
4344
- Async and threading options, e.g. `async.concurrency` and `threading.max_workers`
4445
- Selections of implementations of codecs, codec pipelines and buffers
4546
- Enabling GPU support with `zarr.config.enable_gpu()`. See [GPU support](gpu.md) for more.

‎pyproject.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ requires-python = ">=3.12"
4747
dependencies = [
4848
'packaging>=22.0',
4949
'numpy>=2',
50-
'numcodecs>=0.16',
50+
# 0.16.6 and 0.17.1 refuse to unpickle untrusted data (GHSA-jqc8-qjfv-5m6r); 0.17.0 does not.
51+
'numcodecs>=0.16.6,!=0.17.0',
5152
'google-crc32c>=1.5',
5253
'typing_extensions>=4.14',
5354
'donfig>=0.8',

‎src/zarr/api/asynchronous.py‎

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ async def open(
353353
zarr_format: ZarrFormat | None = None,
354354
path: str | None = None,
355355
storage_options: dict[str, Any] | None = None,
356+
config: ArrayConfigLike | None = None,
356357
**kwargs: Any, # TODO: type kwargs as valid args to open_array
357358
) -> AnyAsyncArray | AsyncGroup:
358359
"""Convenience function to open a group or array using file-mode-like semantics.
@@ -377,6 +378,11 @@ async def open(
377378
storage_options : dict
378379
If using an fsspec URL to create the store, these will be passed to
379380
the backend implementation. Ignored otherwise.
381+
config : ArrayConfigLike or None, default=None
382+
Runtime configuration for the array, whether it is opened or created. Keys not
383+
specified are taken from the global configuration. Groups have no runtime
384+
configuration, so passing `config` when a group is opened or created raises a
385+
`TypeError`.
380386
**kwargs
381387
Additional parameters are passed through to `zarr.open_array` or
382388
`zarr.open_group`.
@@ -417,24 +423,33 @@ async def open(
417423
zarr_format = _metadata_dict["zarr_format"]
418424
is_v3_array = zarr_format == 3 and _metadata_dict.get("node_type") == "array"
419425
if is_v3_array or zarr_format == 2:
420-
return AsyncArray(
421-
store_path=store_path,
422-
metadata=_metadata_dict,
423-
config=kwargs.get("config"),
424-
)
426+
return AsyncArray(store_path=store_path, metadata=_metadata_dict, config=config)
425427
except (FileNotFoundError, NodeTypeValidationError):
426428
pass
429+
_check_no_group_config(config, store_path)
427430
return await open_group(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs)
428431

429432
try:
430-
return await open_array(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs)
433+
return await open_array(
434+
store=store_path, zarr_format=zarr_format, mode=mode, config=config, **kwargs
435+
)
431436
except (KeyError, NodeTypeValidationError):
432437
# KeyError for a missing key
433438
# NodeTypeValidationError for failing to parse node metadata as an array when it's
434439
# actually a group
440+
_check_no_group_config(config, store_path)
435441
return await open_group(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs)
436442

437443

444+
def _check_no_group_config(config: ArrayConfigLike | None, store_path: StorePath) -> None:
445+
"""Raise if `config` is given for a node that `open` resolves to a group."""
446+
if config is not None:
447+
raise TypeError(
448+
f"config is the runtime configuration of an array, but {store_path} resolves to a "
449+
"group. Groups have no runtime configuration."
450+
)
451+
452+
438453
async def open_consolidated(
439454
*args: Any, use_consolidated: Literal[True] = True, **kwargs: Any
440455
) -> AsyncGroup:
@@ -1058,6 +1073,13 @@ async def create(
10581073
10591074
The elements of `codecs` specify the transformation from array values to stored bytes.
10601075
Zarr format 3 only. Zarr format 2 arrays should use `filters` and `compressor` instead.
1076+
The [Zarr format 3 specification](https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-encoding)
1077+
requires, in order, zero or more array -> array codecs
1078+
([`zarr.abc.codec.ArrayArrayCodec`][]), exactly one array -> bytes codec
1079+
([`zarr.abc.codec.ArrayBytesCodec`][]), and zero or more bytes -> bytes codecs
1080+
([`zarr.abc.codec.BytesBytesCodec`][]). Elsewhere in Zarr-Python these are
1081+
called filters, the serializer, and compressors; those names are Zarr-Python
1082+
aliases, not terms defined in the specification.
10611083
10621084
If no codecs are provided, default codecs will be used based on the data type of the array.
10631085
For most data types, the default codecs are the tuple `(BytesCodec(), ZstdCodec())`;
@@ -1306,6 +1328,7 @@ async def open_array(
13061328
zarr_format: ZarrFormat | None = None,
13071329
path: PathLike = "",
13081330
storage_options: dict[str, Any] | None = None,
1331+
config: ArrayConfigLike | None = None,
13091332
**kwargs: Any, # TODO: type kwargs as valid args to save
13101333
) -> AnyAsyncArray:
13111334
"""Open an array using file-mode-like semantics.
@@ -1323,6 +1346,9 @@ async def open_array(
13231346
storage_options : dict
13241347
If using an fsspec URL to create the store, these will be passed to
13251348
the backend implementation. Ignored otherwise.
1349+
config : ArrayConfigLike or None, default=None
1350+
Runtime configuration for the array, whether it is opened or created. Keys not
1351+
specified are taken from the global configuration.
13261352
**kwargs
13271353
Any keyword arguments to pass to [`create`][zarr.api.asynchronous.create].
13281354
@@ -1341,7 +1367,7 @@ async def open_array(
13411367
# mode "w" replaces any existing array, so there is nothing to open
13421368
if mode != "w":
13431369
try:
1344-
return await AsyncArray.open(store_path, zarr_format=zarr_format)
1370+
return await AsyncArray.open(store_path, zarr_format=zarr_format, config=config)
13451371
except FileNotFoundError as err:
13461372
if store_path.read_only or mode not in _CREATE_MODES:
13471373
msg = f"No array found in store {store_path.store} at path {store_path.path}"
@@ -1350,6 +1376,7 @@ async def open_array(
13501376
store=store_path,
13511377
zarr_format=zarr_format or _default_zarr_format(),
13521378
overwrite=_infer_overwrite(mode),
1379+
config=config,
13531380
**kwargs,
13541381
)
13551382

‎src/zarr/api/synchronous.py‎

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ def open(
186186
zarr_format: ZarrFormat | None = None,
187187
path: str | None = None,
188188
storage_options: dict[str, Any] | None = None,
189+
config: ArrayConfigLike | None = None,
189190
**kwargs: Any, # TODO: type kwargs as valid args to async_api.open
190191
) -> AnyArray | Group:
191192
"""Open a group or array using file-mode-like semantics.
@@ -210,6 +211,11 @@ def open(
210211
storage_options : dict
211212
If using an fsspec URL to create the store, these will be passed to
212213
the backend implementation. Ignored otherwise.
214+
config : ArrayConfigLike or None, default=None
215+
Runtime configuration for the array, whether it is opened or created. Keys not
216+
specified are taken from the global configuration. Groups have no runtime
217+
configuration, so passing `config` when a group is opened or created raises a
218+
`TypeError`.
213219
**kwargs
214220
Additional parameters are passed through to `zarr.open_array` or
215221
`zarr.open_group`.
@@ -237,6 +243,7 @@ def open(
237243
zarr_format=zarr_format,
238244
path=path,
239245
storage_options=storage_options,
246+
config=config,
240247
**kwargs,
241248
)
242249
)
@@ -775,6 +782,13 @@ def create(
775782
776783
The elements of `codecs` specify the transformation from array values to stored bytes.
777784
Zarr format 3 only. Zarr format 2 arrays should use `filters` and `compressor` instead.
785+
The [Zarr format 3 specification](https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-encoding)
786+
requires, in order, zero or more array -> array codecs
787+
([`zarr.abc.codec.ArrayArrayCodec`][]), exactly one array -> bytes codec
788+
([`zarr.abc.codec.ArrayBytesCodec`][]), and zero or more bytes -> bytes codecs
789+
([`zarr.abc.codec.BytesBytesCodec`][]). Elsewhere in Zarr-Python these are
790+
called filters, the serializer, and compressors; those names are Zarr-Python
791+
aliases, not terms defined in the specification.
778792
779793
If no codecs are provided, default codecs will be used based on the data type of the array.
780794
For most data types, the default codecs are the tuple `(BytesCodec(), ZstdCodec())`;
@@ -928,10 +942,13 @@ def create_array(
928942
filters are applied (if any are specified) and the data is serialized into bytes.
929943
930944
For Zarr format 3, a "compressor" is a codec that takes a bytestream, and
931-
returns another bytestream. Multiple compressors may be provided for Zarr format 3.
932-
If no `compressors` are provided, a default set of compressors will be used.
933-
These defaults can be changed by modifying the value of `array.v3_default_compressors`
934-
in [`zarr.config`][zarr.config].
945+
returns another bytestream. These values must be instances of
946+
[`zarr.abc.codec.BytesBytesCodec`][], or dict representations of
947+
[`zarr.abc.codec.BytesBytesCodec`][]. Multiple compressors may be provided
948+
for Zarr format 3. Codecs that take an array and return bytes are serializers
949+
and must be supplied with `serializer` instead.
950+
If no `compressors` are provided, the default for Zarr format 3 is
951+
`(ZstdCodec(),)`.
935952
Use `None` to omit default compressors.
936953
937954
For Zarr format 2, a "compressor" can be any numcodecs codec. Only a single compressor may
@@ -943,9 +960,11 @@ def create_array(
943960
serializer : dict[str, JSON] | ArrayBytesCodec, optional
944961
Array-to-bytes codec to use for encoding the array data.
945962
Zarr format 3 only. Zarr format 2 arrays use implicit array-to-bytes conversion.
946-
If no `serializer` is provided, a default serializer will be used.
947-
These defaults can be changed by modifying the value of `array.v3_default_serializer`
948-
in [`zarr.config`][zarr.config].
963+
Codecs that are instances of [`zarr.abc.codec.ArrayBytesCodec`][] must be
964+
supplied here, not with `compressors`.
965+
If no `serializer` is provided, a default serializer is chosen based on the data type:
966+
`VLenUTF8Codec` for variable-length strings, `VLenBytesCodec` for variable-length
967+
bytes, and `BytesCodec` otherwise.
949968
fill_value : Any, optional
950969
Fill value for the array.
951970
order : {"C", "F"}, optional
@@ -1401,6 +1420,7 @@ def open_array(
14011420
zarr_format: ZarrFormat | None = None,
14021421
path: PathLike = "",
14031422
storage_options: dict[str, Any] | None = None,
1423+
config: ArrayConfigLike | None = None,
14041424
**kwargs: Any,
14051425
) -> AnyArray:
14061426
"""Open an array using file-mode-like semantics.
@@ -1418,6 +1438,9 @@ def open_array(
14181438
storage_options : dict
14191439
If using an fsspec URL to create the store, these will be passed to
14201440
the backend implementation. Ignored otherwise.
1441+
config : ArrayConfigLike or None, default=None
1442+
Runtime configuration for the array, whether it is opened or created. Keys not
1443+
specified are taken from the global configuration.
14211444
**kwargs
14221445
Any keyword arguments to pass to [`create`][zarr.api.asynchronous.create].
14231446
@@ -1434,6 +1457,7 @@ def open_array(
14341457
zarr_format=zarr_format,
14351458
path=path,
14361459
storage_options=storage_options,
1460+
config=config,
14371461
**kwargs,
14381462
)
14391463
)

0 commit comments

Comments
 (0)