tests and no covers continue

This commit is contained in:
sneakers-the-rat 2024-05-14 20:18:04 -07:00
parent 0ee371ad05
commit 9b13226164
Signed by untrusted user who does not match committer: jonny
GPG key ID: 6DCB96EF1E4D232D
10 changed files with 198 additions and 9 deletions

View file

@ -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

View file

@ -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"

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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):

View 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()

View file

@ -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)