CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_param_validation.py911 linesDownload Raw Back to utils
1# Authors: The scikit-learn developers
2# SPDX-License-Identifier: BSD-3-Clause
3
4import functools
5import math
6import operator
7import re
8from abc import ABC, abstractmethod
9from collections.abc import Iterable
10from inspect import signature
11from numbers import Integral, Real
12
13import numpy as np
14from scipy.sparse import csr_matrix, issparse
15
16from .._config import config_context, get_config
17from .validation import _is_arraylike_not_scalar
18
19
20class InvalidParameterError(ValueError, TypeError):
21    """Custom exception to be raised when the parameter of a class/method/function
22    does not have a valid type or value.
23    """
24
25    # Inherits from ValueError and TypeError to keep backward compatibility.
26
27
28def validate_parameter_constraints(parameter_constraints, params, caller_name):
29    """Validate types and values of given parameters.
30
31    Parameters
32    ----------
33    parameter_constraints : dict or {"no_validation"}
34        If "no_validation", validation is skipped for this parameter.
35
36        If a dict, it must be a dictionary `param_name: list of constraints`.
37        A parameter is valid if it satisfies one of the constraints from the list.
38        Constraints can be:
39        - an Interval object, representing a continuous or discrete range of numbers
40        - the string "array-like"
41        - the string "sparse matrix"
42        - the string "random_state"
43        - callable
44        - None, meaning that None is a valid value for the parameter
45        - any type, meaning that any instance of this type is valid
46        - an Options object, representing a set of elements of a given type
47        - a StrOptions object, representing a set of strings
48        - the string "boolean"
49        - the string "verbose"
50        - the string "cv_object"
51        - the string "nan"
52        - a MissingValues object representing markers for missing values
53        - a HasMethods object, representing method(s) an object must have
54        - a Hidden object, representing a constraint not meant to be exposed to the user
55
56    params : dict
57        A dictionary `param_name: param_value`. The parameters to validate against the
58        constraints.
59
60    caller_name : str
61        The name of the estimator or function or method that called this function.
62    """
63    for param_name, param_val in params.items():
64        # We allow parameters to not have a constraint so that third party estimators
65        # can inherit from sklearn estimators without having to necessarily use the
66        # validation tools.
67        if param_name not in parameter_constraints:
68            continue
69
70        constraints = parameter_constraints[param_name]
71
72        if constraints == "no_validation":
73            continue
74
75        constraints = [make_constraint(constraint) for constraint in constraints]
76
77        for constraint in constraints:
78            if constraint.is_satisfied_by(param_val):
79                # this constraint is satisfied, no need to check further.
80                break
81        else:
82            # No constraint is satisfied, raise with an informative message.
83
84            # Ignore constraints that we don't want to expose in the error message,
85            # i.e. options that are for internal purpose or not officially supported.
86            constraints = [
87                constraint for constraint in constraints if not constraint.hidden
88            ]
89
90            if len(constraints) == 1:
91                constraints_str = f"{constraints[0]}"
92            else:
93                constraints_str = (
94                    f"{', '.join([str(c) for c in constraints[:-1]])} or"
95                    f" {constraints[-1]}"
96                )
97
98            raise InvalidParameterError(
99                f"The {param_name!r} parameter of {caller_name} must be"
100                f" {constraints_str}. Got {param_val!r} instead."
101            )
102
103
104def make_constraint(constraint):
105    """Convert the constraint into the appropriate Constraint object.
106
107    Parameters
108    ----------
109    constraint : object
110        The constraint to convert.
111
112    Returns
113    -------
114    constraint : instance of _Constraint
115        The converted constraint.
116    """
117    if isinstance(constraint, str) and constraint == "array-like":
118        return _ArrayLikes()
119    if isinstance(constraint, str) and constraint == "sparse matrix":
120        return _SparseMatrices()
121    if isinstance(constraint, str) and constraint == "random_state":
122        return _RandomStates()
123    if constraint is callable:
124        return _Callables()
125    if constraint is None:
126        return _NoneConstraint()
127    if isinstance(constraint, type):
128        return _InstancesOf(constraint)
129    if isinstance(
130        constraint, (Interval, StrOptions, Options, HasMethods, MissingValues)
131    ):
132        return constraint
133    if isinstance(constraint, str) and constraint == "boolean":
134        return _Booleans()
135    if isinstance(constraint, str) and constraint == "verbose":
136        return _VerboseHelper()
137    if isinstance(constraint, str) and constraint == "cv_object":
138        return _CVObjects()
139    if isinstance(constraint, Hidden):
140        constraint = make_constraint(constraint.constraint)
141        constraint.hidden = True
142        return constraint
143    if (isinstance(constraint, str) and constraint == "nan") or (
144        isinstance(constraint, float) and np.isnan(constraint)
145    ):
146        return _NanConstraint()
147    raise ValueError(f"Unknown constraint type: {constraint}")
148
149
150def validate_params(parameter_constraints, *, prefer_skip_nested_validation):
151    """Decorator to validate types and values of functions and methods.
152
153    Parameters
154    ----------
155    parameter_constraints : dict
156        A dictionary `param_name: list of constraints`. See the docstring of
157        `validate_parameter_constraints` for a description of the accepted constraints.
158
159        Note that the *args and **kwargs parameters are not validated and must not be
160        present in the parameter_constraints dictionary.
161
162    prefer_skip_nested_validation : bool
163        If True, the validation of parameters of inner estimators or functions
164        called by the decorated function will be skipped.
165
166        This is useful to avoid validating many times the parameters passed by the
167        user from the public facing API. It's also useful to avoid validating
168        parameters that we pass internally to inner functions that are guaranteed to
169        be valid by the test suite.
170
171        It should be set to True for most functions, except for those that receive
172        non-validated objects as parameters or that are just wrappers around classes
173        because they only perform a partial validation.
174
175    Returns
176    -------
177    decorated_function : function or method
178        The decorated function.
179    """
180
181    def decorator(func):
182        # The dict of parameter constraints is set as an attribute of the function
183        # to make it possible to dynamically introspect the constraints for
184        # automatic testing.
185        setattr(func, "_skl_parameter_constraints", parameter_constraints)
186
187        @functools.wraps(func)
188        def wrapper(*args, **kwargs):
189            global_skip_validation = get_config()["skip_parameter_validation"]
190            if global_skip_validation:
191                return func(*args, **kwargs)
192
193            func_sig = signature(func)
194
195            # Map *args/**kwargs to the function signature
196            params = func_sig.bind(*args, **kwargs)
197            params.apply_defaults()
198
199            # ignore self/cls and positional/keyword markers
200            to_ignore = [
201                p.name
202                for p in func_sig.parameters.values()
203                if p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD)
204            ]
205            to_ignore += ["self", "cls"]
206            params = {k: v for k, v in params.arguments.items() if k not in to_ignore}
207
208            validate_parameter_constraints(
209                parameter_constraints, params, caller_name=func.__qualname__
210            )
211
212            try:
213                with config_context(
214                    skip_parameter_validation=(
215                        prefer_skip_nested_validation or global_skip_validation
216                    )
217                ):
218                    return func(*args, **kwargs)
219            except InvalidParameterError as e:
220                # When the function is just a wrapper around an estimator, we allow
221                # the function to delegate validation to the estimator, but we replace
222                # the name of the estimator by the name of the function in the error
223                # message to avoid confusion.
224                msg = re.sub(
225                    r"parameter of \w+ must be",
226                    f"parameter of {func.__qualname__} must be",
227                    str(e),
228                )
229                raise InvalidParameterError(msg) from e
230
231        return wrapper
232
233    return decorator
234
235
236class RealNotInt(Real):
237    """A type that represents reals that are not instances of int.
238
239    Behaves like float, but also works with values extracted from numpy arrays.
240    isintance(1, RealNotInt) -> False
241    isinstance(1.0, RealNotInt) -> True
242    """
243
244
245RealNotInt.register(float)
246
247
248def _type_name(t):
249    """Convert type into human readable string."""
250    module = t.__module__
251    qualname = t.__qualname__
252    if module == "builtins":
253        return qualname
254    elif t == Real:
255        return "float"
256    elif t == Integral:
257        return "int"
258    return f"{module}.{qualname}"
259
260
261class _Constraint(ABC):
262    """Base class for the constraint objects."""
263
264    def __init__(self):
265        self.hidden = False
266
267    @abstractmethod
268    def is_satisfied_by(self, val):
269        """Whether or not a value satisfies the constraint.
270
271        Parameters
272        ----------
273        val : object
274            The value to check.
275
276        Returns
277        -------
278        is_satisfied : bool
279            Whether or not the constraint is satisfied by this value.
280        """
281
282    @abstractmethod
283    def __str__(self):
284        """A human readable representational string of the constraint."""
285
286
287class _InstancesOf(_Constraint):
288    """Constraint representing instances of a given type.
289
290    Parameters
291    ----------
292    type : type
293        The valid type.
294    """
295
296    def __init__(self, type):
297        super().__init__()
298        self.type = type
299
300    def is_satisfied_by(self, val):
301        return isinstance(val, self.type)
302
303    def __str__(self):
304        return f"an instance of {_type_name(self.type)!r}"
305
306
307class _NoneConstraint(_Constraint):
308    """Constraint representing the None singleton."""
309
310    def is_satisfied_by(self, val):
311        return val is None
312
313    def __str__(self):
314        return "None"
315
316
317class _NanConstraint(_Constraint):
318    """Constraint representing the indicator `np.nan`."""
319
320    def is_satisfied_by(self, val):
321        return (
322            not isinstance(val, Integral) and isinstance(val, Real) and math.isnan(val)
323        )
324
325    def __str__(self):
326        return "numpy.nan"
327
328
329class _PandasNAConstraint(_Constraint):
330    """Constraint representing the indicator `pd.NA`."""
331
332    def is_satisfied_by(self, val):
333        try:
334            import pandas as pd
335
336            return isinstance(val, type(pd.NA)) and pd.isna(val)
337        except ImportError:
338            return False
339
340    def __str__(self):
341        return "pandas.NA"
342
343
344class Options(_Constraint):
345    """Constraint representing a finite set of instances of a given type.
346
347    Parameters
348    ----------
349    type : type
350
351    options : set
352        The set of valid scalars.
353
354    deprecated : set or None, default=None
355        A subset of the `options` to mark as deprecated in the string
356        representation of the constraint.
357    """
358
359    def __init__(self, type, options, *, deprecated=None):
360        super().__init__()
361        self.type = type
362        self.options = options
363        self.deprecated = deprecated or set()
364
365        if self.deprecated - self.options:
366            raise ValueError("The deprecated options must be a subset of the options.")
367
368    def is_satisfied_by(self, val):
369        return isinstance(val, self.type) and val in self.options
370
371    def _mark_if_deprecated(self, option):
372        """Add a deprecated mark to an option if needed."""
373        option_str = f"{option!r}"
374        if option in self.deprecated:
375            option_str = f"{option_str} (deprecated)"
376        return option_str
377
378    def __str__(self):
379        options_str = (
380            f"{', '.join([self._mark_if_deprecated(o) for o in self.options])}"
381        )
382        return f"a {_type_name(self.type)} among {{{options_str}}}"
383
384
385class StrOptions(Options):
386    """Constraint representing a finite set of strings.
387
388    Parameters
389    ----------
390    options : set of str
391        The set of valid strings.
392
393    deprecated : set of str or None, default=None
394        A subset of the `options` to mark as deprecated in the string
395        representation of the constraint.
396    """
397
398    def __init__(self, options, *, deprecated=None):
399        super().__init__(type=str, options=options, deprecated=deprecated)
400
401
402class Interval(_Constraint):
403    """Constraint representing a typed interval.
404
405    Parameters
406    ----------
407    type : {numbers.Integral, numbers.Real, RealNotInt}
408        The set of numbers in which to set the interval.
409
410        If RealNotInt, only reals that don't have the integer type
411        are allowed. For example 1.0 is allowed but 1 is not.
412
413    left : float or int or None
414        The left bound of the interval. None means left bound is -∞.
415
416    right : float, int or None
417        The right bound of the interval. None means right bound is +∞.
418
419    closed : {"left", "right", "both", "neither"}
420        Whether the interval is open or closed. Possible choices are:
421
422        - `"left"`: the interval is closed on the left and open on the right.
423          It is equivalent to the interval `[ left, right )`.
424        - `"right"`: the interval is closed on the right and open on the left.
425          It is equivalent to the interval `( left, right ]`.
426        - `"both"`: the interval is closed.
427          It is equivalent to the interval `[ left, right ]`.
428        - `"neither"`: the interval is open.
429          It is equivalent to the interval `( left, right )`.
430
431    Notes
432    -----
433    Setting a bound to `None` and setting the interval closed is valid. For instance,
434    strictly speaking, `Interval(Real, 0, None, closed="both")` corresponds to
435    `[0, +∞) U {+∞}`.
436    """
437
438    def __init__(self, type, left, right, *, closed):
439        super().__init__()
440        self.type = type
441        self.left = left
442        self.right = right
443        self.closed = closed
444
445        self._check_params()
446
447    def _check_params(self):
448        if self.type not in (Integral, Real, RealNotInt):
449            raise ValueError(
450                "type must be either numbers.Integral, numbers.Real or RealNotInt."
451                f" Got {self.type} instead."
452            )
453
454        if self.closed not in ("left", "right", "both", "neither"):
455            raise ValueError(
456                "closed must be either 'left', 'right', 'both' or 'neither'. "
457                f"Got {self.closed} instead."
458            )
459
460        if self.type is Integral:
461            suffix = "for an interval over the integers."
462            if self.left is not None and not isinstance(self.left, Integral):
463                raise TypeError(f"Expecting left to be an int {suffix}")
464            if self.right is not None and not isinstance(self.right, Integral):
465                raise TypeError(f"Expecting right to be an int {suffix}")
466            if self.left is None and self.closed in ("left", "both"):
467                raise ValueError(
468                    f"left can't be None when closed == {self.closed} {suffix}"
469                )
470            if self.right is None and self.closed in ("right", "both"):
471                raise ValueError(
472                    f"right can't be None when closed == {self.closed} {suffix}"
473                )
474        else:
475            if self.left is not None and not isinstance(self.left, Real):
476                raise TypeError("Expecting left to be a real number.")
477            if self.right is not None and not isinstance(self.right, Real):
478                raise TypeError("Expecting right to be a real number.")
479
480        if self.right is not None and self.left is not None and self.right <= self.left:
481            raise ValueError(
482                f"right can't be less than left. Got left={self.left} and "
483                f"right={self.right}"
484            )
485
486    def __contains__(self, val):
487        if not isinstance(val, Integral) and np.isnan(val):
488            return False
489
490        left_cmp = operator.lt if self.closed in ("left", "both") else operator.le
491        right_cmp = operator.gt if self.closed in ("right", "both") else operator.ge
492
493        left = -np.inf if self.left is None else self.left
494        right = np.inf if self.right is None else self.right
495
496        if left_cmp(val, left):
497            return False
498        if right_cmp(val, right):
499            return False
500        return True
501
502    def is_satisfied_by(self, val):
503        if not isinstance(val, self.type):
504            return False
505
506        return val in self
507
508    def __str__(self):
509        type_str = "an int" if self.type is Integral else "a float"
510        left_bracket = "[" if self.closed in ("left", "both") else "("
511        left_bound = "-inf" if self.left is None else self.left
512        right_bound = "inf" if self.right is None else self.right
513        right_bracket = "]" if self.closed in ("right", "both") else ")"
514
515        # better repr if the bounds were given as integers
516        if not self.type == Integral and isinstance(self.left, Real):
517            left_bound = float(left_bound)
518        if not self.type == Integral and isinstance(self.right, Real):
519            right_bound = float(right_bound)
520
521        return (
522            f"{type_str} in the range "
523            f"{left_bracket}{left_bound}, {right_bound}{right_bracket}"
524        )
525
526
527class _ArrayLikes(_Constraint):
528    """Constraint representing array-likes"""
529
530    def is_satisfied_by(self, val):
531        return _is_arraylike_not_scalar(val)
532
533    def __str__(self):
534        return "an array-like"
535
536
537class _SparseMatrices(_Constraint):
538    """Constraint representing sparse matrices."""
539
540    def is_satisfied_by(self, val):
541        return issparse(val)
542
543    def __str__(self):
544        return "a sparse matrix"
545
546
547class _Callables(_Constraint):
548    """Constraint representing callables."""
549
550    def is_satisfied_by(self, val):
551        return callable(val)
552
553    def __str__(self):
554        return "a callable"
555
556
557class _RandomStates(_Constraint):
558    """Constraint representing random states.
559
560    Convenience class for
561    [Interval(Integral, 0, 2**32 - 1, closed="both"), np.random.RandomState, None]
562    """
563
564    def __init__(self):
565        super().__init__()
566        self._constraints = [
567            Interval(Integral, 0, 2**32 - 1, closed="both"),
568            _InstancesOf(np.random.RandomState),
569            _NoneConstraint(),
570        ]
571
572    def is_satisfied_by(self, val):
573        return any(c.is_satisfied_by(val) for c in self._constraints)
574
575    def __str__(self):
576        return (
577            f"{', '.join([str(c) for c in self._constraints[:-1]])} or"
578            f" {self._constraints[-1]}"
579        )
580
581
582class _Booleans(_Constraint):
583    """Constraint representing boolean likes.
584
585    Convenience class for
586    [bool, np.bool_]
587    """
588
589    def __init__(self):
590        super().__init__()
591        self._constraints = [
592            _InstancesOf(bool),
593            _InstancesOf(np.bool_),
594        ]
595
596    def is_satisfied_by(self, val):
597        return any(c.is_satisfied_by(val) for c in self._constraints)
598
599    def __str__(self):
600        return (
601            f"{', '.join([str(c) for c in self._constraints[:-1]])} or"
602            f" {self._constraints[-1]}"
603        )
604
605
606class _VerboseHelper(_Constraint):
607    """Helper constraint for the verbose parameter.
608
609    Convenience class for
610    [Interval(Integral, 0, None, closed="left"), bool, numpy.bool_]
611    """
612
613    def __init__(self):
614        super().__init__()
615        self._constraints = [
616            Interval(Integral, 0, None, closed="left"),
617            _InstancesOf(bool),
618            _InstancesOf(np.bool_),
619        ]
620
621    def is_satisfied_by(self, val):
622        return any(c.is_satisfied_by(val) for c in self._constraints)
623
624    def __str__(self):
625        return (
626            f"{', '.join([str(c) for c in self._constraints[:-1]])} or"
627            f" {self._constraints[-1]}"
628        )
629
630
631class MissingValues(_Constraint):
632    """Helper constraint for the `missing_values` parameters.
633
634    Convenience for
635    [
636        Integral,
637        Interval(Real, None, None, closed="both"),
638        str,   # when numeric_only is False
639        None,  # when numeric_only is False
640        _NanConstraint(),
641        _PandasNAConstraint(),
642    ]
643
644    Parameters
645    ----------
646    numeric_only : bool, default=False
647        Whether to consider only numeric missing value markers.
648
649    """
650
651    def __init__(self, numeric_only=False):
652        super().__init__()
653
654        self.numeric_only = numeric_only
655
656        self._constraints = [
657            _InstancesOf(Integral),
658            # we use an interval of Real to ignore np.nan that has its own constraint
659            Interval(Real, None, None, closed="both"),
660            _NanConstraint(),
661            _PandasNAConstraint(),
662        ]
663        if not self.numeric_only:
664            self._constraints.extend([_InstancesOf(str), _NoneConstraint()])
665
666    def is_satisfied_by(self, val):
667        return any(c.is_satisfied_by(val) for c in self._constraints)
668
669    def __str__(self):
670        return (
671            f"{', '.join([str(c) for c in self._constraints[:-1]])} or"
672            f" {self._constraints[-1]}"
673        )
674
675
676class HasMethods(_Constraint):
677    """Constraint representing objects that expose specific methods.
678
679    It is useful for parameters following a protocol and where we don't want to impose
680    an affiliation to a specific module or class.
681
682    Parameters
683    ----------
684    methods : str or list of str
685        The method(s) that the object is expected to expose.
686    """
687
688    @validate_params(
689        {"methods": [str, list]},
690        prefer_skip_nested_validation=True,
691    )
692    def __init__(self, methods):
693        super().__init__()
694        if isinstance(methods, str):
695            methods = [methods]
696        self.methods = methods
697
698    def is_satisfied_by(self, val):
699        return all(callable(getattr(val, method, None)) for method in self.methods)
700
701    def __str__(self):
702        if len(self.methods) == 1:
703            methods = f"{self.methods[0]!r}"
704        else:
705            methods = (
706                f"{', '.join([repr(m) for m in self.methods[:-1]])} and"
707                f" {self.methods[-1]!r}"
708            )
709        return f"an object implementing {methods}"
710
711
712class _IterablesNotString(_Constraint):
713    """Constraint representing iterables that are not strings."""
714
715    def is_satisfied_by(self, val):
716        return isinstance(val, Iterable) and not isinstance(val, str)
717
718    def __str__(self):
719        return "an iterable"
720
721
722class _CVObjects(_Constraint):
723    """Constraint representing cv objects.
724
725    Convenient class for
726    [
727        Interval(Integral, 2, None, closed="left"),
728        HasMethods(["split", "get_n_splits"]),
729        _IterablesNotString(),
730        None,
731    ]
732    """
733
734    def __init__(self):
735        super().__init__()
736        self._constraints = [
737            Interval(Integral, 2, None, closed="left"),
738            HasMethods(["split", "get_n_splits"]),
739            _IterablesNotString(),
740            _NoneConstraint(),
741        ]
742
743    def is_satisfied_by(self, val):
744        return any(c.is_satisfied_by(val) for c in self._constraints)
745
746    def __str__(self):
747        return (
748            f"{', '.join([str(c) for c in self._constraints[:-1]])} or"
749            f" {self._constraints[-1]}"
750        )
751
752
753class Hidden:
754    """Class encapsulating a constraint not meant to be exposed to the user.
755
756    Parameters
757    ----------
758    constraint : str or _Constraint instance
759        The constraint to be used internally.
760    """
761
762    def __init__(self, constraint):
763        self.constraint = constraint
764
765
766def generate_invalid_param_val(constraint):
767    """Return a value that does not satisfy the constraint.
768
769    Raises a NotImplementedError if there exists no invalid value for this constraint.
770
771    This is only useful for testing purpose.
772
773    Parameters
774    ----------
775    constraint : _Constraint instance
776        The constraint to generate a value for.
777
778    Returns
779    -------
780    val : object
781        A value that does not satisfy the constraint.
782    """
783    if isinstance(constraint, StrOptions):
784        return f"not {' or '.join(constraint.options)}"
785
786    if isinstance(constraint, MissingValues):
787        return np.array([1, 2, 3])
788
789    if isinstance(constraint, _VerboseHelper):
790        return -1
791
792    if isinstance(constraint, HasMethods):
793        return type("HasNotMethods", (), {})()
794
795    if isinstance(constraint, _IterablesNotString):
796        return "a string"
797
798    if isinstance(constraint, _CVObjects):
799        return "not a cv object"
800
801    if isinstance(constraint, Interval) and constraint.type is Integral:
802        if constraint.left is not None:
803            return constraint.left - 1
804        if constraint.right is not None:
805            return constraint.right + 1
806
807        # There's no integer outside (-inf, +inf)
808        raise NotImplementedError
809
810    if isinstance(constraint, Interval) and constraint.type in (Real, RealNotInt):
811        if constraint.left is not None:
812            return constraint.left - 1e-6
813        if constraint.right is not None:
814            return constraint.right + 1e-6
815
816        # bounds are -inf, +inf
817        if constraint.closed in ("right", "neither"):
818            return -np.inf
819        if constraint.closed in ("left", "neither"):
820            return np.inf
821
822        # interval is [-inf, +inf]
823        return np.nan
824
825    raise NotImplementedError
826
827
828def generate_valid_param(constraint):
829    """Return a value that does satisfy a constraint.
830
831    This is only useful for testing purpose.
832
833    Parameters
834    ----------
835    constraint : Constraint instance
836        The constraint to generate a value for.
837
838    Returns
839    -------
840    val : object
841        A value that does satisfy the constraint.
842    """
843    if isinstance(constraint, _ArrayLikes):
844        return np.array([1, 2, 3])
845
846    if isinstance(constraint, _SparseMatrices):
847        return csr_matrix([[0, 1], [1, 0]])
848
849    if isinstance(constraint, _RandomStates):
850        return np.random.RandomState(42)
851
852    if isinstance(constraint, _Callables):
853        return lambda x: x
854
855    if isinstance(constraint, _NoneConstraint):
856        return None
857
858    if isinstance(constraint, _InstancesOf):
859        if constraint.type is np.ndarray:
860            # special case for ndarray since it can't be instantiated without arguments
861            return np.array([1, 2, 3])
862
863        if constraint.type in (Integral, Real):
864            # special case for Integral and Real since they are abstract classes
865            return 1
866
867        return constraint.type()
868
869    if isinstance(constraint, _Booleans):
870        return True
871
872    if isinstance(constraint, _VerboseHelper):
873        return 1
874
875    if isinstance(constraint, MissingValues) and constraint.numeric_only:
876        return np.nan
877
878    if isinstance(constraint, MissingValues) and not constraint.numeric_only:
879        return "missing"
880
881    if isinstance(constraint, HasMethods):
882        return type(
883            "ValidHasMethods", (), {m: lambda self: None for m in constraint.methods}
884        )()
885
886    if isinstance(constraint, _IterablesNotString):
887        return [1, 2, 3]
888
889    if isinstance(constraint, _CVObjects):
890        return 5
891
892    if isinstance(constraint, Options):  # includes StrOptions
893        for option in constraint.options:
894            return option
895
896    if isinstance(constraint, Interval):
897        interval = constraint
898        if interval.left is None and interval.right is None:
899            return 0
900        elif interval.left is None:
901            return interval.right - 1
902        elif interval.right is None:
903            return interval.left + 1
904        else:
905            if interval.type is Real:
906                return (interval.left + interval.right) / 2
907            else:
908                return interval.left + 1
909
910    raise ValueError(f"Unknown constraint type: {constraint}")
911 
Aluode/PerceptionLabPortable · CoolFace