From 3304ab3d8802b22746587c78e2abf1bb05a91ea3 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Tue, 13 Aug 2024 23:05:28 -0700 Subject: [PATCH 1/2] update changelog, bump version --- src/numpydantic/ndarray.py | 6 +++++- src/numpydantic/vendor/nptyping/__init__.py | 4 +++- src/numpydantic/vendor/nptyping/ndarray.py | 2 -- tests/test_ndarray.py | 14 ++++++++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/numpydantic/ndarray.py b/src/numpydantic/ndarray.py index 8756ae0..d951d3a 100644 --- a/src/numpydantic/ndarray.py +++ b/src/numpydantic/ndarray.py @@ -29,7 +29,7 @@ from numpydantic.schema import ( get_validate_interface, make_json_schema, ) -from numpydantic.types import DtypeType, ShapeType +from numpydantic.types import DtypeType, NDArrayType, ShapeType from numpydantic.vendor.nptyping.error import InvalidArgumentsError from numpydantic.vendor.nptyping.ndarray import NDArrayMeta as _NDArrayMeta from numpydantic.vendor.nptyping.nptyping_type import NPTypingType @@ -54,6 +54,10 @@ class NDArrayMeta(_NDArrayMeta, implementation="NDArray"): if TYPE_CHECKING: # pragma: no cover __getitem__ = SubscriptableMeta.__getitem__ + def __call__(cls, val: NDArrayType) -> NDArrayType: + """Call ndarray as a validator function""" + return get_validate_interface(cls.__args__[0], cls.__args__[1])(val) + def __instancecheck__(self, instance: Any): """ Extended type checking that determines whether diff --git a/src/numpydantic/vendor/nptyping/__init__.py b/src/numpydantic/vendor/nptyping/__init__.py index 4ca8fdb..0a51854 100644 --- a/src/numpydantic/vendor/nptyping/__init__.py +++ b/src/numpydantic/vendor/nptyping/__init__.py @@ -32,7 +32,9 @@ from numpydantic.vendor.nptyping.error import ( ) from numpydantic.vendor.nptyping.ndarray import NDArray from numpydantic.vendor.nptyping.package_info import __version__ -from numpydantic.vendor.nptyping.pandas_.dataframe import DataFrame + +# don't import unnecessarily since we don't use it +# from numpydantic.vendor.nptyping.pandas_.dataframe import DataFrame from numpydantic.vendor.nptyping.recarray import RecArray from numpydantic.vendor.nptyping.shape import Shape from numpydantic.vendor.nptyping.shape_expression import ( diff --git a/src/numpydantic/vendor/nptyping/ndarray.py b/src/numpydantic/vendor/nptyping/ndarray.py index 90a4793..19f06b4 100644 --- a/src/numpydantic/vendor/nptyping/ndarray.py +++ b/src/numpydantic/vendor/nptyping/ndarray.py @@ -31,7 +31,6 @@ import numpy as np from numpydantic.vendor.nptyping.base_meta_classes import ( FinalMeta, ImmutableMeta, - InconstructableMeta, MaybeCheckableMeta, PrintableMeta, SubscriptableMeta, @@ -54,7 +53,6 @@ from numpydantic.vendor.nptyping.typing_ import ( class NDArrayMeta( SubscriptableMeta, - InconstructableMeta, ImmutableMeta, FinalMeta, MaybeCheckableMeta, diff --git a/tests/test_ndarray.py b/tests/test_ndarray.py index 9883c2a..f92a66d 100644 --- a/tests/test_ndarray.py +++ b/tests/test_ndarray.py @@ -350,3 +350,17 @@ def test_instancecheck(): return array my_function(np.zeros((1, 2, 3), int)) + + +def test_callable(): + """ + NDArray objects are callable to validate and cast + Don't test validation here, just that we can be called + """ + annotation = NDArray[Shape["3"], int] + array = np.array([1, 2, 3], dtype=int) + validated = annotation(array) + assert validated is array + + with pytest.raises(DtypeError): + _ = annotation(np.zeros((1, 2, 3))) From 35ec2f16518813da6541726837b156e964b2453e Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Tue, 13 Aug 2024 23:27:12 -0700 Subject: [PATCH 2/2] update changelog, bump version --- docs/changelog.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 938d9bd..683372e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,61 @@ ## 1.* +### 1.3.3 - 24-08-13 - Callable type annotations + +Problem, when you use a numpydantic `"wrap"` validator, it gives the annotation as a `handler` function. + +So this is effectively what happens + +```python +@field_validator("*", mode="wrap") +@classmethod +def cast_specified_columns( + cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo +) -> Any: + # where handler is the callable here + # so + # return handler(val) + + return NDArray[Any, Any](val) +``` + +where `Any, Any` is whatever you had put in there. + +So this makes it so you can use an annotation as a functional validator. it looks a little bit whacky but idk it makes sense as a PARAMETERIZED TYPE + +```python +>>> from numpydantic import NDArray, Shape +>>> import numpy as np + +>>> array = np.array([1,2,3], dtype=int) +>>> validated = NDArray[Shape["3"], int](array) +>>> assert validated is array +True + +>>> bad_array = np.array([1,2,3,4], dtype=int) +>>> _ = NDArray[Shape["3"], int](bad_array) + 175 """ + 176 Raise a ShapeError if the shape is invalid. + 177 + 178 Raises: + 179 :class:`~numpydantic.exceptions.ShapeError` + 180 """ + 181 if not valid: +--> 182 raise ShapeError( + 183 f"Invalid shape! expected shape {self.shape.prepared_args}, " + 184 f"got shape {shape}" + 185 ) + +ShapeError: Invalid shape! expected shape ['3'], got shape (4,) + +``` + +**Performance:** +- Don't import the pandas module if we don't have to, since we are not + using it. This shaves ~600ms off import time. + + ### 1.3.2 - 24-08-12 - Allow subclasses of dtypes (also when using objects for dtypes, subclasses of that object are allowed to validate) diff --git a/pyproject.toml b/pyproject.toml index e1ffa3d..8cdc372 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "numpydantic" -version = "1.3.2" +version = "1.3.3" description = "Type and shape validation and serialization for numpy arrays in pydantic models" authors = [ {name = "sneakers-the-rat", email = "sneakers-the-rat@protonmail.com"},