Skip to content

Commit df40f4f

Browse files
dstansbyd-v-b
andauthored
Fix order handling (#3112)
* Fix order handling Fix imports * Update changes/xxx1.bugfix.rst * Rename bugfix entries * Add test for order warning in from_array * Add test for config warning in init_array * Remove spurious test file * Improve order warning --------- Co-authored-by: Davis Bennett <[email protected]>
1 parent 7a162bf commit df40f4f

File tree

10 files changed

+90
-94
lines changed

10 files changed

+90
-94
lines changed

changes/3112.bugfix.1.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Creating a Zarr format 2 array with the ``order`` keyword argument no longer raises a warning.

changes/3112.bugfix.2.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Creating a Zarr format 3 array with the ``order`` argument now conistently ignores this argument and raises a warning.

changes/3112.bugfix.3.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
When using ``from_array`` to copy a Zarr format 2 array to a Zarr format 3 array, if the memory order of the input array is ``"F"`` a warning is raised and the order ignored.
2+
This is because Zarr format 3 arrays are always stored in "C" order.

changes/3112.bugfix.4.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The ``config`` argument to `zarr.create` (and functions that create arrays) is now used - previously it had no effect.

changes/3112.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed the error message when passing both ``config`` and ``write_empty_chunks`` arguments to reflect the current behaviour (``write_empty_chunks`` takes precedence).

src/zarr/api/asynchronous.py

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from_array,
2020
get_array_metadata,
2121
)
22-
from zarr.core.array_spec import ArrayConfig, ArrayConfigLike, ArrayConfigParams
22+
from zarr.core.array_spec import ArrayConfigLike, parse_array_config
2323
from zarr.core.buffer import NDArrayLike
2424
from zarr.core.common import (
2525
JSON,
@@ -29,7 +29,6 @@
2929
MemoryOrder,
3030
ZarrFormat,
3131
_default_zarr_format,
32-
_warn_order_kwarg,
3332
_warn_write_empty_chunks_kwarg,
3433
)
3534
from zarr.core.dtype import ZDTypeLike, get_data_type_from_native_dtype
@@ -1026,8 +1025,6 @@ async def create(
10261025
if meta_array is not None:
10271026
warnings.warn("meta_array is not yet implemented", RuntimeWarning, stacklevel=2)
10281027

1029-
if order is not None:
1030-
_warn_order_kwarg()
10311028
if write_empty_chunks is not None:
10321029
_warn_write_empty_chunks_kwarg()
10331030

@@ -1036,26 +1033,17 @@ async def create(
10361033
mode = "a"
10371034
store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options)
10381035

1039-
config_dict: ArrayConfigParams = {}
1036+
config_parsed = parse_array_config(config)
10401037

10411038
if write_empty_chunks is not None:
10421039
if config is not None:
10431040
msg = (
10441041
"Both write_empty_chunks and config keyword arguments are set. "
1045-
"This is redundant. When both are set, write_empty_chunks will be ignored and "
1046-
"config will be used."
1042+
"This is redundant. When both are set, write_empty_chunks will be used instead "
1043+
"of the value in config."
10471044
)
10481045
warnings.warn(UserWarning(msg), stacklevel=1)
1049-
config_dict["write_empty_chunks"] = write_empty_chunks
1050-
if order is not None and config is not None:
1051-
msg = (
1052-
"Both order and config keyword arguments are set. "
1053-
"This is redundant. When both are set, order will be ignored and "
1054-
"config will be used."
1055-
)
1056-
warnings.warn(UserWarning(msg), stacklevel=1)
1057-
1058-
config_parsed = ArrayConfig.from_dict(config_dict)
1046+
config_parsed = dataclasses.replace(config_parsed, write_empty_chunks=write_empty_chunks)
10591047

10601048
return await AsyncArray._create(
10611049
store_path,
@@ -1258,8 +1246,6 @@ async def open_array(
12581246

12591247
zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)
12601248

1261-
if "order" in kwargs:
1262-
_warn_order_kwarg()
12631249
if "write_empty_chunks" in kwargs:
12641250
_warn_write_empty_chunks_kwarg()
12651251

src/zarr/core/array.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
_default_zarr_format,
6363
_warn_order_kwarg,
6464
concurrent_map,
65-
parse_order,
6665
parse_shapelike,
6766
product,
6867
)
@@ -651,7 +650,6 @@ async def _create(
651650

652651
if order is not None:
653652
_warn_order_kwarg()
654-
config_parsed = replace(config_parsed, order=order)
655653

656654
result = await cls._create_v3(
657655
store_path,
@@ -679,9 +677,10 @@ async def _create(
679677
raise ValueError("dimension_names cannot be used for arrays with zarr_format 2.")
680678

681679
if order is None:
682-
order_parsed = parse_order(zarr_config.get("array.order"))
680+
order_parsed = config_parsed.order
683681
else:
684682
order_parsed = order
683+
config_parsed = replace(config_parsed, order=order)
685684

686685
result = await cls._create_v2(
687686
store_path,
@@ -4326,10 +4325,8 @@ async def init_array(
43264325
chunks_out = chunk_shape_parsed
43274326
codecs_out = sub_codecs
43284327

4329-
if config is None:
4330-
config = {}
4331-
if order is not None and isinstance(config, dict):
4332-
config["order"] = config.get("order", order)
4328+
if order is not None:
4329+
_warn_order_kwarg()
43334330

43344331
meta = AsyncArray._create_metadata_v3(
43354332
shape=shape_parsed,
@@ -4580,8 +4577,18 @@ def _parse_keep_array_attr(
45804577
serializer = "auto"
45814578
if fill_value is None:
45824579
fill_value = data.fill_value
4583-
if order is None:
4580+
4581+
if data.metadata.zarr_format == 2 and zarr_format == 3 and data.order == "F":
4582+
# Can't set order="F" for v3 arrays
4583+
warnings.warn(
4584+
"The 'order' attribute of a Zarr format 2 array does not have a direct analogue in Zarr format 3. "
4585+
"The existing order='F' of the source Zarr format 2 array will be ignored.",
4586+
UserWarning,
4587+
stacklevel=2,
4588+
)
4589+
elif order is None and zarr_format == 2:
45844590
order = data.order
4591+
45854592
if chunk_key_encoding is None and zarr_format == data.metadata.zarr_format:
45864593
if isinstance(data.metadata, ArrayV2Metadata):
45874594
chunk_key_encoding = {"name": "v2", "separator": data.metadata.dimension_separator}

src/zarr/core/group.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2826,7 +2826,7 @@ def array(
28262826
compressor: CompressorLike = None,
28272827
serializer: SerializerLike = "auto",
28282828
fill_value: Any | None = DEFAULT_FILL_VALUE,
2829-
order: MemoryOrder | None = "C",
2829+
order: MemoryOrder | None = None,
28302830
attributes: dict[str, JSON] | None = None,
28312831
chunk_key_encoding: ChunkKeyEncodingLike | None = None,
28322832
dimension_names: DimensionNames = None,

tests/test_api.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import zarr.codecs
88
import zarr.storage
9+
from zarr.core.array import init_array
10+
from zarr.storage._common import StorePath
911

1012
if TYPE_CHECKING:
1113
from collections.abc import Callable
@@ -335,34 +337,66 @@ def test_open_with_mode_w_minus(tmp_path: Path) -> None:
335337
zarr.open(store=tmp_path, mode="w-")
336338

337339

338-
def test_array_order(zarr_format: ZarrFormat) -> None:
339-
arr = zarr.ones(shape=(2, 2), order=None, zarr_format=zarr_format)
340-
expected = zarr.config.get("array.order")
341-
assert arr.order == expected
340+
@pytest.mark.parametrize("order", ["C", "F", None])
341+
@pytest.mark.parametrize("config", [{"order": "C"}, {"order": "F"}, {}], ids=["C", "F", "None"])
342+
def test_array_order(
343+
order: MemoryOrder | None, config: dict[str, MemoryOrder | None], zarr_format: ZarrFormat
344+
) -> None:
345+
"""
346+
Check that:
347+
- For v2, memory order is taken from the `order` keyword argument.
348+
- For v3, memory order is taken from `config`, and when order is passed a warning is raised
349+
- The numpy array returned has the expected order
350+
- For v2, the order metadata is set correctly
351+
"""
352+
default_order = zarr.config.get("array.order")
353+
ctx: contextlib.AbstractContextManager # type: ignore[type-arg]
342354

343-
vals = np.asarray(arr)
344-
if expected == "C":
345-
assert vals.flags.c_contiguous
346-
elif expected == "F":
347-
assert vals.flags.f_contiguous
348-
else:
349-
raise AssertionError
355+
if zarr_format == 3:
356+
if order is None:
357+
ctx = contextlib.nullcontext()
358+
else:
359+
ctx = pytest.warns(
360+
RuntimeWarning,
361+
match="The `order` keyword argument has no effect for Zarr format 3 arrays",
362+
)
350363

364+
expected_order = config.get("order", default_order)
351365

352-
@pytest.mark.parametrize("order", ["C", "F"])
353-
def test_array_order_warns(order: MemoryOrder | None, zarr_format: ZarrFormat) -> None:
354-
with pytest.warns(RuntimeWarning, match="The `order` keyword argument .*"):
355-
arr = zarr.ones(shape=(2, 2), order=order, zarr_format=zarr_format)
356-
assert arr.order == order
366+
if zarr_format == 2:
367+
ctx = contextlib.nullcontext()
368+
expected_order = order or config.get("order", default_order)
369+
370+
with ctx:
371+
arr = zarr.ones(shape=(2, 2), order=order, zarr_format=zarr_format, config=config)
357372

373+
assert arr.order == expected_order
358374
vals = np.asarray(arr)
359-
if order == "C":
375+
if expected_order == "C":
360376
assert vals.flags.c_contiguous
361-
elif order == "F":
377+
elif expected_order == "F":
362378
assert vals.flags.f_contiguous
363379
else:
364380
raise AssertionError
365381

382+
if zarr_format == 2:
383+
assert arr.metadata.zarr_format == 2
384+
assert arr.metadata.order == expected_order
385+
386+
387+
async def test_init_order_warns() -> None:
388+
with pytest.warns(
389+
RuntimeWarning, match="The `order` keyword argument has no effect for Zarr format 3 arrays"
390+
):
391+
await init_array(
392+
store_path=StorePath(store=MemoryStore()),
393+
shape=(1,),
394+
dtype="uint8",
395+
config=None,
396+
zarr_format=3,
397+
order="F",
398+
)
399+
366400

367401
# def test_lazy_loader():
368402
# foo = np.arange(100)

tests/test_array.py

Lines changed: 11 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from zarr.core.buffer import NDArrayLike, NDArrayLikeOrScalar, default_buffer_prototype
4242
from zarr.core.chunk_grids import _auto_partition
4343
from zarr.core.chunk_key_encodings import ChunkKeyEncodingParams
44-
from zarr.core.common import JSON, MemoryOrder, ZarrFormat
44+
from zarr.core.common import JSON, ZarrFormat
4545
from zarr.core.dtype import (
4646
DateTime64,
4747
Float32,
@@ -61,14 +61,14 @@
6161
from zarr.core.group import AsyncGroup
6262
from zarr.core.indexing import BasicIndexer, ceildiv
6363
from zarr.core.metadata.v2 import ArrayV2Metadata
64+
from zarr.core.metadata.v3 import ArrayV3Metadata
6465
from zarr.core.sync import sync
6566
from zarr.errors import ContainsArrayError, ContainsGroupError
6667
from zarr.storage import LocalStore, MemoryStore, StorePath
6768

6869
from .test_dtype.conftest import zdtype_examples
6970

7071
if TYPE_CHECKING:
71-
from zarr.core.array_spec import ArrayConfigLike
7272
from zarr.core.metadata.v3 import ArrayV3Metadata
7373

7474

@@ -1447,52 +1447,6 @@ async def test_data_ignored_params(store: Store) -> None:
14471447
):
14481448
await create_array(store, data=data, shape=None, dtype=data.dtype, overwrite=True)
14491449

1450-
@staticmethod
1451-
@pytest.mark.parametrize("order", ["C", "F", None])
1452-
@pytest.mark.parametrize("with_config", [True, False])
1453-
def test_order(
1454-
order: MemoryOrder | None,
1455-
with_config: bool,
1456-
zarr_format: ZarrFormat,
1457-
store: MemoryStore,
1458-
) -> None:
1459-
"""
1460-
Test that the arrays generated by array indexing have a memory order defined by the config order
1461-
value, and that for zarr v2 arrays, the ``order`` field in the array metadata is set correctly.
1462-
"""
1463-
config: ArrayConfigLike | None = {}
1464-
if order is None:
1465-
config = {}
1466-
expected = zarr.config.get("array.order")
1467-
else:
1468-
config = {"order": order}
1469-
expected = order
1470-
1471-
if not with_config:
1472-
# Test without passing config parameter
1473-
config = None
1474-
1475-
arr = zarr.create_array(
1476-
store=store,
1477-
shape=(2, 2),
1478-
zarr_format=zarr_format,
1479-
dtype="i4",
1480-
order=order,
1481-
config=config,
1482-
)
1483-
assert arr.order == expected
1484-
if zarr_format == 2:
1485-
assert arr.metadata.zarr_format == 2
1486-
assert arr.metadata.order == expected
1487-
1488-
vals = np.asarray(arr)
1489-
if expected == "C":
1490-
assert vals.flags.c_contiguous
1491-
elif expected == "F":
1492-
assert vals.flags.f_contiguous
1493-
else:
1494-
raise AssertionError
1495-
14961450
@staticmethod
14971451
@pytest.mark.parametrize("write_empty_chunks", [True, False])
14981452
async def test_write_empty_chunks_config(write_empty_chunks: bool, store: Store) -> None:
@@ -1674,6 +1628,15 @@ async def test_from_array_arraylike(
16741628
np.testing.assert_array_equal(result[...], np.full_like(src, fill_value))
16751629

16761630

1631+
def test_from_array_F_order() -> None:
1632+
arr = zarr.create_array(store={}, data=np.array([1]), order="F", zarr_format=2)
1633+
with pytest.warns(
1634+
UserWarning,
1635+
match="The existing order='F' of the source Zarr format 2 array will be ignored.",
1636+
):
1637+
zarr.from_array(store={}, data=arr, zarr_format=3)
1638+
1639+
16771640
async def test_orthogonal_set_total_slice() -> None:
16781641
"""Ensure that a whole chunk overwrite does not read chunks"""
16791642
store = MemoryStore()

0 commit comments

Comments
 (0)