mirror of
https://github.com/p2p-ld/numpydantic.git
synced 2024-11-10 00:34:29 +00:00
tests and no covers continue
This commit is contained in:
parent
0ee371ad05
commit
9b13226164
10 changed files with 198 additions and 9 deletions
|
@ -10,7 +10,7 @@ from numpydantic.interface.interface import Interface
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from dask.array.core import Array as DaskArray
|
from dask.array.core import Array as DaskArray
|
||||||
except ImportError:
|
except ImportError: # pragma: no cover
|
||||||
DaskArray = None
|
DaskArray = None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ from numpydantic.types import NDArrayType
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import h5py
|
import h5py
|
||||||
except ImportError:
|
except ImportError: # pragma: no cover
|
||||||
h5py = None
|
h5py = None
|
||||||
|
|
||||||
if sys.version_info.minor >= 10:
|
if sys.version_info.minor >= 10:
|
||||||
|
@ -160,9 +160,11 @@ class H5Interface(Interface):
|
||||||
"""Create an :class:`.H5Proxy` to use throughout validation"""
|
"""Create an :class:`.H5Proxy` to use throughout validation"""
|
||||||
if isinstance(array, H5ArrayPath):
|
if isinstance(array, H5ArrayPath):
|
||||||
array = H5Proxy.from_h5array(h5array=array)
|
array = H5Proxy.from_h5array(h5array=array)
|
||||||
elif isinstance(array, (tuple, list)) and len(array) == 2:
|
elif isinstance(array, (tuple, list)) and len(array) == 2: # pragma: no cover
|
||||||
array = H5Proxy(file=array[0], path=array[1])
|
array = H5Proxy(file=array[0], path=array[1])
|
||||||
else:
|
else: # pragma: no cover
|
||||||
|
# this should never happen really since `check` confirms this before
|
||||||
|
# we'd reach here, but just to complete the if else...
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Need to specify a file and a path within an HDF5 file to use the HDF5 "
|
"Need to specify a file and a path within an HDF5 file to use the HDF5 "
|
||||||
"Interface"
|
"Interface"
|
||||||
|
|
|
@ -141,7 +141,7 @@ class Interface(ABC, Generic[T]):
|
||||||
for iface in cls.interfaces():
|
for iface in cls.interfaces():
|
||||||
if isinstance(iface.input_types, Union[tuple, list]):
|
if isinstance(iface.input_types, Union[tuple, list]):
|
||||||
in_types.extend(iface.input_types)
|
in_types.extend(iface.input_types)
|
||||||
else:
|
else: # pragma: no cover
|
||||||
in_types.append(iface.input_types)
|
in_types.append(iface.input_types)
|
||||||
|
|
||||||
return tuple(in_types)
|
return tuple(in_types)
|
||||||
|
|
|
@ -11,7 +11,7 @@ try:
|
||||||
|
|
||||||
ENABLED = True
|
ENABLED = True
|
||||||
|
|
||||||
except ImportError:
|
except ImportError: # pragma: no cover
|
||||||
ENABLED = False
|
ENABLED = False
|
||||||
ndarray = None
|
ndarray = None
|
||||||
|
|
||||||
|
@ -23,6 +23,13 @@ class NumpyInterface(Interface):
|
||||||
|
|
||||||
input_types = (ndarray, list)
|
input_types = (ndarray, list)
|
||||||
return_type = ndarray
|
return_type = ndarray
|
||||||
|
priority = -1
|
||||||
|
"""
|
||||||
|
The numpy interface is usually the interface of last resort.
|
||||||
|
We want to use any more specific interface that we might have,
|
||||||
|
because the numpy interface checks for anything that could be coerced
|
||||||
|
to a numpy array (see :meth:`.NumpyInterface.check` )
|
||||||
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check(cls, array: Any) -> bool:
|
def check(cls, array: Any) -> bool:
|
||||||
|
|
|
@ -13,7 +13,7 @@ try:
|
||||||
import zarr
|
import zarr
|
||||||
from zarr.core import Array as ZarrArray
|
from zarr.core import Array as ZarrArray
|
||||||
from zarr.storage import StoreLike
|
from zarr.storage import StoreLike
|
||||||
except ImportError:
|
except ImportError: # pragma: no cover
|
||||||
ZarrArray = None
|
ZarrArray = None
|
||||||
StoreLike = None
|
StoreLike = None
|
||||||
storage = None
|
storage = None
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional, Tuple, Type, Union
|
from typing import Any, Callable, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
import h5py
|
import h5py
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -85,6 +85,16 @@ def model_rgb() -> Type[BaseModel]:
|
||||||
return RGB
|
return RGB
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def model_blank() -> Type[BaseModel]:
|
||||||
|
"""A model with any shape and dtype"""
|
||||||
|
|
||||||
|
class BlankModel(BaseModel):
|
||||||
|
array: NDArray[Shape["*, ..."], Any]
|
||||||
|
|
||||||
|
return BlankModel
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def hdf5_file(tmp_output_dir_func) -> h5py.File:
|
def hdf5_file(tmp_output_dir_func) -> h5py.File:
|
||||||
h5f_file = tmp_output_dir_func / "h5f.h5"
|
h5f_file = tmp_output_dir_func / "h5f.h5"
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
import pdb
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import json
|
||||||
|
|
||||||
import dask.array as da
|
import dask.array as da
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
@ -42,3 +45,12 @@ def test_dask_shape(shape_cases):
|
||||||
|
|
||||||
def test_dask_dtype(dtype_cases):
|
def test_dask_dtype(dtype_cases):
|
||||||
_test_dask_case(dtype_cases)
|
_test_dask_case(dtype_cases)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dask_to_json(array_model):
|
||||||
|
array_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
|
||||||
|
array = da.array(array_list)
|
||||||
|
model = array_model((3, 3), int)
|
||||||
|
instance = model(array=array)
|
||||||
|
jsonified = json.loads(instance.model_dump_json())
|
||||||
|
assert jsonified["array"] == array_list
|
||||||
|
|
|
@ -1,7 +1,69 @@
|
||||||
import pdb
|
import pdb
|
||||||
import json
|
import json
|
||||||
|
import pytest
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ValidationError
|
||||||
|
|
||||||
|
from numpydantic.interface import H5Interface
|
||||||
|
from numpydantic.interface.hdf5 import H5ArrayPath
|
||||||
|
from numpydantic.exceptions import DtypeError, ShapeError
|
||||||
|
|
||||||
|
from tests.conftest import ValidationCase
|
||||||
|
|
||||||
|
|
||||||
|
def hdf5_array_case(case: ValidationCase, array_func) -> H5ArrayPath:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
case:
|
||||||
|
array_func: ( the function returned from the `hdf5_array` fixture )
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return array_func(case.shape, case.dtype)
|
||||||
|
|
||||||
|
|
||||||
|
def _test_hdf5_case(case: ValidationCase, array_func):
|
||||||
|
array = hdf5_array_case(case, array_func)
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
def test_hdf5_check(interface_type):
|
||||||
|
if interface_type[1] is H5Interface:
|
||||||
|
if interface_type[0].__name__ == "_hdf5_array":
|
||||||
|
interface_type = (interface_type[0](), interface_type[1])
|
||||||
|
assert H5Interface.check(interface_type[0])
|
||||||
|
if isinstance(interface_type[0], H5ArrayPath):
|
||||||
|
# also test that we can instantiate from a tuple like the H5ArrayPath
|
||||||
|
assert H5Interface.check((interface_type[0].file, interface_type[0].path))
|
||||||
|
else:
|
||||||
|
assert not H5Interface.check(interface_type[0])
|
||||||
|
|
||||||
|
|
||||||
|
def test_hdf5_shape(shape_cases, hdf5_array):
|
||||||
|
_test_hdf5_case(shape_cases, hdf5_array)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hdf5_dtype(dtype_cases, hdf5_array):
|
||||||
|
if dtype_cases.dtype is str:
|
||||||
|
pytest.skip("hdf5 cant do string arrays")
|
||||||
|
_test_hdf5_case(dtype_cases, hdf5_array)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hdf5_dataset_not_exists(hdf5_array, model_blank):
|
||||||
|
array = hdf5_array()
|
||||||
|
with pytest.raises(ValueError) as e:
|
||||||
|
model_blank(array=H5ArrayPath(file=array.file, path="/some/random/path"))
|
||||||
|
assert "file located" in e
|
||||||
|
assert "no array found" in e
|
||||||
|
|
||||||
|
|
||||||
def test_to_json(hdf5_array, array_model):
|
def test_to_json(hdf5_array, array_model):
|
||||||
|
|
90
tests/test_interface/test_interface.py
Normal file
90
tests/test_interface/test_interface.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from numpydantic.interface import Interface
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def interfaces():
|
||||||
|
"""Define test interfaces in this module, and delete afterwards"""
|
||||||
|
|
||||||
|
class Interface1(Interface):
|
||||||
|
input_types = (list,)
|
||||||
|
return_type = tuple
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check(cls, array):
|
||||||
|
if isinstance(array, list):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enabled(cls) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
Interface2 = type("Interface2", Interface1.__bases__, dict(Interface1.__dict__))
|
||||||
|
|
||||||
|
class Interface3(Interface1):
|
||||||
|
@classmethod
|
||||||
|
def enabled(cls) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
class Interfaces:
|
||||||
|
interface1 = Interface1
|
||||||
|
interface2 = Interface2
|
||||||
|
interface3 = Interface3
|
||||||
|
|
||||||
|
yield Interfaces
|
||||||
|
del Interface1
|
||||||
|
del Interface2
|
||||||
|
del Interface3
|
||||||
|
|
||||||
|
|
||||||
|
def test_interface_match_error(interfaces):
|
||||||
|
"""
|
||||||
|
Test that `match` and `match_output` raises errors when no or multiple matches are found
|
||||||
|
"""
|
||||||
|
with pytest.raises(ValueError) as e:
|
||||||
|
Interface.match([1, 2, 3])
|
||||||
|
assert "Interface1" in e
|
||||||
|
assert "Interface2" in e
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as e:
|
||||||
|
Interface.match("hey")
|
||||||
|
assert "No matching interfaces" in e
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as e:
|
||||||
|
Interface.match_output((1, 2, 3))
|
||||||
|
assert "Interface1" in e
|
||||||
|
assert "Interface2" in e
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as e:
|
||||||
|
Interface.match_output("hey")
|
||||||
|
assert "No matching interfaces" in e
|
||||||
|
|
||||||
|
|
||||||
|
def test_interface_enabled(interfaces):
|
||||||
|
"""
|
||||||
|
An interface shouldn't be included if it's not enabled
|
||||||
|
"""
|
||||||
|
assert not interfaces.Interface3.enabled()
|
||||||
|
assert interfaces.Interface3 not in Interface.interfaces()
|
||||||
|
|
||||||
|
|
||||||
|
def test_interface_type_lists():
|
||||||
|
"""
|
||||||
|
Seems like a silly test, but ensure that our return types and input types
|
||||||
|
lists have all the class attrs
|
||||||
|
"""
|
||||||
|
for interface in Interface.interfaces():
|
||||||
|
|
||||||
|
if isinstance(interface.input_types, (list, tuple)):
|
||||||
|
for atype in interface.input_types:
|
||||||
|
assert atype in Interface.input_types()
|
||||||
|
else:
|
||||||
|
assert interface.input_types in Interface.input_types()
|
||||||
|
|
||||||
|
if isinstance(interface.return_type, (list, tuple)):
|
||||||
|
for atype in interface.return_type:
|
||||||
|
assert atype in Interface.return_types()
|
||||||
|
else:
|
||||||
|
assert interface.return_type in Interface.return_types()
|
|
@ -25,3 +25,9 @@ def test_numpy_shape(shape_cases):
|
||||||
|
|
||||||
def test_numpy_dtype(dtype_cases):
|
def test_numpy_dtype(dtype_cases):
|
||||||
_test_np_case(dtype_cases)
|
_test_np_case(dtype_cases)
|
||||||
|
|
||||||
|
|
||||||
|
def test_numpy_coercion(model_blank):
|
||||||
|
"""If no other interface matches, we try and coerce to a numpy array"""
|
||||||
|
instance = model_blank(array=[1, 2, 3])
|
||||||
|
assert isinstance(instance.array, np.ndarray)
|
||||||
|
|
Loading…
Reference in a new issue