Aluode/PerceptionLabPortable
0
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 