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

p2p-ld / numpydantic / 27169184073

08 Jun 2026 09:49PM UTC coverage: 96.981% (-0.1%) from 97.114%
27169184073

Pull #71

github

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

116 of 120 new or added lines in 8 files covered. (96.67%)

2 existing lines in 2 files now uncovered.

2024 of 2087 relevant lines covered (96.98%)

6.76 hits per line

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

96.37
/src/numpydantic/interface/interface.py
1
"""
2
Base Interface metaclass
3
"""
4

5
import builtins
7✔
6
import importlib
7✔
7
import inspect
7✔
8
import sys
7✔
9
import warnings
7✔
10
from abc import ABC, abstractmethod
7✔
11
from datetime import datetime
7✔
12
from functools import lru_cache
7✔
13
from importlib.metadata import PackageNotFoundError, version
7✔
14
from operator import attrgetter
7✔
15
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, Union
7✔
16

17
if TYPE_CHECKING:
7✔
18
    from numpydantic.interface.typing import InterfaceTyping
×
19

20
import numpy as np
7✔
21
from pydantic import BaseModel, SerializationInfo, ValidationError
7✔
22

23
from numpydantic.exceptions import (
7✔
24
    DtypeError,
25
    MarkMismatchError,
26
    NoMatchError,
27
    ShapeError,
28
    TooManyMatchesError,
29
)
30
from numpydantic.types import DtypeType, NDArrayType, ShapeType
7✔
31
from numpydantic.validation import validate_dtype, validate_shape
7✔
32

33
T = TypeVar("T", bound=NDArrayType)
7✔
34
U = TypeVar("U", bound="JsonDict")
7✔
35
V = TypeVar("V")  # input type
7✔
36
W = TypeVar("W")  # Any type in handle_input
7✔
37

38

39
class InterfaceMark(BaseModel):
7✔
40
    """JSON-able mark to be able to round-trip json dumps"""
41

42
    module: str
7✔
43
    cls: str
7✔
44
    name: str
7✔
45
    version: str
7✔
46

47
    def is_valid(self, cls: type["Interface"], raise_on_error: bool = False) -> bool:
7✔
48
        """
49
        Check that a given interface matches the mark.
50

51
        Args:
52
            cls (Type): Interface type to check
53
            raise_on_error (bool): Raise an ``MarkMismatchError`` when the match
54
                is incorrect
55

56
        Returns:
57
            bool
58

59
        Raises:
60
            :class:`.MarkMismatchError` if requested by ``raise_on_error``
61
            for an invalid match
62
        """
63
        mark = cls.mark_interface()
7✔
64
        valid = self == mark
7✔
65
        if not valid and raise_on_error:
7✔
66
            raise MarkMismatchError(
7✔
67
                "Mismatch between serialized mark and current interface, "
68
                f"Serialized: {self}; current: {cls}"
69
            )
70
        return valid
7✔
71

72
    def match_by_name(self) -> type["Interface"] | None:
7✔
73
        """
74
        Try to find a matching interface by its name, returning it if found,
75
        or None if not found.
76
        """
77
        for i in Interface.interfaces(sort=False):
7✔
78
            if i.name == self.name:
7✔
79
                return i
7✔
80
        return None
7✔
81

82

83
class JsonDict(BaseModel):
7✔
84
    """
85
    Representation of array when dumped with round_trip == True.
86

87
    .. admonition:: Developer's Note
88

89
        Any JsonDict that contains an actual array should be named ``value``
90
        rather than array (or any other name), and nothing but the
91
        array data should be named ``value`` .
92

93
        During JSON serialization, it becomes ambiguous what contains an array
94
        of data vs. an array of metadata. For the moment we would like to
95
        reserve the ability to have lists of metadata, so until we rule that out,
96
        we would like to be able to avoid iterating over every element of an array
97
        in any context parameter transformation like relativizing/absolutizing paths.
98
        To avoid that, it's good to agree on a single value name -- ``value`` --
99
        and avoid using it for anything else.
100

101
    """
102

103
    type: str
7✔
104

105
    @abstractmethod
7✔
106
    def to_array_input(self) -> V:
7✔
107
        """
108
        Convert this roundtrip specifier to the relevant input class
109
        (one of the ``input_types`` of an interface).
110
        """
111

112
    @classmethod
7✔
113
    def is_valid(cls, val: dict, raise_on_error: bool = False) -> bool:
7✔
114
        """
115
        Check whether a given dictionary matches this JsonDict specification
116

117
        Args:
118
            val (dict): The dictionary to check for validity
119
            raise_on_error (bool): If ``True``, raise the validation error
120
                rather than returning a bool. (default: ``False``)
121

122
        Returns:
123
            bool - true if valid, false if not
124
        """
125
        try:
7✔
126
            _ = cls.model_validate(val)
7✔
127
            return True
7✔
128
        except ValidationError as e:
7✔
129
            if raise_on_error:
7✔
130
                raise e
7✔
131
            return False
7✔
132

133
    @classmethod
7✔
134
    def handle_input(cls: type[U], value: dict | U | W) -> V | W:
7✔
135
        """
136
        Handle input that is the json serialized roundtrip version
137
        (from :func:`~pydantic.BaseModel.model_dump` with ``round_trip=True``)
138
        converting it to the input format with :meth:`.JsonDict.to_array_input`
139
        or passing it through if not applicable
140
        """
141
        if isinstance(value, dict):
7✔
142
            value = cls(**value).to_array_input()
7✔
143
        elif isinstance(value, cls):
7✔
144
            value = value.to_array_input()
7✔
145
        return value
7✔
146

147
    @staticmethod
7✔
148
    def reshape_input(value: T, shape: tuple[int, ...]) -> T:
7✔
149
        """
150
        If a `reshape` value is present on the array, and the array shape doesn't match,
151
        attempt to reshape it.
152
        """
153
        if value.shape != shape:
7✔
154
            try:
7✔
155
                value = value.reshape(shape)
7✔
156
            except ValueError:
×
157
                warnings.warn(
×
158
                    f"Input data has shape {value.shape}, "
159
                    f"but roundtrip form specifies {shape},"
160
                    f"and {value.shape} can't be cast to {shape}. "
161
                    f"Attempting to proceed with validation without reshaping.",
162
                    stacklevel=1,
163
                )
164
        return value
7✔
165

166
    @staticmethod
7✔
167
    def resolve_python_identifier(ref: str) -> Any:
7✔
168
        """
169
        Given some fully-qualified package.subpackage.Class identifier,
170
        return the referenced object, importing if needed.
171
        """
172
        if "." not in ref:
7✔
NEW
173
            return getattr(builtins, ref)
×
174
        else:
175
            module_name, obj = ref.rsplit(".", 1)
7✔
176
            module = sys.modules.get(module_name, importlib.import_module(module_name))
7✔
177

178
            return getattr(module, obj)
7✔
179

180
    def cast_objects(self, array: T, object_cls_name: str) -> T:
7✔
181
        """
182
        Recast objects in object arrays to the type they were before serialization
183
        """
184
        if object_cls_name == "datetime.datetime":
7✔
185
            # special case: must use constructor method
186
            array = np.vectorize(lambda x: datetime.fromisoformat(x))(array)
5✔
187
        else:
188
            object_cls = self.resolve_python_identifier(object_cls_name)
7✔
189
            if isinstance(object_cls, type) and issubclass(object_cls, BaseModel):
7✔
190
                # mild code duplication but we want both -
191
                # convert back to proper object type when deserializing from JSON,
192
                # and also coerce dicts to objects when given on object instantiation
193
                array = np.vectorize(lambda x: object_cls(**x))(array)
7✔
194
            else:
NEW
195
                array = np.vectorize(lambda x: object_cls(x))(array)
×
196
        return array
7✔
197

198

199
class MarkedJson(BaseModel):
7✔
200
    """
201
    Model of JSON dumped with an additional interface mark
202
    with ``model_dump_json({'mark_interface': True})``
203
    """
204

205
    interface: InterfaceMark
7✔
206
    value: list | dict
7✔
207
    """
7✔
208
    Inner value of the array, we don't validate for JsonDict here, 
209
    that should be downstream from us for performance reasons 
210
    """
211

212
    @classmethod
7✔
213
    def try_cast(cls, value: V | dict) -> Union[V, "MarkedJson"]:
7✔
214
        """
215
        Try to cast to MarkedJson if applicable, otherwise return input
216
        """
217
        if isinstance(value, dict) and "interface" in value and "value" in value:
7✔
218
            try:
7✔
219
                value = MarkedJson(**value)
7✔
220
            except ValidationError:
7✔
221
                # fine, just not a MarkedJson dict even if it looks like one
222
                return value
7✔
223
        return value
7✔
224

225

226
class Interface(ABC, Generic[T]):
7✔
227
    """
228
    Abstract parent class for interfaces to different array formats
229
    """
230

231
    input_types: tuple[Any, ...]
5✔
232
    return_type: type[T]
5✔
233
    priority: int = 0
7✔
234
    typing: ClassVar[type["InterfaceTyping"] | None] = None
7✔
235
    """
7✔
236
    Optional static-typing companion class used by the mypy plugin and
237
    the mypy test generator. ``None`` means this interface does not opt
238
    into static constructor inference.
239
    """
240

241
    def __init__(self, shape: ShapeType = Any, dtype: DtypeType = Any) -> None:
7✔
242
        self.shape = shape
7✔
243
        self.dtype = dtype
7✔
244

245
    def validate(self, array: Any) -> T:
7✔
246
        """
247
        Validate input, returning final array type
248

249
        Calls the methods, in order:
250

251
        * array = :meth:`.deserialize` (array)
252
        * array = :meth:`.before_validation` (array)
253
        * dtype = :meth:`.get_dtype` (array) - get the dtype from the array,
254
            override if eg. the dtype is not contained in ``array.dtype``
255
        * valid = :meth:`.validate_dtype` (dtype) - check that the dtype matches
256
            the one in the NDArray specification. Override if special
257
            validation logic is needed for a given format
258
        * :meth:`.raise_for_dtype` (valid, dtype) - after checking dtype validity,
259
            raise an exception if it was invalid. Override to implement custom
260
            exceptions or error conditions, or make validation errors conditional.
261
        * array = :meth:`.after_validate_dtype` (array) - hook for additional
262
            validation or array modification mid-validation
263
        * shape = :meth:`.get_shape` (array) - get the shape from the array,
264
            override if eg. the shape is not contained in ``array.shape``
265
        * valid = :meth:`.validate_shape` (shape) - check that the shape matches
266
            the one in the NDArray specification. Override if special validation
267
            logic is needed.
268
        * :meth:`.raise_for_shape` (valid, shape) - after checking shape validity,
269
            raise an exception if it was invalid. You know the deal bc it's the same
270
            as raise for dtype.
271
        * :meth:`.after_validation` - hook after validation for modifying the array
272
            that is set as the model field value
273

274
        Follow the method signatures and return types to override.
275

276
        Implementing an interface subclass largely consists of overriding these methods
277
        as needed.
278

279
        If validation fails, rather than eg. returning ``False``, exceptions will
280
        be raised (to halt the rest of the pydantic validation process).
281
        When using interfaces outside of pydantic, you must catch both
282
        :class:`.DtypeError` and :class:`.ShapeError` (both of which are children
283
        of :class:`.InterfaceError` )
284

285
        Raises:
286
            :class:`.DtypeError`: Dtype of data doesn't match specification
287
            :class:`.ShapeError`: Shape of data doesn't match specification
288

289
        """
290
        array = self.deserialize(array)
7✔
291

292
        array = self.before_validation(array)
7✔
293

294
        dtype = self.get_dtype(array)
7✔
295
        dtype_valid = self.validate_dtype(dtype)
7✔
296
        self.raise_for_dtype(dtype_valid, dtype)
7✔
297
        array = self.after_validate_dtype(array)
7✔
298

299
        shape = self.get_shape(array)
7✔
300
        shape_valid = self.validate_shape(shape)
7✔
301
        self.raise_for_shape(shape_valid, shape)
7✔
302

303
        array = self.after_validation(array)
7✔
304

305
        return array
7✔
306

307
    def deserialize(self, array: Any) -> V | Any:
7✔
308
        """
309
        If given a JSON serialized version of the array,
310
        deserialize it first.
311

312
        If a roundtrip-serialized :class:`.JsonDict`,
313
        pass to :meth:`.JsonDict.handle_input`.
314

315
        If a roundtrip-serialized :class:`.MarkedJson`,
316
        unpack mark, check for validity, warn if not,
317
        and try to continue with validation
318
        """
319
        if isinstance(marked_array := MarkedJson.try_cast(array), MarkedJson):
7✔
320
            try:
7✔
321
                marked_array.interface.is_valid(self.__class__, raise_on_error=True)
7✔
322
            except MarkMismatchError as e:
7✔
323
                warnings.warn(
7✔
324
                    str(e) + "\nAttempting to continue validation...", stacklevel=2
325
                )
326
            array = marked_array.value
7✔
327

328
        return self.json_model.handle_input(array)
7✔
329

330
    def before_validation(self, array: Any) -> NDArrayType:
7✔
331
        """
332
        Optional step pre-validation that coerces the input into a type that can be
333
        validated for shape and dtype
334

335
        Default method is a no-op
336
        """
337
        return array
×
338

339
    def get_dtype(self, array: NDArrayType) -> DtypeType:
7✔
340
        """
341
        Get the dtype from the input array.
342
        """
343
        if hasattr(array.dtype, "type") and array.dtype.type is np.object_:
7✔
344
            return self.get_object_dtype(array)
7✔
345
        else:
346
            return array.dtype
7✔
347

348
    def get_object_dtype(self, array: NDArrayType) -> DtypeType:
7✔
349
        """
350
        When an array contains an object, get the dtype of the object contained
351
        by the array.
352

353
        If this method returns `Any`, the dtype validation passes -
354
        used for e.g. empty arrays for which the dtype of the array can't be determined
355
        (since there are no objects).
356
        """
357
        try:
7✔
358
            return type(array.ravel()[0])
7✔
359
        except IndexError:
7✔
360
            return Any
7✔
361

362
    def validate_dtype(self, dtype: DtypeType) -> bool:
7✔
363
        """
364
        Validate the dtype of the given array, returning
365
        ``True`` if valid, ``False`` if not.
366
        """
367
        return validate_dtype(dtype, self.dtype)
7✔
368

369
    def raise_for_dtype(self, valid: bool, dtype: DtypeType) -> None:
7✔
370
        """
371
        After validating, raise an exception if invalid
372
        Raises:
373
            :class:`~numpydantic.exceptions.DtypeError`
374
        """
375
        if not valid:
7✔
376
            raise DtypeError(f"Invalid dtype! expected {self.dtype}, got {dtype}")
7✔
377

378
    def after_validate_dtype(self, array: NDArrayType) -> NDArrayType:
7✔
379
        """
380
        Hook to modify array after validating dtype.
381
        Default is a no-op.
382
        """
383
        return array
7✔
384

385
    def get_shape(self, array: NDArrayType) -> tuple[int, ...]:
7✔
386
        """
387
        Get the shape from the array as a tuple of integers
388
        """
389
        return array.shape
7✔
390

391
    def validate_shape(self, shape: tuple[int, ...]) -> bool:
7✔
392
        """
393
        Validate the shape of the given array against the shape
394
        specifier, returning ``True`` if valid, ``False`` if not.
395

396

397
        """
398
        if self.shape is Any:
7✔
399
            return True
7✔
400

401
        return validate_shape(shape, self.shape)
7✔
402

403
    def raise_for_shape(self, valid: bool, shape: tuple[int, ...]) -> None:
7✔
404
        """
405
        Raise a ShapeError if the shape is invalid.
406

407
        Raises:
408
            :class:`~numpydantic.exceptions.ShapeError`
409
        """
410
        if not valid:
7✔
411
            raise ShapeError(
7✔
412
                f"Invalid shape! expected shape {self.shape.prepared_args}, "
413
                f"got shape {shape}"
414
            )
415

416
    def after_validation(self, array: NDArrayType) -> T:
7✔
417
        """
418
        Optional step post-validation that coerces the intermediate array type into the
419
        return type
420

421
        Default method is a no-op
422
        """
423
        return array
7✔
424

425
    @classmethod
7✔
426
    @abstractmethod
7✔
427
    def check(cls, array: Any) -> bool:
7✔
428
        """
429
        Method to check whether a given input applies to this interface
430
        """
431

432
    @classmethod
7✔
433
    @abstractmethod
7✔
434
    def enabled(cls) -> bool:
7✔
435
        """
436
        Check whether this array interface can be used (eg. its dependent packages are
437
        installed, etc.)
438
        """
439

440
    @property
7✔
441
    @abstractmethod
7✔
442
    def name(self) -> str:
7✔
443
        """
444
        Short name for this interface
445
        """
446

447
    @property
7✔
448
    @abstractmethod
7✔
449
    def json_model(self) -> JsonDict:
7✔
450
        """
451
        The :class:`.JsonDict` model used for roundtripping
452
        JSON serialization
453
        """
454

455
    @classmethod
7✔
456
    @abstractmethod
7✔
457
    def to_json(cls, array: type[T], info: SerializationInfo) -> list | JsonDict:
7✔
458
        """
459
        Convert an array of :attr:`.Interface.return_type` to a JSON-compatible format
460
        using base python types
461
        """
462

463
    @classmethod
7✔
464
    def mark_json(cls, array: list | dict) -> dict:
7✔
465
        """
466
        When using ``model_dump_json`` with ``mark_interface: True`` in the ``context``,
467
        add additional annotations that would allow the serialized array to be
468
        roundtripped.
469

470
        Default is just to add an :class:`.InterfaceMark`
471

472
        Examples:
473

474
            >>> from pprint import pprint
475
            >>> pprint(Interface.mark_json([1.0, 2.0]))
476
            {'interface': {'cls': 'Interface',
477
                           'module': 'numpydantic.interface.interface',
478
                           'version': '1.2.2'},
479
             'value': [1.0, 2.0]}
480
        """
481
        return {"interface": cls.mark_interface(), "value": array}
7✔
482

483
    @classmethod
7✔
484
    def interfaces(
7✔
485
        cls, with_disabled: bool = False, sort: bool = True
486
    ) -> tuple[type["Interface"], ...]:
487
        """
488
        Enabled interface subclasses
489

490
        Args:
491
            with_disabled (bool): If ``True`` , get every known interface.
492
                If ``False`` (default), get only enabled interfaces.
493
            sort (bool): If ``True`` (default), sort interfaces by priority.
494
                If ``False`` , sorted by definition order. Used for recursion:
495
                we only want to sort once at the top level.
496
        """
497
        # get recursively
498
        subclasses = []
7✔
499
        for i in cls.__subclasses__():
7✔
500
            if with_disabled:
7✔
501
                subclasses.append(i)
7✔
502

503
            if i.enabled():
7✔
504
                subclasses.append(i)
7✔
505

506
            subclasses.extend(i.interfaces(with_disabled=with_disabled, sort=False))
7✔
507

508
        if sort:
7✔
509
            subclasses = sorted(
7✔
510
                subclasses,
511
                key=attrgetter("priority"),
512
                reverse=True,
513
            )
514

515
        return tuple(subclasses)
7✔
516

517
    @classmethod
7✔
518
    def return_types(cls) -> tuple[NDArrayType, ...]:
7✔
519
        """Return types for all enabled interfaces"""
520
        return tuple([i.return_type for i in cls.interfaces()])
7✔
521

522
    @classmethod
7✔
523
    def input_types(cls) -> tuple[Any, ...]:
7✔
524
        """Input types for all enabled interfaces"""
525
        in_types = []
7✔
526
        for iface in cls.interfaces():
7✔
527
            if isinstance(iface.input_types, (tuple, list)):
7✔
528
                in_types.extend(iface.input_types)
7✔
529
            else:  # pragma: no cover
530
                in_types.append(iface.input_types)
531

532
        return tuple(in_types)
7✔
533

534
    @classmethod
7✔
535
    def match_mark(cls, array: Any) -> type["Interface"] | None:
7✔
536
        """
537
        Match a marked JSON dump of this array to the interface that it indicates.
538

539
        First find an interface that matches by name, and then run its
540
        ``check`` method, because arrays can be dumped with a mark
541
        but without ``round_trip == True`` (and thus can't necessarily
542
        use the same interface that they were dumped with)
543

544
        Returns:
545
            Interface if match found, None otherwise
546
        """
547
        mark = MarkedJson.try_cast(array)
7✔
548
        if not isinstance(mark, MarkedJson):
7✔
549
            return None
7✔
550

551
        interface = mark.interface.match_by_name()
7✔
552
        if interface is not None and interface.check(mark.value):
7✔
553
            return interface
7✔
554
        return None
×
555

556
    @classmethod
7✔
557
    def match(cls, array: Any, fast: bool = False) -> type["Interface"]:
7✔
558
        """
559
        Find the interface that should be used for this array based on its input type
560

561
        First runs the ``check`` method for all interfaces returned by
562
        :meth:`.Interface.interfaces` **except** for :class:`.NumpyInterface` ,
563
        and if no match is found then try the numpy interface. This is because
564
        :meth:`.NumpyInterface.check` can be expensive, as we could potentially
565
        try to
566

567
        Args:
568
            fast (bool): if ``False`` , check all interfaces and raise exceptions for
569
              having multiple matching interfaces (default). If ``True`` ,
570
              check each interface (as ordered by its ``priority`` , decreasing),
571
              and return on the first match.
572
        """
573
        # Shortcircuit match if this is a marked json dump
574
        array = MarkedJson.try_cast(array)
7✔
575
        if (match := cls.match_mark(array)) is not None:
7✔
576
            return match
7✔
577
        elif isinstance(array, MarkedJson):
7✔
578
            array = array.value
×
579

580
        # first try and find a non-numpy interface, since the numpy interface
581
        # will try and load the array into memory in its check method
582
        interfaces = cls.interfaces()
7✔
583
        non_np_interfaces = [i for i in interfaces if i.name != "numpy"]
7✔
584
        np_interface = [i for i in interfaces if i.name == "numpy"][0]
7✔
585

586
        if fast:
7✔
587
            matches = []
7✔
588
            for i in non_np_interfaces:
7✔
589
                if i.check(array):
7✔
590
                    return i
7✔
591
        else:
592
            matches = [i for i in non_np_interfaces if i.check(array)]
7✔
593

594
        if len(matches) > 1:
7✔
595
            msg = f"More than one interface matches input {array}:\n"
7✔
596
            msg += "\n".join([f"  - {i}" for i in matches])
7✔
597
            raise TooManyMatchesError(msg)
7✔
598
        elif len(matches) == 0:
7✔
599
            # now try the numpy interface
600
            if np_interface.check(array):
7✔
601
                return np_interface
7✔
602
            else:
603
                raise NoMatchError(f"No matching interfaces found for input {array}")
7✔
604
        else:
605
            return matches[0]
7✔
606

607
    @classmethod
7✔
608
    def match_output(cls, array: Any) -> type["Interface"]:
7✔
609
        """
610
        Find the interface that should be used based on the output type -
611
        in the case that the output type differs from the input type, eg.
612
        the HDF5 interface, match an instantiated array for purposes of
613
        serialization to json, etc.
614
        """
615
        matches = [i for i in cls.interfaces() if isinstance(array, i.return_type)]
7✔
616
        if len(matches) > 1:
7✔
617
            msg = f"More than one interface matches output {array}:\n"
7✔
618
            msg += "\n".join([f"  - {i}" for i in matches])
7✔
619
            raise TooManyMatchesError(msg)
7✔
620
        elif len(matches) == 0:
7✔
621
            raise NoMatchError(f"No matching interfaces found for output {array}")
7✔
622
        else:
623
            return matches[0]
7✔
624

625
    @classmethod
7✔
626
    @lru_cache(maxsize=32)
7✔
627
    def mark_interface(cls) -> InterfaceMark:
7✔
628
        """
629
        Create an interface mark indicating this interface for validation after
630
        JSON serialization with ``round_trip==True``
631
        """
632
        interface_module = inspect.getmodule(cls)
7✔
633
        interface_module = (
7✔
634
            None if interface_module is None else interface_module.__name__
635
        )
636
        try:
7✔
637
            v = (
7✔
638
                None
639
                if interface_module is None
640
                else version(interface_module.split(".")[0])
641
            )
642
        except (
643
            PackageNotFoundError
644
        ):  # pragma: no cover - no tests for missing interface deps
645
            v = None
646

647
        return InterfaceMark(
7✔
648
            module=interface_module, cls=cls.__name__, name=cls.name, version=v
649
        )
650

651

652
class Proxy(ABC):
7✔
653
    """
654
    A proxy class that exposes some non-array data source (like a video) as an array
655
    """
656

657
    @classmethod
7✔
658
    @abstractmethod
7✔
659
    def proxy_for(cls) -> type[Interface]:
7✔
660
        """
661
        Declare the interface that this is a proxy for,
662
        allowing the proxy to be used with the NDArraySchema annotation
663
        with any of the input types that the Interface supports.
664
        """
665
        raise NotImplementedError()
×
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