finish replacing interface tests with new helper system

This commit is contained in:
sneakers-the-rat 2024-10-10 18:58:05 -07:00
parent 3356738e42
commit 5d4f03a8a9
Signed by untrusted user who does not match committer: jonny
GPG key ID: 6DCB96EF1E4D232D
7 changed files with 150 additions and 183 deletions

View file

@ -31,6 +31,53 @@ else:
YES_PIPE = False
def merged_product(
*args: Sequence[ValidationCase],
) -> Generator[ValidationCase, None, None]:
"""
Generator for the product of the iterators of validation cases,
merging each tuple, and respecting if they should be :meth:`.ValidationCase.skip`
or not.
Examples:
.. code-block:: python
shape_cases = [
ValidationCase(shape=(10, 10, 10), passes=True, id="valid shape"),
ValidationCase(shape=(10, 10), passes=False, id="missing dimension"),
]
dtype_cases = [
ValidationCase(dtype=float, passes=True, id="float"),
ValidationCase(dtype=int, passes=False, id="int"),
]
iterator = merged_product(shape_cases, dtype_cases))
next(iterator)
# ValidationCase(
# shape=(10, 10, 10),
# dtype=float,
# passes=True,
# id="valid shape-float"
# )
next(iterator)
# ValidationCase(
# shape=(10, 10, 10),
# dtype=int,
# passes=False,
# id="valid shape-int"
# )
"""
iterator = product(*args)
for case_tuple in iterator:
case = merge_cases(case_tuple)
if case.skip():
continue
yield case
class BasicModel(BaseModel):
x: int
@ -58,7 +105,6 @@ FLOAT: TypeAlias = NDArray[Shape["*, *, *"], Float]
STRING: TypeAlias = NDArray[Shape["*, *, *"], str]
MODEL: TypeAlias = NDArray[Shape["*, *, *"], BasicModel]
UNION_TYPE: TypeAlias = NDArray[Shape["*, *, *"], Union[np.uint32, np.float32]]
UNION_PIPE: TypeAlias = NDArray[Shape["*, *, *"], np.uint32 | np.float32]
SHAPE_CASES = (
ValidationCase(shape=(10, 10, 10), passes=True, id="valid shape"),
@ -135,6 +181,8 @@ DTYPE_CASES = [
if YES_PIPE:
UNION_PIPE: TypeAlias = NDArray[Shape["*, *, *"], np.uint32 | np.float32]
DTYPE_CASES.extend(
[
ValidationCase(
@ -178,50 +226,3 @@ _INTERFACE_CASES = [
ZarrNestedCase,
VideoCase,
]
def merged_product(
*args: Sequence[ValidationCase],
) -> Generator[ValidationCase, None, None]:
"""
Generator for the product of the iterators of validation cases,
merging each tuple, and respecting if they should be :meth:`.ValidationCase.skip`
or not.
Examples:
.. code-block:: python
shape_cases = [
ValidationCase(shape=(10, 10, 10), passes=True, id="valid shape"),
ValidationCase(shape=(10, 10), passes=False, id="missing dimension"),
]
dtype_cases = [
ValidationCase(dtype=float, passes=True, id="float"),
ValidationCase(dtype=int, passes=False, id="int"),
]
iterator = merged_product(shape_cases, dtype_cases))
next(iterator)
# ValidationCase(
# shape=(10, 10, 10),
# dtype=float,
# passes=True,
# id="valid shape-float"
# )
next(iterator)
# ValidationCase(
# shape=(10, 10, 10),
# dtype=int,
# passes=False,
# id="valid shape-int"
# )
"""
iterator = product(*args)
for case_tuple in iterator:
case = merge_cases(case_tuple)
if case.skip():
continue
yield case

View file

@ -46,9 +46,16 @@ class InterfaceCase(ABC):
shape: Tuple[int, ...] = (10, 10),
dtype: DtypeType = float,
path: Optional[Path] = None,
array: Optional[NDArrayType] = None,
) -> Optional[NDArrayType]:
"""
Make an array from a shape and dtype, and a path if needed
Args:
shape: shape of the array
dtype: dtype of the array
path: Path, if needed to generate on disk
array: Rather than passing shape and dtype, pass a literal arraylike thing
"""
@classmethod

View file

@ -19,7 +19,7 @@ from numpydantic.interface import (
ZarrInterface,
)
from numpydantic.testing.helpers import InterfaceCase
from numpydantic.types import DtypeType
from numpydantic.types import DtypeType, NDArrayType
class NumpyCase(InterfaceCase):
@ -33,8 +33,11 @@ class NumpyCase(InterfaceCase):
shape: Tuple[int, ...] = (10, 10),
dtype: DtypeType = float,
path: Optional[Path] = None,
array: Optional[NDArrayType] = None,
) -> np.ndarray:
if issubclass(dtype, BaseModel):
if array is not None:
return np.array(array, dtype=dtype)
elif issubclass(dtype, BaseModel):
return np.full(shape=shape, fill_value=dtype(x=1))
else:
return np.zeros(shape=shape, dtype=dtype)
@ -59,6 +62,7 @@ class HDF5Case(_HDF5MetaCase):
shape: Tuple[int, ...] = (10, 10),
dtype: DtypeType = float,
path: Optional[Path] = None,
array: Optional[NDArrayType] = None,
) -> Optional[H5ArrayPath]:
if cls.skip(shape, dtype):
return None
@ -67,7 +71,9 @@ class HDF5Case(_HDF5MetaCase):
array_path = "/" + "_".join([str(s) for s in shape]) + "__" + dtype.__name__
generator = np.random.default_rng()
if dtype is str:
if array is not None:
data = np.array(array, dtype=dtype)
elif dtype is str:
data = generator.random(shape).astype(bytes)
elif dtype is datetime:
data = np.empty(shape, dtype="S32")
@ -91,13 +97,16 @@ class HDF5CompoundCase(_HDF5MetaCase):
shape: Tuple[int, ...] = (10, 10),
dtype: DtypeType = float,
path: Optional[Path] = None,
array: Optional[NDArrayType] = None,
) -> Optional[H5ArrayPath]:
if cls.skip(shape, dtype):
return None
hdf5_file = path / "h5f.h5"
array_path = "/" + "_".join([str(s) for s in shape]) + "__" + dtype.__name__
if dtype is str:
if array is not None:
data = np.array(array, dtype=dtype)
elif dtype is str:
dt = np.dtype([("data", np.dtype("S10")), ("extra", "i8")])
data = np.array([("hey", 0)] * np.prod(shape), dtype=dt).reshape(shape)
elif dtype is datetime:
@ -128,7 +137,10 @@ class DaskCase(InterfaceCase):
shape: Tuple[int, ...] = (10, 10),
dtype: DtypeType = float,
path: Optional[Path] = None,
array: Optional[NDArrayType] = None,
) -> da.Array:
if array is not None:
return da.array(array, dtype=dtype, chunks=-1)
if issubclass(dtype, BaseModel):
return da.full(shape=shape, fill_value=dtype(x=1), chunks=-1)
else:
@ -142,7 +154,7 @@ class _ZarrMetaCase(InterfaceCase):
@classmethod
def skip(cls, shape: Tuple[int, ...], dtype: DtypeType) -> bool:
return not issubclass(dtype, BaseModel)
return issubclass(dtype, BaseModel)
class ZarrCase(_ZarrMetaCase):
@ -154,7 +166,11 @@ class ZarrCase(_ZarrMetaCase):
shape: Tuple[int, ...] = (10, 10),
dtype: DtypeType = float,
path: Optional[Path] = None,
array: Optional[NDArrayType] = None,
) -> Optional[zarr.Array]:
if array is not None:
return zarr.array(array, dtype=dtype, chunks=-1)
else:
return zarr.zeros(shape=shape, dtype=dtype)
@ -167,8 +183,12 @@ class ZarrDirCase(_ZarrMetaCase):
shape: Tuple[int, ...] = (10, 10),
dtype: DtypeType = float,
path: Optional[Path] = None,
array: Optional[NDArrayType] = None,
) -> Optional[zarr.Array]:
store = zarr.DirectoryStore(str(path / "array.zarr"))
if array is not None:
return zarr.array(array, dtype=dtype, store=store, chunks=-1)
else:
return zarr.zeros(shape=shape, dtype=dtype, store=store)
@ -181,8 +201,12 @@ class ZarrZipCase(_ZarrMetaCase):
shape: Tuple[int, ...] = (10, 10),
dtype: DtypeType = float,
path: Optional[Path] = None,
array: Optional[NDArrayType] = None,
) -> Optional[zarr.Array]:
store = zarr.ZipStore(str(path / "array.zarr"), mode="w")
if array is not None:
return zarr.array(array, dtype=dtype, store=store, chunks=-1)
else:
return zarr.zeros(shape=shape, dtype=dtype, store=store)
@ -195,10 +219,14 @@ class ZarrNestedCase(_ZarrMetaCase):
shape: Tuple[int, ...] = (10, 10),
dtype: DtypeType = float,
path: Optional[Path] = None,
array: Optional[NDArrayType] = None,
) -> ZarrArrayPath:
file = str(path / "nested.zarr")
root = zarr.open(file, mode="w")
subpath = "a/b/c"
if array is not None:
_ = root.array(subpath, array, dtype=dtype)
else:
_ = root.zeros(subpath, shape=shape, dtype=dtype)
return ZarrArrayPath(file=file, path=subpath)
@ -214,10 +242,15 @@ class VideoCase(InterfaceCase):
shape: Tuple[int, ...] = (10, 10),
dtype: DtypeType = float,
path: Optional[Path] = None,
array: Optional[NDArrayType] = None,
) -> Optional[Path]:
if cls.skip(shape, dtype):
return None
if array is not None:
array = np.ndarray(shape, dtype=np.uint8)
shape = array.shape
is_color = len(shape) == 4
frames = shape[0]
frame_shape = shape[1:]
@ -232,13 +265,16 @@ class VideoCase(InterfaceCase):
)
for i in range(frames):
# make fresh array every time bc opencv eats them
array = np.zeros(frame_shape, dtype=np.uint8)
if not is_color:
array[i, i] = i
if array is not None:
frame = array[i]
else:
array[i, i, :] = i
writer.write(array)
# make fresh array every time bc opencv eats them
frame = np.zeros(frame_shape, dtype=np.uint8)
if not is_color:
frame[i, i] = i
else:
frame[i, i, :] = i
writer.write(frame)
writer.release()
return video_path

View file

@ -7,7 +7,6 @@ import zarr
from numpydantic.interface.hdf5 import H5ArrayPath
from numpydantic.interface.zarr import ZarrArrayPath
from numpydantic.testing import ValidationCase
from numpydantic.testing.interfaces import HDF5Case, HDF5CompoundCase, VideoCase
@ -57,8 +56,8 @@ def avi_video(tmp_output_dir_func) -> Callable[[Tuple[int, int], int, bool], Pat
shape = (frames, *shape)
if is_color:
shape = (*shape, 3)
return VideoCase.array_from_case(
ValidationCase(shape=shape, dtype=np.uint8), tmp_output_dir_func
return VideoCase.make_array(
shape=shape, dtype=np.uint8, path=tmp_output_dir_func
)
return _make_video

View file

@ -5,13 +5,11 @@ from typing import Any
import h5py
import numpy as np
import pytest
from pydantic import BaseModel, ValidationError
from pydantic import BaseModel
from numpydantic import NDArray, Shape
from numpydantic.exceptions import DtypeError, ShapeError
from numpydantic.interface import H5Interface
from numpydantic.interface.hdf5 import H5ArrayPath, H5Proxy
from numpydantic.testing.helpers import ValidationCase
from numpydantic.testing.interfaces import HDF5Case, HDF5CompoundCase
pytestmark = pytest.mark.hdf5
@ -27,35 +25,24 @@ def hdf5_cases(request):
return request.param
def hdf5_array_case(
case: ValidationCase, array_func, compound: bool = False
) -> H5ArrayPath:
"""
Args:
case:
array_func: ( the function returned from the `hdf5_array` fixture )
Returns:
"""
if issubclass(case.dtype, BaseModel):
pytest.skip("hdf5 cant support arbitrary python objects")
return array_func(case.shape, case.dtype, compound)
def _test_hdf5_case(case: ValidationCase, array_func, compound: bool = False) -> None:
array = hdf5_array_case(case, array_func, compound)
if case.passes:
case.model(array=array)
else:
with pytest.raises((ValidationError, DtypeError, ShapeError)):
case.model(array=array)
def test_hdf5_enabled():
assert H5Interface.enabled()
@pytest.mark.shape
def test_hdf5_shape(shape_cases, hdf5_cases):
shape_cases.interface = hdf5_cases
if shape_cases.skip():
pytest.skip()
shape_cases.validate_case()
@pytest.mark.dtype
def test_hdf5_dtype(dtype_cases, hdf5_cases):
dtype_cases.interface = hdf5_cases
dtype_cases.validate_case()
def test_hdf5_check(interface_type):
if interface_type[1] is H5Interface:
assert H5Interface.check(interface_type[0])
@ -82,20 +69,6 @@ def test_hdf5_check_not_hdf5(tmp_path):
assert not H5Interface.check(spec)
@pytest.mark.shape
def test_hdf5_shape(shape_cases, hdf5_cases):
shape_cases.interface = hdf5_cases
if shape_cases.skip():
pytest.skip()
shape_cases.validate_case()
@pytest.mark.dtype
def test_hdf5_dtype(dtype_cases, hdf5_cases):
dtype_cases.interface = hdf5_cases
dtype_cases.validate_case()
def test_hdf5_dataset_not_exists(hdf5_array, model_blank):
array = hdf5_array()
with pytest.raises(ValueError) as e:

View file

@ -1,37 +1,21 @@
import numpy as np
import pytest
from pydantic import BaseModel, ValidationError
from numpydantic.exceptions import DtypeError, ShapeError
from numpydantic.testing.helpers import ValidationCase
from numpydantic.testing.cases import NumpyCase
pytestmark = pytest.mark.numpy
def numpy_array(case: ValidationCase) -> np.ndarray:
if issubclass(case.dtype, BaseModel):
return np.full(shape=case.shape, fill_value=case.dtype(x=1))
else:
return np.zeros(shape=case.shape, dtype=case.dtype)
def _test_np_case(case: ValidationCase):
array = numpy_array(case)
if case.passes:
case.model(array=array)
else:
with pytest.raises((ValidationError, DtypeError, ShapeError)):
case.model(array=array)
@pytest.mark.shape
def test_numpy_shape(shape_cases):
_test_np_case(shape_cases)
shape_cases.interface = NumpyCase
shape_cases.validate_case()
@pytest.mark.dtype
def test_numpy_dtype(dtype_cases):
_test_np_case(dtype_cases)
dtype_cases.interface = NumpyCase
dtype_cases.validate_case()
def test_numpy_coercion(model_blank):

View file

@ -1,58 +1,21 @@
import json
import numpy as np
import pytest
import zarr
from pydantic import BaseModel, ValidationError
from numpydantic.exceptions import DtypeError, ShapeError
from numpydantic.interface import ZarrInterface
from numpydantic.interface.zarr import ZarrArrayPath
from numpydantic.testing.helpers import ValidationCase
from numpydantic.testing.cases import ZarrCase, ZarrDirCase, ZarrNestedCase, ZarrZipCase
from numpydantic.testing.helpers import InterfaceCase
pytestmark = pytest.mark.zarr
@pytest.fixture()
def dir_array(tmp_output_dir_func) -> zarr.DirectoryStore:
store = zarr.DirectoryStore(tmp_output_dir_func / "array.zarr")
return store
@pytest.fixture()
def zip_array(tmp_output_dir_func) -> zarr.ZipStore:
store = zarr.ZipStore(tmp_output_dir_func / "array.zip", mode="w")
return store
@pytest.fixture()
def nested_dir_array(tmp_output_dir_func) -> zarr.NestedDirectoryStore:
store = zarr.NestedDirectoryStore(tmp_output_dir_func / "nested")
return store
def _zarr_array(case: ValidationCase, store) -> zarr.core.Array:
if issubclass(case.dtype, BaseModel):
pytest.skip(
"Zarr can't handle objects properly at the moment, "
"see https://github.com/zarr-developers/zarr-python/issues/2081"
@pytest.fixture(
params=[ZarrCase, ZarrZipCase, ZarrDirCase, ZarrNestedCase],
)
# return zarr.full(
# shape=case.shape,
# fill_value=case.dtype(x=1),
# dtype=object,
# object_codec=Pickle(),
# )
else:
return zarr.zeros(shape=case.shape, dtype=case.dtype, store=store)
def _test_zarr_case(case: ValidationCase, store):
array = _zarr_array(case, store)
if case.passes:
case.model(array=array)
else:
with pytest.raises((ValidationError, DtypeError, ShapeError)):
case.model(array=array)
def zarr_case(request) -> InterfaceCase:
return request.param
@pytest.fixture(
@ -86,13 +49,17 @@ def test_zarr_check(interface_type):
@pytest.mark.shape
def test_zarr_shape(store, shape_cases):
_test_zarr_case(shape_cases, store)
def test_zarr_shape(shape_cases, zarr_case):
shape_cases.interface = zarr_case
shape_cases.validate_case()
@pytest.mark.dtype
def test_zarr_dtype(dtype_cases, store):
_test_zarr_case(dtype_cases, store)
def test_zarr_dtype(dtype_cases, zarr_case):
dtype_cases.interface = zarr_case
if dtype_cases.skip():
pytest.skip()
dtype_cases.validate_case()
@pytest.mark.parametrize("array", ["zarr_nested_array", "zarr_array"])
@ -126,7 +93,7 @@ def test_zarr_array_path_from_iterable(zarr_array):
@pytest.mark.serialization
@pytest.mark.parametrize("dump_array", [True, False])
@pytest.mark.parametrize("roundtrip", [True, False])
def test_zarr_to_json(store, model_blank, roundtrip, dump_array):
def test_zarr_to_json(zarr_case, model_blank, roundtrip, dump_array, tmp_path):
expected_fields = (
"Type",
"Data type",
@ -136,9 +103,9 @@ def test_zarr_to_json(store, model_blank, roundtrip, dump_array):
"Store type",
"hexdigest",
)
lol_array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
lol_array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=int)
array = zarr.array(lol_array, store=store)
array = zarr_case.make_array(array=lol_array, dtype=int, path=tmp_path)
instance = model_blank(array=array)
context = {"dump_array": dump_array}
@ -148,7 +115,7 @@ def test_zarr_to_json(store, model_blank, roundtrip, dump_array):
if roundtrip:
if dump_array:
assert as_json["value"] == lol_array
assert np.array_equal(as_json["value"], lol_array)
else:
if as_json.get("file", False):
assert "array" not in as_json
@ -158,4 +125,4 @@ def test_zarr_to_json(store, model_blank, roundtrip, dump_array):
assert len(as_json["info"]["hexdigest"]) == 40
else:
assert as_json == lol_array
assert np.array_equal(as_json, lol_array)