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