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

p2p-ld / numpydantic / 27167875897

08 Jun 2026 09:24PM UTC coverage: 96.502% (-0.6%) from 97.114%
27167875897

Pull #71

github

web-flow
Merge 544cd0f65 into a67c8ebe8
Pull Request #71: Validate object arrays of python datetime against datetime annotation (#46)

114 of 120 new or added lines in 8 files covered. (95.0%)

11 existing lines in 5 files now uncovered.

2014 of 2087 relevant lines covered (96.5%)

4.82 hits per line

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

97.33
/src/numpydantic/interface/numpy.py
1
"""
2
Interface to numpy arrays
3
"""
4

5
import contextlib
5✔
6
from typing import Any, Literal
5✔
7

8
from pydantic import BaseModel, SerializationInfo
5✔
9

10
from numpydantic.interface.interface import Interface, JsonDict
5✔
11
from numpydantic.interface.typing import ConstructorSpec, InterfaceTyping
5✔
12

13
try:
5✔
14
    import numpy as np
5✔
15
    from numpy import ndarray
5✔
16

17
    ENABLED = True
5✔
18

19
except ImportError:  # pragma: no cover
20
    ENABLED = False
21
    ndarray = None
22
    np = None
23

24

25
class NumpyJsonDict(JsonDict):
5✔
26
    """
27
    JSON-able roundtrip representation of numpy array
28
    """
29

30
    type: Literal["numpy"]
5✔
31
    dtype: str
5✔
32
    value: list
5✔
33
    # allow shape to be None for backwards compatibility.
34
    shape: tuple[int, ...] | None = None
5✔
35
    # store absolute python identifier for objects
36
    object_cls: str | None = None
5✔
37

38
    def to_array_input(self) -> ndarray:
5✔
39
        """
40
        Construct a numpy array
41
        """
42
        array = np.array(self.value, dtype=self.dtype)
5✔
43

44
        # recast to object, if relevant
45
        if self.dtype == "object" and self.object_cls is not None:
5✔
46
            array = self.cast_objects(array, self.object_cls)
5✔
47

48
        if self.shape is not None and array.shape != self.shape:
5✔
49
            array = self.reshape_input(array, self.shape)
5✔
50
        return array
5✔
51

52

53
class NumpyTyping(InterfaceTyping):
5✔
54
    """Static-typing companion for :class:`NumpyInterface`."""
55

56
    constructors = (
5✔
57
        ConstructorSpec(fullname="numpy.ones"),
58
        ConstructorSpec(fullname="numpy.zeros"),
59
        ConstructorSpec(fullname="numpy.empty"),
60
        ConstructorSpec(fullname="numpy.full"),
61
        # Newer numpy stubs route the public ``np.zeros`` etc. through a
62
        # ``Final[_ConstructorEmpty]`` protocol instance, so mypy sees the
63
        # call as a method on that protocol.
64
        ConstructorSpec(
65
            fullname="numpy._core.multiarray._ConstructorEmpty.__call__",
66
            mode="method",
67
        ),
68
    )
69

70
    @classmethod
5✔
71
    def emit_imports(cls) -> list[str]:
5✔
72
        """Just importing numpy over here!"""
73
        return ["import numpy"]
5✔
74

75
    @classmethod
5✔
76
    def emit_constructor_source(cls, shape: tuple[int, ...], dtype: str) -> str | None:
5✔
77
        """Constructor using :func:`numpy.zeros`"""
78
        return f"numpy.zeros({tuple(shape)!r}, dtype={dtype})"
5✔
79

80

81
class NumpyInterface(Interface):
5✔
82
    """
83
    Numpy :class:`~numpy.ndarray` s!
84
    """
85

86
    name = "numpy"
5✔
87
    input_types = (ndarray,)
5✔
88
    return_type = ndarray
5✔
89
    json_model = NumpyJsonDict
5✔
90
    priority = -999
5✔
91
    """
5✔
92
    The numpy interface is usually the interface of last resort.
93
    We want to use any more specific interface that we might have,
94
    because the numpy interface checks for anything that could be coerced
95
    to a numpy array (see :meth:`.NumpyInterface.check` )
96
    """
97
    typing = NumpyTyping
5✔
98

99
    @classmethod
5✔
100
    def check(cls, array: Any) -> bool:
5✔
101
        """
102
        Check that this is in fact a numpy ndarray or something that can be
103
        coerced to one
104
        """
105
        if array is None:
5✔
106
            return False
×
107

108
        if isinstance(array, ndarray):
5✔
109
            return True
5✔
110
        elif isinstance(array, dict):
5✔
111
            return NumpyJsonDict.is_valid(array)
5✔
112
        else:
113
            try:
5✔
114
                _ = np.array(array)
5✔
115
                return True
5✔
116
            except Exception:
5✔
117
                return False
5✔
118

119
    def before_validation(self, array: Any) -> ndarray:
5✔
120
        """
121
        Coerce to an ndarray. We have already checked if coercion is possible
122
        in :meth:`.check`
123
        """
124
        if not isinstance(array, ndarray):
5✔
125
            array = np.array(array)
5✔
126

127
        try:
5✔
128
            # try to convert a dict to a basemodel, if relevant
129
            # this is the *only* dtype coercion that we should attempt to do,
130
            # because pydantic treats dicts as equivalent to models in inputs.
131
            # other coercion when e.g. deserializing from JSON should go
132
            # in the JSONDict object's deserialization methods.
133
            if (
5✔
134
                issubclass(self.dtype, BaseModel)
135
                and len(array) > 0
136
                and isinstance(array.flat[0], dict)
137
            ):
UNCOV
138
                array = np.vectorize(lambda x: self.dtype(**x))(array)
×
139
        except TypeError:
5✔
140
            # fine, dtype isn't a type
141
            pass
5✔
142

143
        return array
5✔
144

145
    @classmethod
5✔
146
    def enabled(cls) -> bool:
5✔
147
        """Check that numpy is present in the environment"""
148
        return ENABLED
5✔
149

150
    @classmethod
5✔
151
    def to_json(cls, array: ndarray, info: SerializationInfo = None) -> list | JsonDict:
5✔
152
        """
153
        Convert an array of :attr:`.return_type` to a JSON-compatible format using
154
        base python types
155
        """
156
        if not isinstance(array, np.ndarray):  # pragma: no cover
157
            array = np.array(array)
158

159
        json_array = [array.tolist()] if array.ndim == 0 else array.tolist()
5✔
160

161
        if info.round_trip:
5✔
162
            # store object dtype
163
            dtype = str(array.dtype)
5✔
164
            object_cls = None
5✔
165
            if dtype == "object":
5✔
166
                with contextlib.suppress(AttributeError, IndexError):
5✔
167
                    obj = array.ravel()[0].__class__
5✔
168
                    object_cls = f"{obj.__module__}.{obj.__name__}"
5✔
169

170
            json_array = NumpyJsonDict(
5✔
171
                type=cls.name,
172
                dtype=dtype,
173
                value=json_array,
174
                shape=array.shape,
175
                object_cls=object_cls,
176
            )
177
        return json_array
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc