codekingpro/portable-devtools
114k
1# SPDX-License-Identifier: MIT2 3"""4Commonly useful validators.5"""6 7import operator8import re9 10from contextlib import contextmanager11from re import Pattern12 13from ._config import get_run_validators, set_run_validators14from ._make import _AndValidator, and_, attrib, attrs15from .converters import default_if_none16from .exceptions import NotCallableError17 18 19__all__ = [20 "and_",21 "deep_iterable",22 "deep_mapping",23 "disabled",24 "ge",25 "get_disabled",26 "gt",27 "in_",28 "instance_of",29 "is_callable",30 "le",31 "lt",32 "matches_re",33 "max_len",34 "min_len",35 "not_",36 "optional",37 "or_",38 "set_disabled",39]40 41 42def set_disabled(disabled):43 """44 Globally disable or enable running validators.45 46 By default, they are run.47 48 Args:49 disabled (bool): If `True`, disable running all validators.50 51 .. warning::52 53 This function is not thread-safe!54 55 .. versionadded:: 21.3.056 """57 set_run_validators(not disabled)58 59 60def get_disabled():61 """62 Return a bool indicating whether validators are currently disabled or not.63 64 Returns:65 bool:`True` if validators are currently disabled.66 67 .. versionadded:: 21.3.068 """69 return not get_run_validators()70 71 72@contextmanager73def disabled():74 """75 Context manager that disables running validators within its context.76 77 .. warning::78 79 This context manager is not thread-safe!80 81 .. versionadded:: 21.3.082 .. versionchanged:: 26.1.0 The contextmanager is nestable.83 """84 prev = get_run_validators()85 set_run_validators(False)86 try:87 yield88 finally:89 set_run_validators(prev)90 91 92@attrs(repr=False, slots=True, unsafe_hash=True)93class _InstanceOfValidator:94 type = attrib()95 96 def __call__(self, inst, attr, value):97 """98 We use a callable class to be able to change the ``__repr__``.99 """100 if not isinstance(value, self.type):101 msg = f"'{attr.name}' must be {self.type!r} (got {value!r} that is a {value.__class__!r})."102 raise TypeError(103 msg,104 attr,105 self.type,106 value,107 )108 109 def __repr__(self):110 return f"<instance_of validator for type {self.type!r}>"111 112 113def instance_of(type):114 """115 A validator that raises a `TypeError` if the initializer is called with a116 wrong type for this particular attribute (checks are performed using117 `isinstance` therefore it's also valid to pass a tuple of types).118 119 Args:120 type (type | tuple[type]): The type to check for.121 122 Raises:123 TypeError:124 With a human readable error message, the attribute (of type125 `attrs.Attribute`), the expected type, and the value it got.126 """127 return _InstanceOfValidator(type)128 129 130@attrs(repr=False, frozen=True, slots=True)131class _MatchesReValidator:132 pattern = attrib()133 match_func = attrib()134 135 def __call__(self, inst, attr, value):136 """137 We use a callable class to be able to change the ``__repr__``.138 """139 if not self.match_func(value):140 msg = f"'{attr.name}' must match regex {self.pattern.pattern!r} ({value!r} doesn't)"141 raise ValueError(142 msg,143 attr,144 self.pattern,145 value,146 )147 148 def __repr__(self):149 return f"<matches_re validator for pattern {self.pattern!r}>"150 151 152def matches_re(regex, flags=0, func=None):153 r"""154 A validator that raises `ValueError` if the initializer is called with a155 string that doesn't match *regex*.156 157 Args:158 regex (str, re.Pattern):159 A regex string or precompiled pattern to match against160 161 flags (int):162 Flags that will be passed to the underlying re function (default 0)163 164 func (typing.Callable):165 Which underlying `re` function to call. Valid options are166 `re.fullmatch`, `re.search`, and `re.match`; the default `None`167 means `re.fullmatch`. For performance reasons, the pattern is168 always precompiled using `re.compile`.169 170 .. versionadded:: 19.2.0171 .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern.172 """173 valid_funcs = (re.fullmatch, None, re.search, re.match)174 if func not in valid_funcs:175 msg = "'func' must be one of {}.".format(176 ", ".join(177 sorted((e and e.__name__) or "None" for e in set(valid_funcs))178 )179 )180 raise ValueError(msg)181 182 if isinstance(regex, Pattern):183 if flags:184 msg = "'flags' can only be used with a string pattern; pass flags to re.compile() instead"185 raise TypeError(msg)186 pattern = regex187 else:188 pattern = re.compile(regex, flags)189 190 if func is re.match:191 match_func = pattern.match192 elif func is re.search:193 match_func = pattern.search194 else:195 match_func = pattern.fullmatch196 197 return _MatchesReValidator(pattern, match_func)198 199 200@attrs(repr=False, slots=True, unsafe_hash=True)201class _OptionalValidator:202 validator = attrib()203 204 def __call__(self, inst, attr, value):205 if value is None:206 return207 208 self.validator(inst, attr, value)209 210 def __repr__(self):211 return f"<optional validator for {self.validator!r} or None>"212 213 214def optional(validator):215 """216 A validator that makes an attribute optional. An optional attribute is one217 which can be set to `None` in addition to satisfying the requirements of218 the sub-validator.219 220 Args:221 validator222 (typing.Callable | tuple[typing.Callable] | list[typing.Callable]):223 A validator (or validators) that is used for non-`None` values.224 225 .. versionadded:: 15.1.0226 .. versionchanged:: 17.1.0 *validator* can be a list of validators.227 .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators.228 """229 if isinstance(validator, (list, tuple)):230 return _OptionalValidator(_AndValidator(validator))231 232 return _OptionalValidator(validator)233 234 235@attrs(repr=False, slots=True, unsafe_hash=True)236class _InValidator:237 options = attrib()238 _original_options = attrib(hash=False)239 240 def __call__(self, inst, attr, value):241 try:242 in_options = value in self.options243 except TypeError: # e.g. `1 in "abc"`244 in_options = False245 246 if not in_options:247 msg = f"'{attr.name}' must be in {self._original_options!r} (got {value!r})"248 raise ValueError(249 msg,250 attr,251 self._original_options,252 value,253 )254 255 def __repr__(self):256 return f"<in_ validator with options {self._original_options!r}>"257 258 259def in_(options):260 """261 A validator that raises a `ValueError` if the initializer is called with a262 value that does not belong in the *options* provided.263 264 The check is performed using ``value in options``, so *options* has to265 support that operation.266 267 To keep the validator hashable, dicts, lists, and sets are transparently268 transformed into a `tuple`.269 270 Args:271 options: Allowed options.272 273 Raises:274 ValueError:275 With a human readable error message, the attribute (of type276 `attrs.Attribute`), the expected options, and the value it got.277 278 .. versionadded:: 17.1.0279 .. versionchanged:: 22.1.0280 The ValueError was incomplete until now and only contained the human281 readable error message. Now it contains all the information that has282 been promised since 17.1.0.283 .. versionchanged:: 24.1.0284 *options* that are a list, dict, or a set are now transformed into a285 tuple to keep the validator hashable.286 """287 repr_options = options288 if isinstance(options, (list, dict, set)):289 options = tuple(options)290 291 return _InValidator(options, repr_options)292 293 294@attrs(repr=False, slots=False, unsafe_hash=True)295class _IsCallableValidator:296 def __call__(self, inst, attr, value):297 """298 We use a callable class to be able to change the ``__repr__``.299 """300 if not callable(value):301 message = (302 "'{name}' must be callable "303 "(got {value!r} that is a {actual!r})."304 )305 raise NotCallableError(306 msg=message.format(307 name=attr.name, value=value, actual=value.__class__308 ),309 value=value,310 )311 312 def __repr__(self):313 return "<is_callable validator>"314 315 316def is_callable():317 """318 A validator that raises a `attrs.exceptions.NotCallableError` if the319 initializer is called with a value for this particular attribute that is320 not callable.321 322 .. versionadded:: 19.1.0323 324 Raises:325 attrs.exceptions.NotCallableError:326 With a human readable error message containing the attribute327 (`attrs.Attribute`) name, and the value it got.328 """329 return _IsCallableValidator()330 331 332@attrs(repr=False, slots=True, unsafe_hash=True)333class _DeepIterable:334 member_validator = attrib(validator=is_callable())335 iterable_validator = attrib(336 default=None, validator=optional(is_callable())337 )338 339 def __call__(self, inst, attr, value):340 """341 We use a callable class to be able to change the ``__repr__``.342 """343 if self.iterable_validator is not None:344 self.iterable_validator(inst, attr, value)345 346 for member in value:347 self.member_validator(inst, attr, member)348 349 def __repr__(self):350 iterable_identifier = (351 ""352 if self.iterable_validator is None353 else f" {self.iterable_validator!r}"354 )355 return (356 f"<deep_iterable validator for{iterable_identifier}"357 f" iterables of {self.member_validator!r}>"358 )359 360 361def deep_iterable(member_validator, iterable_validator=None):362 """363 A validator that performs deep validation of an iterable.364 365 Args:366 member_validator: Validator(s) to apply to iterable members.367 368 iterable_validator:369 Validator(s) to apply to iterable itself (optional).370 371 Raises372 TypeError: if any sub-validators fail373 374 .. versionadded:: 19.1.0375 376 .. versionchanged:: 25.4.0377 *member_validator* and *iterable_validator* can now be a list or tuple378 of validators.379 """380 if isinstance(member_validator, (list, tuple)):381 member_validator = and_(*member_validator)382 if isinstance(iterable_validator, (list, tuple)):383 iterable_validator = and_(*iterable_validator)384 return _DeepIterable(member_validator, iterable_validator)385 386 387@attrs(repr=False, slots=True, unsafe_hash=True)388class _DeepMapping:389 key_validator = attrib(validator=optional(is_callable()))390 value_validator = attrib(validator=optional(is_callable()))391 mapping_validator = attrib(validator=optional(is_callable()))392 393 def __call__(self, inst, attr, value):394 """395 We use a callable class to be able to change the ``__repr__``.396 """397 if self.mapping_validator is not None:398 self.mapping_validator(inst, attr, value)399 400 for key in value:401 if self.key_validator is not None:402 self.key_validator(inst, attr, key)403 if self.value_validator is not None:404 self.value_validator(inst, attr, value[key])405 406 def __repr__(self):407 return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>"408 409 410def deep_mapping(411 key_validator=None, value_validator=None, mapping_validator=None412):413 """414 A validator that performs deep validation of a dictionary.415 416 All validators are optional, but at least one of *key_validator* or417 *value_validator* must be provided.418 419 Args:420 key_validator: Validator(s) to apply to dictionary keys.421 422 value_validator: Validator(s) to apply to dictionary values.423 424 mapping_validator:425 Validator(s) to apply to top-level mapping attribute.426 427 .. versionadded:: 19.1.0428 429 .. versionchanged:: 25.4.0430 *key_validator* and *value_validator* are now optional, but at least one431 of them must be provided.432 433 .. versionchanged:: 25.4.0434 *key_validator*, *value_validator*, and *mapping_validator* can now be a435 list or tuple of validators.436 437 Raises:438 TypeError: If any sub-validator fails on validation.439 440 ValueError:441 If neither *key_validator* nor *value_validator* is provided on442 instantiation.443 """444 if key_validator is None and value_validator is None:445 msg = (446 "At least one of key_validator or value_validator must be provided"447 )448 raise ValueError(msg)449 450 if isinstance(key_validator, (list, tuple)):451 key_validator = and_(*key_validator)452 if isinstance(value_validator, (list, tuple)):453 value_validator = and_(*value_validator)454 if isinstance(mapping_validator, (list, tuple)):455 mapping_validator = and_(*mapping_validator)456 457 return _DeepMapping(key_validator, value_validator, mapping_validator)458 459 460@attrs(repr=False, frozen=True, slots=True)461class _NumberValidator:462 bound = attrib()463 compare_op = attrib()464 compare_func = attrib()465 466 def __call__(self, inst, attr, value):467 """468 We use a callable class to be able to change the ``__repr__``.469 """470 if not self.compare_func(value, self.bound):471 msg = f"'{attr.name}' must be {self.compare_op} {self.bound}: {value}"472 raise ValueError(msg)473 474 def __repr__(self):475 return f"<Validator for x {self.compare_op} {self.bound}>"476 477 478def lt(val):479 """480 A validator that raises `ValueError` if the initializer is called with a481 number larger or equal to *val*.482 483 The validator uses `operator.lt` to compare the values.484 485 Args:486 val: Exclusive upper bound for values.487 488 .. versionadded:: 21.3.0489 """490 return _NumberValidator(val, "<", operator.lt)491 492 493def le(val):494 """495 A validator that raises `ValueError` if the initializer is called with a496 number greater than *val*.497 498 The validator uses `operator.le` to compare the values.499 500 Args:501 val: Inclusive upper bound for values.502 503 .. versionadded:: 21.3.0504 """505 return _NumberValidator(val, "<=", operator.le)506 507 508def ge(val):509 """510 A validator that raises `ValueError` if the initializer is called with a511 number smaller than *val*.512 513 The validator uses `operator.ge` to compare the values.514 515 Args:516 val: Inclusive lower bound for values517 518 .. versionadded:: 21.3.0519 """520 return _NumberValidator(val, ">=", operator.ge)521 522 523def gt(val):524 """525 A validator that raises `ValueError` if the initializer is called with a526 number smaller or equal to *val*.527 528 The validator uses `operator.gt` to compare the values.529 530 Args:531 val: Exclusive lower bound for values532 533 .. versionadded:: 21.3.0534 """535 return _NumberValidator(val, ">", operator.gt)536 537 538@attrs(repr=False, frozen=True, slots=True)539class _MaxLengthValidator:540 max_length = attrib()541 542 def __call__(self, inst, attr, value):543 """544 We use a callable class to be able to change the ``__repr__``.545 """546 if len(value) > self.max_length:547 msg = f"Length of '{attr.name}' must be <= {self.max_length}: {len(value)}"548 raise ValueError(msg)549 550 def __repr__(self):551 return f"<max_len validator for {self.max_length}>"552 553 554def max_len(length):555 """556 A validator that raises `ValueError` if the initializer is called557 with a string or iterable that is longer than *length*.558 559 Args:560 length (int): Maximum length of the string or iterable561 562 .. versionadded:: 21.3.0563 """564 return _MaxLengthValidator(length)565 566 567@attrs(repr=False, frozen=True, slots=True)568class _MinLengthValidator:569 min_length = attrib()570 571 def __call__(self, inst, attr, value):572 """573 We use a callable class to be able to change the ``__repr__``.574 """575 if len(value) < self.min_length:576 msg = f"Length of '{attr.name}' must be >= {self.min_length}: {len(value)}"577 raise ValueError(msg)578 579 def __repr__(self):580 return f"<min_len validator for {self.min_length}>"581 582 583def min_len(length):584 """585 A validator that raises `ValueError` if the initializer is called586 with a string or iterable that is shorter than *length*.587 588 Args:589 length (int): Minimum length of the string or iterable590 591 .. versionadded:: 22.1.0592 """593 return _MinLengthValidator(length)594 595 596@attrs(repr=False, slots=True, unsafe_hash=True)597class _SubclassOfValidator:598 type = attrib()599 600 def __call__(self, inst, attr, value):601 """602 We use a callable class to be able to change the ``__repr__``.603 """604 if not issubclass(value, self.type):605 msg = f"'{attr.name}' must be a subclass of {self.type!r} (got {value!r})."606 raise TypeError(607 msg,608 attr,609 self.type,610 value,611 )612 613 def __repr__(self):614 return f"<subclass_of validator for type {self.type!r}>"615 616 617def _subclass_of(type):618 """619 A validator that raises a `TypeError` if the initializer is called with a620 wrong type for this particular attribute (checks are performed using621 `issubclass` therefore it's also valid to pass a tuple of types).622 623 Args:624 type (type | tuple[type, ...]): The type(s) to check for.625 626 Raises:627 TypeError:628 With a human readable error message, the attribute (of type629 `attrs.Attribute`), the expected type, and the value it got.630 """631 return _SubclassOfValidator(type)632 633 634@attrs(repr=False, slots=True, unsafe_hash=True)635class _NotValidator:636 validator = attrib()637 msg = attrib(638 converter=default_if_none(639 "not_ validator child '{validator!r}' "640 "did not raise a captured error"641 )642 )643 exc_types = attrib(644 validator=deep_iterable(645 member_validator=_subclass_of(Exception),646 iterable_validator=instance_of(tuple),647 ),648 )649 650 def __call__(self, inst, attr, value):651 try:652 self.validator(inst, attr, value)653 except self.exc_types:654 pass # suppress error to invert validity655 else:656 raise ValueError(657 self.msg.format(658 validator=self.validator,659 exc_types=self.exc_types,660 ),661 attr,662 self.validator,663 value,664 self.exc_types,665 )666 667 def __repr__(self):668 return f"<not_ validator wrapping {self.validator!r}, capturing {self.exc_types!r}>"669 670 671def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)):672 """673 A validator that wraps and logically 'inverts' the validator passed to it.674 It will raise a `ValueError` if the provided validator *doesn't* raise a675 `ValueError` or `TypeError` (by default), and will suppress the exception676 if the provided validator *does*.677 678 Intended to be used with existing validators to compose logic without679 needing to create inverted variants, for example, ``not_(in_(...))``.680 681 Args:682 validator: A validator to be logically inverted.683 684 msg (str):685 Message to raise if validator fails. Formatted with keys686 ``exc_types`` and ``validator``.687 688 exc_types (tuple[type, ...]):689 Exception type(s) to capture. Other types raised by child690 validators will not be intercepted and pass through.691 692 Raises:693 ValueError:694 With a human readable error message, the attribute (of type695 `attrs.Attribute`), the validator that failed to raise an696 exception, the value it got, and the expected exception types.697 698 .. versionadded:: 22.2.0699 """700 try:701 exc_types = tuple(exc_types)702 except TypeError:703 exc_types = (exc_types,)704 return _NotValidator(validator, msg, exc_types)705 706 707@attrs(repr=False, slots=True, unsafe_hash=True)708class _OrValidator:709 validators = attrib()710 711 def __call__(self, inst, attr, value):712 for v in self.validators:713 try:714 v(inst, attr, value)715 except Exception: # noqa: BLE001, PERF203, S112716 continue717 else:718 return719 720 msg = f"None of {self.validators!r} satisfied for value {value!r}"721 raise ValueError(msg)722 723 def __repr__(self):724 return f"<or validator wrapping {self.validators!r}>"725 726 727def or_(*validators):728 """729 A validator that composes multiple validators into one.730 731 When called on a value, it runs all wrapped validators until one of them is732 satisfied.733 734 Args:735 validators (~collections.abc.Iterable[typing.Callable]):736 Arbitrary number of validators.737 738 Raises:739 ValueError:740 If no validator is satisfied. Raised with a human-readable error741 message listing all the wrapped validators and the value that742 failed all of them.743 744 .. versionadded:: 24.1.0745 """746 vals = []747 for v in validators:748 vals.extend(v.validators if isinstance(v, _OrValidator) else [v])749 750 return _OrValidator(tuple(vals))751 