• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

p2p-ld / numpydantic / 10382572278

14 Aug 2024 06:29AM UTC coverage: 98.177% (+0.004%) from 98.173%
10382572278

push

github

web-flow
Merge pull request #8 from p2p-ld/constructable

Make NDArray callable as a functional validator

3 of 3 new or added lines in 1 file covered. (100.0%)

808 of 823 relevant lines covered (98.18%)

9.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.06
/src/numpydantic/ndarray.py
1
"""
2
Extension of nptyping NDArray for pydantic that allows for JSON-Schema serialization
3

4
.. note::
5

6
    This module should *only* have the :class:`.NDArray` class in it, because the
7
    type stub ``ndarray.pyi`` is only created for :class:`.NDArray` . Otherwise,
8
    type checkers will complain about using any helper functions elsewhere -
9
    those all belong in :mod:`numpydantic.schema` .
10

11
    Keeping with nptyping's style, NDArrayMeta is in this module even if it's
12
    excluded from the type stub.
13

14
"""
15

16
from typing import TYPE_CHECKING, Any, Tuple
10✔
17

18
import numpy as np
10✔
19
from pydantic import GetJsonSchemaHandler
10✔
20
from pydantic_core import core_schema
10✔
21

22
from numpydantic.dtype import DType
10✔
23
from numpydantic.exceptions import InterfaceError
10✔
24
from numpydantic.interface import Interface
10✔
25
from numpydantic.maps import python_to_nptyping
10✔
26
from numpydantic.schema import (
10✔
27
    _handler_type,
28
    _jsonize_array,
29
    get_validate_interface,
30
    make_json_schema,
31
)
32
from numpydantic.types import DtypeType, NDArrayType, ShapeType
10✔
33
from numpydantic.vendor.nptyping.error import InvalidArgumentsError
10✔
34
from numpydantic.vendor.nptyping.ndarray import NDArrayMeta as _NDArrayMeta
10✔
35
from numpydantic.vendor.nptyping.nptyping_type import NPTypingType
10✔
36
from numpydantic.vendor.nptyping.structure import Structure
10✔
37
from numpydantic.vendor.nptyping.structure_expression import check_type_names
10✔
38
from numpydantic.vendor.nptyping.typing_ import (
10✔
39
    dtype_per_name,
40
)
41

42
if TYPE_CHECKING:  # pragma: no cover
43
    from nptyping.base_meta_classes import SubscriptableMeta
44

45
    from numpydantic import Shape
46

47

48
class NDArrayMeta(_NDArrayMeta, implementation="NDArray"):
10✔
49
    """
50
    Hooking into nptyping's array metaclass to override methods pending
51
    completion of the transition away from nptyping
52
    """
53

54
    if TYPE_CHECKING:  # pragma: no cover
55
        __getitem__ = SubscriptableMeta.__getitem__
56

57
    def __call__(cls, val: NDArrayType) -> NDArrayType:
10✔
58
        """Call ndarray as a validator function"""
59
        return get_validate_interface(cls.__args__[0], cls.__args__[1])(val)
10✔
60

61
    def __instancecheck__(self, instance: Any):
10✔
62
        """
63
        Extended type checking that determines whether
64

65
        1) the ``type`` of the given instance is one of those in
66
            :meth:`.Interface.input_types`
67

68
        but also
69

70
        2) it satisfies the constraints set on the :class:`.NDArray` annotation
71

72
        Args:
73
            instance (:class:`typing.Any`): Thing to check!
74

75
        Returns:
76
            bool: ``True`` if matches constraints, ``False`` otherwise.
77
        """
78
        shape, dtype = self.__args__
10✔
79
        try:
10✔
80
            interface_cls = Interface.match(instance, fast=True)
10✔
81
            interface = interface_cls(shape, dtype)
10✔
82
            _ = interface.validate(instance)
10✔
83
            return True
10✔
84
        except InterfaceError:
10✔
85
            return False
10✔
86

87
    def _get_shape(cls, dtype_candidate: Any) -> "Shape":
10✔
88
        """
89
        Override of base method to use our local definition of shape
90
        """
91
        from numpydantic.shape import Shape
10✔
92

93
        if dtype_candidate is Any or dtype_candidate is Shape:
10✔
94
            shape = Any
10✔
95
        elif issubclass(dtype_candidate, Shape):
10✔
96
            shape = dtype_candidate
10✔
97
        elif cls._is_literal_like(dtype_candidate):
×
98
            shape_expression = dtype_candidate.__args__[0]
×
99
            shape = Shape[shape_expression]
×
100
        else:
101
            raise InvalidArgumentsError(
×
102
                f"Unexpected argument '{dtype_candidate}', expecting"
103
                " Shape[<ShapeExpression>]"
104
                " or Literal[<ShapeExpression>]"
105
                " or typing.Any."
106
            )
107
        return shape
10✔
108

109
    def _get_dtype(cls, dtype_candidate: Any) -> DType:
10✔
110
        """
111
        Override of base _get_dtype method to allow for compound tuple types
112
        """
113
        if dtype_candidate in python_to_nptyping:
10✔
114
            dtype_candidate = python_to_nptyping[dtype_candidate]
10✔
115
        is_dtype = isinstance(dtype_candidate, type) and issubclass(
10✔
116
            dtype_candidate, np.generic
117
        )
118

119
        if dtype_candidate is Any:
10✔
120
            dtype = Any
10✔
121
        elif is_dtype:
10✔
122
            dtype = dtype_candidate
10✔
123
        elif issubclass(dtype_candidate, Structure):  # pragma: no cover
124
            dtype = dtype_candidate
125
            check_type_names(dtype, dtype_per_name)
126
        elif cls._is_literal_like(dtype_candidate):  # pragma: no cover
127
            structure_expression = dtype_candidate.__args__[0]
128
            dtype = Structure[structure_expression]
129
            check_type_names(dtype, dtype_per_name)
130
        elif isinstance(dtype_candidate, tuple):  # pragma: no cover
131
            dtype = tuple([cls._get_dtype(dt) for dt in dtype_candidate])
132
        else:
133
            # arbitrary dtype - allow failure elsewhere :)
134
            dtype = dtype_candidate
10✔
135

136
        return dtype
10✔
137

138
    def _dtype_to_str(cls, dtype: Any) -> str:
10✔
139
        if dtype is Any:
10✔
140
            result = "Any"
10✔
141
        elif issubclass(dtype, Structure):
10✔
142
            result = str(dtype)
×
143
        elif isinstance(dtype, tuple):
10✔
144
            result = ", ".join([str(dt) for dt in dtype])
10✔
145
        return result
10✔
146

147

148
class NDArray(NPTypingType, metaclass=NDArrayMeta):
10✔
149
    """
150
    Constrained array type allowing npytyping syntax for dtype and shape validation
151
    and serialization.
152

153
    This class is not intended to be instantiated or used for type checking, it
154
    implements the ``__get_pydantic_core_schema__` method to invoke
155
    the relevant :ref:`interface <Interfaces>` for validation and serialization.
156

157
    References:
158
        - https://docs.pydantic.dev/latest/usage/types/custom/#handling-third-party-types
159
    """
160

161
    __args__: Tuple[ShapeType, DtypeType] = (Any, Any)
10✔
162

163
    @classmethod
10✔
164
    def __get_pydantic_core_schema__(
10✔
165
        cls,
166
        _source_type: "NDArray",
167
        _handler: _handler_type,
168
    ) -> core_schema.CoreSchema:
169
        shape, dtype = _source_type.__args__
10✔
170
        shape: ShapeType
171
        dtype: DtypeType
172

173
        # get pydantic core schema as a list of lists for JSON schema
174
        list_schema = make_json_schema(shape, dtype, _handler)
10✔
175

176
        return core_schema.json_or_python_schema(
10✔
177
            json_schema=list_schema,
178
            python_schema=core_schema.with_info_plain_validator_function(
179
                get_validate_interface(shape, dtype)
180
            ),
181
            serialization=core_schema.plain_serializer_function_ser_schema(
182
                _jsonize_array, when_used="json", info_arg=True
183
            ),
184
        )
185

186
    @classmethod
10✔
187
    def __get_pydantic_json_schema__(
10✔
188
        cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
189
    ) -> core_schema.JsonSchema:
190
        json_schema = handler(schema)
10✔
191
        json_schema = handler.resolve_ref_schema(json_schema)
10✔
192

193
        dtype = cls.__args__[1]
10✔
194
        if not isinstance(dtype, tuple) and dtype.__module__ not in (
10✔
195
            "builtins",
196
            "typing",
197
        ):
198
            json_schema["dtype"] = ".".join([dtype.__module__, dtype.__name__])
10✔
199

200
        return json_schema
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc