|
5 | 5 | import enum |
6 | 6 | import sys |
7 | 7 | import warnings |
8 | | -from typing import Any, cast |
| 8 | +from typing import TYPE_CHECKING, Any, Literal, cast |
9 | 9 |
|
| 10 | +import numpy as np |
10 | 11 | import pytest |
11 | 12 |
|
| 13 | +import zarr |
| 14 | +from tests.conftest import Expect, ExpectFail |
| 15 | +from zarr.abc.codec import SupportsSyncCodec |
12 | 16 | from zarr.codecs.bytes import ( |
13 | 17 | ENDIAN, |
14 | 18 | BytesCodec, |
15 | 19 | Endian, |
16 | 20 | EndianLiteral, |
17 | 21 | ) |
18 | 22 | from zarr.core.array_spec import ArrayConfig, ArraySpec |
19 | | -from zarr.core.buffer import default_buffer_prototype |
| 23 | +from zarr.core.buffer import NDBuffer, default_buffer_prototype |
| 24 | +from zarr.core.dtype import get_data_type_from_native_dtype |
20 | 25 | from zarr.core.dtype.npy.int import Int8, Int32 |
21 | 26 | from zarr.core.dtype.npy.structured import Struct |
| 27 | +from zarr.storage import StorePath |
| 28 | + |
| 29 | +from .test_codecs import _AsyncArrayProxy |
| 30 | + |
| 31 | +if TYPE_CHECKING: |
| 32 | + from zarr.abc.store import Store |
| 33 | + |
| 34 | + |
| 35 | +@pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) |
| 36 | +@pytest.mark.parametrize("input_dtype", [">u2", "<u2"]) |
| 37 | +@pytest.mark.parametrize("store_endian", ["big", "little"]) |
| 38 | +async def test_endian( |
| 39 | + store: Store, |
| 40 | + input_dtype: Literal[">u2", "<u2"], |
| 41 | + store_endian: Literal["big", "little"], |
| 42 | +) -> None: |
| 43 | + """ |
| 44 | + The `bytes` codec stores multi-byte data in the byte order configured on the |
| 45 | + codec, regardless of the input array's byte order, and reads it back to the |
| 46 | + original values. The input-dtype/store-endian cross-product exercises the |
| 47 | + encode-side byteswap (input byte order != store byte order) and the no-op |
| 48 | + case alike. Compression is disabled so the stored chunk is the codec's raw |
| 49 | + output and its byte layout can be asserted directly. |
| 50 | + """ |
| 51 | + data = np.arange(0, 256, dtype=input_dtype).reshape((16, 16)) |
| 52 | + path = "endian" |
| 53 | + spath = StorePath(store, path) |
| 54 | + a = await zarr.api.asynchronous.create_array( |
| 55 | + spath, |
| 56 | + shape=data.shape, |
| 57 | + chunks=(16, 16), |
| 58 | + dtype="uint16", |
| 59 | + fill_value=0, |
| 60 | + compressors=None, |
| 61 | + serializer=BytesCodec(endian=store_endian), |
| 62 | + ) |
| 63 | + |
| 64 | + await _AsyncArrayProxy(a)[:, :].set(data) |
| 65 | + |
| 66 | + # The stored chunk is laid out in the byte order configured on the codec. |
| 67 | + stored = await store.get(f"{path}/c/0/0", prototype=default_buffer_prototype()) |
| 68 | + assert stored is not None |
| 69 | + expected_dtype = ">u2" if store_endian == "big" else "<u2" |
| 70 | + assert stored.to_bytes() == data.astype(expected_dtype).tobytes() |
| 71 | + |
| 72 | + # ... and the data reads back to the original values. |
| 73 | + readback_data = await _AsyncArrayProxy(a)[:, :].get() |
| 74 | + assert np.array_equal(data, readback_data) |
| 75 | + |
| 76 | + |
| 77 | +def test_bytes_codec_supports_sync() -> None: |
| 78 | + assert isinstance(BytesCodec(), SupportsSyncCodec) |
| 79 | + |
| 80 | + |
| 81 | +def test_bytes_codec_sync_roundtrip() -> None: |
| 82 | + codec = BytesCodec() |
| 83 | + arr = np.arange(100, dtype="float64") |
| 84 | + zdtype = get_data_type_from_native_dtype(arr.dtype) |
| 85 | + spec = ArraySpec( |
| 86 | + shape=arr.shape, |
| 87 | + dtype=zdtype, |
| 88 | + fill_value=zdtype.cast_scalar(0), |
| 89 | + config=ArrayConfig(order="C", write_empty_chunks=True), |
| 90 | + prototype=default_buffer_prototype(), |
| 91 | + ) |
| 92 | + nd_buf: NDBuffer = default_buffer_prototype().nd_buffer.from_numpy_array(arr) |
| 93 | + |
| 94 | + codec = codec.evolve_from_array_spec(spec) |
| 95 | + |
| 96 | + encoded = codec._encode_sync(nd_buf, spec) |
| 97 | + assert encoded is not None |
| 98 | + decoded = codec._decode_sync(encoded, spec) |
| 99 | + np.testing.assert_array_equal(arr, decoded.as_numpy_array()) |
22 | 100 |
|
23 | 101 |
|
24 | 102 | @pytest.mark.parametrize("endian", ENDIAN) |
@@ -46,6 +124,43 @@ def test_bytes_codec_json_roundtrip(endian: EndianLiteral) -> None: |
46 | 124 | assert restored == codec |
47 | 125 |
|
48 | 126 |
|
| 127 | +# to_dict and from_dict are inverses over this (endian setting, wire dict) mapping: |
| 128 | +# to_dict turns the endian setting into the dict; from_dict recovers it. |
| 129 | +_ENDIAN_DICT_CASES: list[Expect[EndianLiteral | None, dict[str, Any]]] = [ |
| 130 | + Expect( |
| 131 | + input="little", |
| 132 | + output={"name": "bytes", "configuration": {"endian": "little"}}, |
| 133 | + id="little", |
| 134 | + ), |
| 135 | + Expect( |
| 136 | + input="big", |
| 137 | + output={"name": "bytes", "configuration": {"endian": "big"}}, |
| 138 | + id="big", |
| 139 | + ), |
| 140 | + Expect(input=None, output={"name": "bytes"}, id="missing"), |
| 141 | +] |
| 142 | + |
| 143 | + |
| 144 | +@pytest.mark.parametrize("case", _ENDIAN_DICT_CASES, ids=lambda c: c.id) |
| 145 | +def test_to_dict(case: Expect[EndianLiteral | None, dict[str, Any]]) -> None: |
| 146 | + assert BytesCodec(endian=case.input).to_dict() == case.output |
| 147 | + |
| 148 | + |
| 149 | +@pytest.mark.parametrize("case", _ENDIAN_DICT_CASES, ids=lambda c: c.id) |
| 150 | +def test_from_dict(case: Expect[EndianLiteral | None, dict[str, Any]]) -> None: |
| 151 | + assert BytesCodec.from_dict(case.output).endian == case.input |
| 152 | + |
| 153 | + |
| 154 | +@pytest.mark.parametrize("endian", ["little", "big", pytest.param(None, id="missing")]) |
| 155 | +def test_roundtrip(endian: EndianLiteral | None) -> None: |
| 156 | + codec = BytesCodec(endian=endian) |
| 157 | + |
| 158 | + encoded = codec.to_dict() |
| 159 | + roundtripped = BytesCodec.from_dict(encoded) |
| 160 | + |
| 161 | + assert codec == roundtripped |
| 162 | + |
| 163 | + |
49 | 164 | @pytest.mark.parametrize( |
50 | 165 | ("member", "expected"), |
51 | 166 | [("little", "little"), ("big", "big")], |
@@ -105,14 +220,25 @@ def test_bytes_codec_init_with_deprecated_class_member() -> None: |
105 | 220 | assert codec.endian == "little" |
106 | 221 |
|
107 | 222 |
|
108 | | -def test_bytes_codec_rejects_unknown_endian() -> None: |
| 223 | +@pytest.mark.parametrize( |
| 224 | + "case", |
| 225 | + [ |
| 226 | + ExpectFail( |
| 227 | + input="north", |
| 228 | + exception=ValueError, |
| 229 | + id="unknown-string", |
| 230 | + msg="endian must be one of", |
| 231 | + ), |
| 232 | + ], |
| 233 | + ids=lambda c: c.id, |
| 234 | +) |
| 235 | +def test_bytes_codec_rejects_unknown_endian(case: ExpectFail[Any]) -> None: |
109 | 236 | """ |
110 | | - `BytesCodec.__init__` raises `ValueError` when given a string outside |
| 237 | + `BytesCodec.__init__` raises `ValueError` when given a value outside |
111 | 238 | `ENDIAN`, and the error message names the offending parameter. |
112 | 239 | """ |
113 | | - kwargs: dict[str, Any] = {"endian": "north"} |
114 | | - with pytest.raises(ValueError, match="endian must be one of"): |
115 | | - BytesCodec(**kwargs) |
| 240 | + with case.raises(): |
| 241 | + BytesCodec(endian=case.input) |
116 | 242 |
|
117 | 243 |
|
118 | 244 | def test_endian_attribute_error_for_unknown_member() -> None: |
|
0 commit comments