Merge pull request #8 from p2p-ld/constructable

Make NDArray callable as a functional validator
This commit is contained in:
Jonny Saunders 2024-08-13 23:29:20 -07:00 committed by GitHub
commit 6a397a9aba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 78 additions and 5 deletions

View file

@ -2,6 +2,61 @@
## 1.* ## 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 ### 1.3.2 - 24-08-12 - Allow subclasses of dtypes
(also when using objects for dtypes, subclasses of that object are allowed to validate) (also when using objects for dtypes, subclasses of that object are allowed to validate)

View file

@ -1,6 +1,6 @@
[project] [project]
name = "numpydantic" name = "numpydantic"
version = "1.3.2" version = "1.3.3"
description = "Type and shape validation and serialization for numpy arrays in pydantic models" description = "Type and shape validation and serialization for numpy arrays in pydantic models"
authors = [ authors = [
{name = "sneakers-the-rat", email = "sneakers-the-rat@protonmail.com"}, {name = "sneakers-the-rat", email = "sneakers-the-rat@protonmail.com"},

View file

@ -29,7 +29,7 @@ from numpydantic.schema import (
get_validate_interface, get_validate_interface,
make_json_schema, 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.error import InvalidArgumentsError
from numpydantic.vendor.nptyping.ndarray import NDArrayMeta as _NDArrayMeta from numpydantic.vendor.nptyping.ndarray import NDArrayMeta as _NDArrayMeta
from numpydantic.vendor.nptyping.nptyping_type import NPTypingType from numpydantic.vendor.nptyping.nptyping_type import NPTypingType
@ -54,6 +54,10 @@ class NDArrayMeta(_NDArrayMeta, implementation="NDArray"):
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
__getitem__ = SubscriptableMeta.__getitem__ __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): def __instancecheck__(self, instance: Any):
""" """
Extended type checking that determines whether Extended type checking that determines whether

View file

@ -32,7 +32,9 @@ from numpydantic.vendor.nptyping.error import (
) )
from numpydantic.vendor.nptyping.ndarray import NDArray from numpydantic.vendor.nptyping.ndarray import NDArray
from numpydantic.vendor.nptyping.package_info import __version__ 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.recarray import RecArray
from numpydantic.vendor.nptyping.shape import Shape from numpydantic.vendor.nptyping.shape import Shape
from numpydantic.vendor.nptyping.shape_expression import ( from numpydantic.vendor.nptyping.shape_expression import (

View file

@ -31,7 +31,6 @@ import numpy as np
from numpydantic.vendor.nptyping.base_meta_classes import ( from numpydantic.vendor.nptyping.base_meta_classes import (
FinalMeta, FinalMeta,
ImmutableMeta, ImmutableMeta,
InconstructableMeta,
MaybeCheckableMeta, MaybeCheckableMeta,
PrintableMeta, PrintableMeta,
SubscriptableMeta, SubscriptableMeta,
@ -54,7 +53,6 @@ from numpydantic.vendor.nptyping.typing_ import (
class NDArrayMeta( class NDArrayMeta(
SubscriptableMeta, SubscriptableMeta,
InconstructableMeta,
ImmutableMeta, ImmutableMeta,
FinalMeta, FinalMeta,
MaybeCheckableMeta, MaybeCheckableMeta,

View file

@ -350,3 +350,17 @@ def test_instancecheck():
return array return array
my_function(np.zeros((1, 2, 3), int)) 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)))