Source code for numpydantic.ndarray
"""
Extension of nptyping NDArray for pydantic that allows for JSON-Schema serialization
.. note::
This module should *only* have the :class:`.NDArray` class in it, because the
type stub ``ndarray.pyi`` is only created for :class:`.NDArray` . Otherwise,
type checkers will complain about using any helper functions elsewhere -
those all belong in :mod:`numpydantic.schema` .
Keeping with nptyping's style, NDArrayMeta is in this module even if it's
excluded from the type stub.
"""
from typing import TYPE_CHECKING, Any, Tuple
import numpy as np
from pydantic import GetJsonSchemaHandler
from pydantic_core import core_schema
from numpydantic.dtype import DType
from numpydantic.exceptions import InterfaceError
from numpydantic.interface import Interface
from numpydantic.maps import python_to_nptyping
from numpydantic.schema import (
_handler_type,
_jsonize_array,
get_validate_interface,
make_json_schema,
)
from numpydantic.types import DtypeType, 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
from numpydantic.vendor.nptyping.structure import Structure
from numpydantic.vendor.nptyping.structure_expression import check_type_names
from numpydantic.vendor.nptyping.typing_ import (
dtype_per_name,
)
if TYPE_CHECKING: # pragma: no cover
from nptyping.base_meta_classes import SubscriptableMeta
from numpydantic import Shape
[docs]
class NDArrayMeta(_NDArrayMeta, implementation="NDArray"):
"""
Hooking into nptyping's array metaclass to override methods pending
completion of the transition away from nptyping
"""
if TYPE_CHECKING: # pragma: no cover
__getitem__ = SubscriptableMeta.__getitem__
[docs]
def __instancecheck__(self, instance: Any):
"""
Extended type checking that determines whether
1) the ``type`` of the given instance is one of those in
:meth:`.Interface.input_types`
but also
2) it satisfies the constraints set on the :class:`.NDArray` annotation
Args:
instance (:class:`typing.Any`): Thing to check!
Returns:
bool: ``True`` if matches constraints, ``False`` otherwise.
"""
shape, dtype = self.__args__
try:
interface_cls = Interface.match(instance, fast=True)
interface = interface_cls(shape, dtype)
_ = interface.validate(instance)
return True
except InterfaceError:
return False
def _get_shape(cls, dtype_candidate: Any) -> "Shape":
"""
Override of base method to use our local definition of shape
"""
from numpydantic.shape import Shape
if dtype_candidate is Any or dtype_candidate is Shape:
shape = Any
elif issubclass(dtype_candidate, Shape):
shape = dtype_candidate
elif cls._is_literal_like(dtype_candidate):
shape_expression = dtype_candidate.__args__[0]
shape = Shape[shape_expression]
else:
raise InvalidArgumentsError(
f"Unexpected argument '{dtype_candidate}', expecting"
" Shape[<ShapeExpression>]"
" or Literal[<ShapeExpression>]"
" or typing.Any."
)
return shape
def _get_dtype(cls, dtype_candidate: Any) -> DType:
"""
Override of base _get_dtype method to allow for compound tuple types
"""
if dtype_candidate in python_to_nptyping:
dtype_candidate = python_to_nptyping[dtype_candidate]
is_dtype = isinstance(dtype_candidate, type) and issubclass(
dtype_candidate, np.generic
)
if dtype_candidate is Any:
dtype = Any
elif is_dtype:
dtype = dtype_candidate
elif issubclass(dtype_candidate, Structure): # pragma: no cover
dtype = dtype_candidate
check_type_names(dtype, dtype_per_name)
elif cls._is_literal_like(dtype_candidate): # pragma: no cover
structure_expression = dtype_candidate.__args__[0]
dtype = Structure[structure_expression]
check_type_names(dtype, dtype_per_name)
elif isinstance(dtype_candidate, tuple): # pragma: no cover
dtype = tuple([cls._get_dtype(dt) for dt in dtype_candidate])
else:
# arbitrary dtype - allow failure elsewhere :)
dtype = dtype_candidate
return dtype
def _dtype_to_str(cls, dtype: Any) -> str:
if dtype is Any:
result = "Any"
elif issubclass(dtype, Structure):
result = str(dtype)
elif isinstance(dtype, tuple):
result = ", ".join([str(dt) for dt in dtype])
return result
[docs]
class NDArray(NPTypingType, metaclass=NDArrayMeta):
"""
Constrained array type allowing npytyping syntax for dtype and shape validation
and serialization.
This class is not intended to be instantiated or used for type checking, it
implements the ``__get_pydantic_core_schema__` method to invoke
the relevant :ref:`interface <Interfaces>` for validation and serialization.
References:
- https://docs.pydantic.dev/latest/usage/types/custom/#handling-third-party-types
"""
__args__: Tuple[ShapeType, DtypeType] = (Any, Any)
@classmethod
def __get_pydantic_core_schema__(
cls,
_source_type: "NDArray",
_handler: _handler_type,
) -> core_schema.CoreSchema:
shape, dtype = _source_type.__args__
shape: ShapeType
dtype: DtypeType
# get pydantic core schema as a list of lists for JSON schema
list_schema = make_json_schema(shape, dtype, _handler)
return core_schema.json_or_python_schema(
json_schema=list_schema,
python_schema=core_schema.with_info_plain_validator_function(
get_validate_interface(shape, dtype)
),
serialization=core_schema.plain_serializer_function_ser_schema(
_jsonize_array, when_used="json", info_arg=True
),
)
@classmethod
def __get_pydantic_json_schema__(
cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
) -> core_schema.JsonSchema:
json_schema = handler(schema)
json_schema = handler.resolve_ref_schema(json_schema)
dtype = cls.__args__[1]
if not isinstance(dtype, tuple) and dtype.__module__ not in (
"builtins",
"typing",
):
json_schema["dtype"] = ".".join([dtype.__module__, dtype.__name__])
return json_schema