From f291afbbe2d9b0e30081e4b01194f5a67943c566 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Thu, 3 Oct 2024 19:33:40 -0700 Subject: [PATCH] create testing module, add to docs --- docs/api/testing/cases.md | 7 ++ docs/api/testing/helpers.md | 7 ++ docs/api/testing/index.md | 8 ++ src/numpydantic/testing/__init__.py | 0 src/numpydantic/testing/cases.py | 132 ++++++++++++++++++++++ src/numpydantic/testing/helpers.py | 39 +++++++ tests/conftest.py | 164 +--------------------------- tests/test_interface/test_dask.py | 2 +- tests/test_interface/test_hdf5.py | 2 +- tests/test_interface/test_numpy.py | 2 +- tests/test_interface/test_zarr.py | 2 +- 11 files changed, 199 insertions(+), 166 deletions(-) create mode 100644 docs/api/testing/cases.md create mode 100644 docs/api/testing/helpers.md create mode 100644 docs/api/testing/index.md create mode 100644 src/numpydantic/testing/__init__.py create mode 100644 src/numpydantic/testing/cases.py create mode 100644 src/numpydantic/testing/helpers.py diff --git a/docs/api/testing/cases.md b/docs/api/testing/cases.md new file mode 100644 index 0000000..784bd62 --- /dev/null +++ b/docs/api/testing/cases.md @@ -0,0 +1,7 @@ +# cases + +```{eval-rst} +.. automodule:: numpydantic.testing.cases + :members: + :undoc-members: +``` \ No newline at end of file diff --git a/docs/api/testing/helpers.md b/docs/api/testing/helpers.md new file mode 100644 index 0000000..084901b --- /dev/null +++ b/docs/api/testing/helpers.md @@ -0,0 +1,7 @@ +# helpers + +```{eval-rst} +.. automodule:: numpydantic.testing.helpers + :members: + :undoc-members: +``` \ No newline at end of file diff --git a/docs/api/testing/index.md b/docs/api/testing/index.md new file mode 100644 index 0000000..687835b --- /dev/null +++ b/docs/api/testing/index.md @@ -0,0 +1,8 @@ +# testing + +Utilities for testing and 3rd-party interface development. + +```{toctree} +cases +helpers +``` \ No newline at end of file diff --git a/src/numpydantic/testing/__init__.py b/src/numpydantic/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/numpydantic/testing/cases.py b/src/numpydantic/testing/cases.py new file mode 100644 index 0000000..dd9274d --- /dev/null +++ b/src/numpydantic/testing/cases.py @@ -0,0 +1,132 @@ +import sys +from typing import TypeAlias, Union + +import numpy as np +from pydantic import BaseModel + +from numpydantic import NDArray, Shape +from numpydantic.dtype import Float, Integer, Number +from numpydantic.testing.helpers import ValidationCase + +if sys.version_info.minor >= 10: + from typing import TypeAlias + + YES_PIPE = True +else: + from typing_extensions import TypeAlias + + YES_PIPE = False + + + +class BasicModel(BaseModel): + x: int + + +class BadModel(BaseModel): + x: int + + +class SubClass(BasicModel): + pass + + +RGB_UNION: TypeAlias = Union[ + NDArray[Shape["* x, * y"], Number], + NDArray[Shape["* x, * y, 3 r_g_b"], Number], + NDArray[Shape["* x, * y, 3 r_g_b, 4 r_g_b_a"], Number], +] +NUMBER: TypeAlias = NDArray[Shape["*, *, *"], Number] +INTEGER: TypeAlias = NDArray[Shape["*, *, *"], Integer] +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] +DTYPE_CASES = [ + ValidationCase(dtype=float, passes=True), + ValidationCase(dtype=int, passes=False), + ValidationCase(dtype=np.uint8, passes=False), + ValidationCase(annotation=NUMBER, dtype=int, passes=True), + ValidationCase(annotation=NUMBER, dtype=float, passes=True), + ValidationCase(annotation=NUMBER, dtype=np.uint8, passes=True), + ValidationCase(annotation=NUMBER, dtype=np.float16, passes=True), + ValidationCase(annotation=NUMBER, dtype=str, passes=False), + ValidationCase(annotation=INTEGER, dtype=int, passes=True), + ValidationCase(annotation=INTEGER, dtype=np.uint8, passes=True), + ValidationCase(annotation=INTEGER, dtype=float, passes=False), + ValidationCase(annotation=INTEGER, dtype=np.float32, passes=False), + ValidationCase(annotation=INTEGER, dtype=str, passes=False), + ValidationCase(annotation=FLOAT, dtype=float, passes=True), + ValidationCase(annotation=FLOAT, dtype=np.float32, passes=True), + ValidationCase(annotation=FLOAT, dtype=int, passes=False), + ValidationCase(annotation=FLOAT, dtype=np.uint8, passes=False), + ValidationCase(annotation=FLOAT, dtype=str, passes=False), + ValidationCase(annotation=STRING, dtype=str, passes=True), + ValidationCase(annotation=STRING, dtype=int, passes=False), + ValidationCase(annotation=STRING, dtype=float, passes=False), + ValidationCase(annotation=MODEL, dtype=BasicModel, passes=True), + ValidationCase(annotation=MODEL, dtype=BadModel, passes=False), + ValidationCase(annotation=MODEL, dtype=int, passes=False), + ValidationCase(annotation=MODEL, dtype=SubClass, passes=True), + ValidationCase(annotation=UNION_TYPE, dtype=np.uint32, passes=True), + ValidationCase(annotation=UNION_TYPE, dtype=np.float32, passes=True), + ValidationCase(annotation=UNION_TYPE, dtype=np.uint64, passes=False), + ValidationCase(annotation=UNION_TYPE, dtype=np.float64, passes=False), + ValidationCase(annotation=UNION_TYPE, dtype=str, passes=False), +] + + +DTYPE_IDS = [ + "float", + "int", + "uint8", + "number-int", + "number-float", + "number-uint8", + "number-float16", + "number-str", + "integer-int", + "integer-uint8", + "integer-float", + "integer-float32", + "integer-str", + "float-float", + "float-float32", + "float-int", + "float-uint8", + "float-str", + "str-str", + "str-int", + "str-float", + "model-model", + "model-badmodel", + "model-int", + "model-subclass", + "union-type-uint32", + "union-type-float32", + "union-type-uint64", + "union-type-float64", + "union-type-str", +] + +if YES_PIPE: + DTYPE_CASES.extend( + [ + ValidationCase(annotation=UNION_PIPE, dtype=np.uint32, passes=True), + ValidationCase(annotation=UNION_PIPE, dtype=np.float32, passes=True), + ValidationCase(annotation=UNION_PIPE, dtype=np.uint64, passes=False), + ValidationCase(annotation=UNION_PIPE, dtype=np.float64, passes=False), + ValidationCase(annotation=UNION_PIPE, dtype=str, passes=False), + ] + ) + DTYPE_IDS.extend( + [ + "union-pipe-uint32", + "union-pipe-float32", + "union-pipe-uint64", + "union-pipe-float64", + "union-pipe-str", + ] + ) + diff --git a/src/numpydantic/testing/helpers.py b/src/numpydantic/testing/helpers.py new file mode 100644 index 0000000..4b196d0 --- /dev/null +++ b/src/numpydantic/testing/helpers.py @@ -0,0 +1,39 @@ +from typing import Any, Tuple, Type, Union + +from pydantic import BaseModel, ConfigDict, computed_field + +from numpydantic import NDArray, Shape +from numpydantic.dtype import Float + + +class ValidationCase(BaseModel): + """ + Test case for validating an array. + + Contains both the validating model and the parameterization for an array to + test in a given interface + """ + + annotation: Any = NDArray[Shape["10, 10, *"], Float] + """ + Array annotation used in the validating model + Any typed because the types of type annotations are weird + """ + shape: Tuple[int, ...] = (10, 10, 10) + """Shape of the array to validate""" + dtype: Union[Type, np.dtype] = float + """Dtype of the array to validate""" + passes: bool + """Whether the validation should pass or not""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + @computed_field() + def model(self) -> Type[BaseModel]: + """A model with a field ``array`` with the given annotation""" + annotation = self.annotation + + class Model(BaseModel): + array: annotation + + return Model diff --git a/tests/conftest.py b/tests/conftest.py index 96f8a7e..cea8ea6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,25 +1,10 @@ import sys -import pytest -from typing import Any, Tuple, Union, Type - -from pydantic import BaseModel, computed_field, ConfigDict -from numpydantic import NDArray, Shape -from numpydantic.ndarray import NDArrayMeta -from numpydantic.dtype import Float, Number, Integer -import numpy as np +from numpydantic.testing.cases import YES_PIPE, RGB_UNION, UNION_PIPE, DTYPE_CASES, DTYPE_IDS +from numpydantic.testing.helpers import ValidationCase from tests.fixtures import * -if sys.version_info.minor >= 10: - from typing import TypeAlias - - YES_PIPE = True -else: - from typing_extensions import TypeAlias - - YES_PIPE = False - def pytest_addoption(parser): parser.addoption( @@ -29,65 +14,7 @@ def pytest_addoption(parser): ) -class ValidationCase(BaseModel): - """ - Test case for validating an array. - Contains both the validating model and the parameterization for an array to - test in a given interface - """ - - annotation: Any = NDArray[Shape["10, 10, *"], Float] - """ - Array annotation used in the validating model - Any typed because the types of type annotations are weird - """ - shape: Tuple[int, ...] = (10, 10, 10) - """Shape of the array to validate""" - dtype: Union[Type, np.dtype] = float - """Dtype of the array to validate""" - passes: bool - """Whether the validation should pass or not""" - - model_config = ConfigDict(arbitrary_types_allowed=True) - - @computed_field() - def model(self) -> Type[BaseModel]: - """A model with a field ``array`` with the given annotation""" - annotation = self.annotation - - class Model(BaseModel): - array: annotation - - return Model - - -class BasicModel(BaseModel): - x: int - - -class BadModel(BaseModel): - x: int - - -class SubClass(BasicModel): - pass - - -RGB_UNION: TypeAlias = Union[ - NDArray[Shape["* x, * y"], Number], - NDArray[Shape["* x, * y, 3 r_g_b"], Number], - NDArray[Shape["* x, * y, 3 r_g_b, 4 r_g_b_a"], Number], -] - -NUMBER: TypeAlias = NDArray[Shape["*, *, *"], Number] -INTEGER: TypeAlias = NDArray[Shape["*, *, *"], Integer] -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]] -if YES_PIPE: - UNION_PIPE: TypeAlias = NDArray[Shape["*, *, *"], np.uint32 | np.float32] @pytest.fixture( @@ -127,93 +54,6 @@ def shape_cases(request) -> ValidationCase: return request.param -DTYPE_CASES = [ - ValidationCase(dtype=float, passes=True), - ValidationCase(dtype=int, passes=False), - ValidationCase(dtype=np.uint8, passes=False), - ValidationCase(annotation=NUMBER, dtype=int, passes=True), - ValidationCase(annotation=NUMBER, dtype=float, passes=True), - ValidationCase(annotation=NUMBER, dtype=np.uint8, passes=True), - ValidationCase(annotation=NUMBER, dtype=np.float16, passes=True), - ValidationCase(annotation=NUMBER, dtype=str, passes=False), - ValidationCase(annotation=INTEGER, dtype=int, passes=True), - ValidationCase(annotation=INTEGER, dtype=np.uint8, passes=True), - ValidationCase(annotation=INTEGER, dtype=float, passes=False), - ValidationCase(annotation=INTEGER, dtype=np.float32, passes=False), - ValidationCase(annotation=INTEGER, dtype=str, passes=False), - ValidationCase(annotation=FLOAT, dtype=float, passes=True), - ValidationCase(annotation=FLOAT, dtype=np.float32, passes=True), - ValidationCase(annotation=FLOAT, dtype=int, passes=False), - ValidationCase(annotation=FLOAT, dtype=np.uint8, passes=False), - ValidationCase(annotation=FLOAT, dtype=str, passes=False), - ValidationCase(annotation=STRING, dtype=str, passes=True), - ValidationCase(annotation=STRING, dtype=int, passes=False), - ValidationCase(annotation=STRING, dtype=float, passes=False), - ValidationCase(annotation=MODEL, dtype=BasicModel, passes=True), - ValidationCase(annotation=MODEL, dtype=BadModel, passes=False), - ValidationCase(annotation=MODEL, dtype=int, passes=False), - ValidationCase(annotation=MODEL, dtype=SubClass, passes=True), - ValidationCase(annotation=UNION_TYPE, dtype=np.uint32, passes=True), - ValidationCase(annotation=UNION_TYPE, dtype=np.float32, passes=True), - ValidationCase(annotation=UNION_TYPE, dtype=np.uint64, passes=False), - ValidationCase(annotation=UNION_TYPE, dtype=np.float64, passes=False), - ValidationCase(annotation=UNION_TYPE, dtype=str, passes=False), -] - -DTYPE_IDS = [ - "float", - "int", - "uint8", - "number-int", - "number-float", - "number-uint8", - "number-float16", - "number-str", - "integer-int", - "integer-uint8", - "integer-float", - "integer-float32", - "integer-str", - "float-float", - "float-float32", - "float-int", - "float-uint8", - "float-str", - "str-str", - "str-int", - "str-float", - "model-model", - "model-badmodel", - "model-int", - "model-subclass", - "union-type-uint32", - "union-type-float32", - "union-type-uint64", - "union-type-float64", - "union-type-str", -] - -if YES_PIPE: - DTYPE_CASES.extend( - [ - ValidationCase(annotation=UNION_PIPE, dtype=np.uint32, passes=True), - ValidationCase(annotation=UNION_PIPE, dtype=np.float32, passes=True), - ValidationCase(annotation=UNION_PIPE, dtype=np.uint64, passes=False), - ValidationCase(annotation=UNION_PIPE, dtype=np.float64, passes=False), - ValidationCase(annotation=UNION_PIPE, dtype=str, passes=False), - ] - ) - DTYPE_IDS.extend( - [ - "union-pipe-uint32", - "union-pipe-float32", - "union-pipe-uint64", - "union-pipe-float64", - "union-pipe-str", - ] - ) - - @pytest.fixture(scope="module", params=DTYPE_CASES, ids=DTYPE_IDS) def dtype_cases(request) -> ValidationCase: return request.param diff --git a/tests/test_interface/test_dask.py b/tests/test_interface/test_dask.py index c3b70e0..919455f 100644 --- a/tests/test_interface/test_dask.py +++ b/tests/test_interface/test_dask.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, ValidationError from numpydantic.interface import DaskInterface from numpydantic.exceptions import DtypeError, ShapeError -from tests.conftest import ValidationCase +from numpydantic.testing.helpers import ValidationCase pytestmark = pytest.mark.dask diff --git a/tests/test_interface/test_hdf5.py b/tests/test_interface/test_hdf5.py index bd94810..361eb90 100644 --- a/tests/test_interface/test_hdf5.py +++ b/tests/test_interface/test_hdf5.py @@ -12,7 +12,7 @@ from numpydantic.interface import H5Interface from numpydantic.interface.hdf5 import H5ArrayPath, H5Proxy from numpydantic.exceptions import DtypeError, ShapeError -from tests.conftest import ValidationCase +from numpydantic.testing.helpers import ValidationCase pytestmark = pytest.mark.hdf5 diff --git a/tests/test_interface/test_numpy.py b/tests/test_interface/test_numpy.py index bfb4c4d..17bfe9b 100644 --- a/tests/test_interface/test_numpy.py +++ b/tests/test_interface/test_numpy.py @@ -3,7 +3,7 @@ import pytest from pydantic import ValidationError, BaseModel from numpydantic.exceptions import DtypeError, ShapeError -from tests.conftest import ValidationCase +from numpydantic.testing.helpers import ValidationCase pytestmark = pytest.mark.numpy diff --git a/tests/test_interface/test_zarr.py b/tests/test_interface/test_zarr.py index 6b21b20..2fa086c 100644 --- a/tests/test_interface/test_zarr.py +++ b/tests/test_interface/test_zarr.py @@ -10,7 +10,7 @@ from numpydantic.interface import ZarrInterface from numpydantic.interface.zarr import ZarrArrayPath from numpydantic.exceptions import DtypeError, ShapeError -from tests.conftest import ValidationCase +from numpydantic.testing.helpers import ValidationCase pytestmark = pytest.mark.zarr