validators.py
594 lines
| 16.4 KiB
| text/x-python
|
PythonLexer
Matt Harbison
|
r50538 | # SPDX-License-Identifier: MIT | ||
Siddharth Agarwal
|
r34398 | """ | ||
Commonly useful validators. | ||||
""" | ||||
Matt Harbison
|
r50538 | |||
import operator | ||||
import re | ||||
from contextlib import contextmanager | ||||
Siddharth Agarwal
|
r34398 | |||
Matt Harbison
|
r50538 | from ._config import get_run_validators, set_run_validators | ||
from ._make import _AndValidator, and_, attrib, attrs | ||||
from .exceptions import NotCallableError | ||||
try: | ||||
Pattern = re.Pattern | ||||
except AttributeError: # Python <3.7 lacks a Pattern type. | ||||
Pattern = type(re.compile("")) | ||||
Siddharth Agarwal
|
r34398 | |||
__all__ = [ | ||||
"and_", | ||||
Matt Harbison
|
r50538 | "deep_iterable", | ||
"deep_mapping", | ||||
"disabled", | ||||
"ge", | ||||
"get_disabled", | ||||
"gt", | ||||
Siddharth Agarwal
|
r34398 | "in_", | ||
"instance_of", | ||||
Matt Harbison
|
r50538 | "is_callable", | ||
"le", | ||||
"lt", | ||||
"matches_re", | ||||
"max_len", | ||||
"min_len", | ||||
Siddharth Agarwal
|
r34398 | "optional", | ||
"provides", | ||||
Matt Harbison
|
r50538 | "set_disabled", | ||
Siddharth Agarwal
|
r34398 | ] | ||
Matt Harbison
|
r50538 | def set_disabled(disabled): | ||
""" | ||||
Globally disable or enable running validators. | ||||
By default, they are run. | ||||
:param disabled: If ``True``, disable running all validators. | ||||
:type disabled: bool | ||||
.. warning:: | ||||
This function is not thread-safe! | ||||
.. versionadded:: 21.3.0 | ||||
""" | ||||
set_run_validators(not disabled) | ||||
def get_disabled(): | ||||
""" | ||||
Return a bool indicating whether validators are currently disabled or not. | ||||
:return: ``True`` if validators are currently disabled. | ||||
:rtype: bool | ||||
.. versionadded:: 21.3.0 | ||||
""" | ||||
return not get_run_validators() | ||||
@contextmanager | ||||
def disabled(): | ||||
""" | ||||
Context manager that disables running validators within its context. | ||||
.. warning:: | ||||
This context manager is not thread-safe! | ||||
.. versionadded:: 21.3.0 | ||||
""" | ||||
set_run_validators(False) | ||||
try: | ||||
yield | ||||
finally: | ||||
set_run_validators(True) | ||||
@attrs(repr=False, slots=True, hash=True) | ||||
class _InstanceOfValidator: | ||||
type = attrib() | ||||
Siddharth Agarwal
|
r34398 | |||
def __call__(self, inst, attr, value): | ||||
""" | ||||
We use a callable class to be able to change the ``__repr__``. | ||||
""" | ||||
if not isinstance(value, self.type): | ||||
raise TypeError( | ||||
"'{name}' must be {type!r} (got {value!r} that is a " | ||||
Matt Harbison
|
r50538 | "{actual!r}).".format( | ||
name=attr.name, | ||||
type=self.type, | ||||
actual=value.__class__, | ||||
value=value, | ||||
), | ||||
attr, | ||||
self.type, | ||||
value, | ||||
Siddharth Agarwal
|
r34398 | ) | ||
def __repr__(self): | ||||
Matt Harbison
|
r50538 | return "<instance_of validator for type {type!r}>".format( | ||
type=self.type | ||||
Siddharth Agarwal
|
r34398 | ) | ||
def instance_of(type): | ||||
""" | ||||
Matt Harbison
|
r50538 | A validator that raises a `TypeError` if the initializer is called | ||
with a wrong type for this particular attribute (checks are performed using | ||||
`isinstance` therefore it's also valid to pass a tuple of types). | ||||
Siddharth Agarwal
|
r34398 | |||
:param type: The type to check for. | ||||
:type type: type or tuple of types | ||||
:raises TypeError: With a human readable error message, the attribute | ||||
Matt Harbison
|
r50538 | (of type `attrs.Attribute`), the expected type, and the value it | ||
Siddharth Agarwal
|
r34398 | got. | ||
""" | ||||
return _InstanceOfValidator(type) | ||||
Matt Harbison
|
r50538 | @attrs(repr=False, frozen=True, slots=True) | ||
class _MatchesReValidator: | ||||
pattern = attrib() | ||||
match_func = attrib() | ||||
def __call__(self, inst, attr, value): | ||||
""" | ||||
We use a callable class to be able to change the ``__repr__``. | ||||
""" | ||||
if not self.match_func(value): | ||||
raise ValueError( | ||||
"'{name}' must match regex {pattern!r}" | ||||
" ({value!r} doesn't)".format( | ||||
name=attr.name, pattern=self.pattern.pattern, value=value | ||||
), | ||||
attr, | ||||
self.pattern, | ||||
value, | ||||
) | ||||
def __repr__(self): | ||||
return "<matches_re validator for pattern {pattern!r}>".format( | ||||
pattern=self.pattern | ||||
) | ||||
def matches_re(regex, flags=0, func=None): | ||||
r""" | ||||
A validator that raises `ValueError` if the initializer is called | ||||
with a string that doesn't match *regex*. | ||||
:param regex: a regex string or precompiled pattern to match against | ||||
:param int flags: flags that will be passed to the underlying re function | ||||
(default 0) | ||||
:param callable func: which underlying `re` function to call. Valid options | ||||
are `re.fullmatch`, `re.search`, and `re.match`; the default ``None`` | ||||
means `re.fullmatch`. For performance reasons, the pattern is always | ||||
precompiled using `re.compile`. | ||||
.. versionadded:: 19.2.0 | ||||
.. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. | ||||
""" | ||||
valid_funcs = (re.fullmatch, None, re.search, re.match) | ||||
if func not in valid_funcs: | ||||
raise ValueError( | ||||
"'func' must be one of {}.".format( | ||||
", ".join( | ||||
sorted( | ||||
e and e.__name__ or "None" for e in set(valid_funcs) | ||||
) | ||||
) | ||||
) | ||||
) | ||||
if isinstance(regex, Pattern): | ||||
if flags: | ||||
raise TypeError( | ||||
"'flags' can only be used with a string pattern; " | ||||
"pass flags to re.compile() instead" | ||||
) | ||||
pattern = regex | ||||
else: | ||||
pattern = re.compile(regex, flags) | ||||
if func is re.match: | ||||
match_func = pattern.match | ||||
elif func is re.search: | ||||
match_func = pattern.search | ||||
else: | ||||
match_func = pattern.fullmatch | ||||
return _MatchesReValidator(pattern, match_func) | ||||
@attrs(repr=False, slots=True, hash=True) | ||||
class _ProvidesValidator: | ||||
interface = attrib() | ||||
Siddharth Agarwal
|
r34398 | |||
def __call__(self, inst, attr, value): | ||||
""" | ||||
We use a callable class to be able to change the ``__repr__``. | ||||
""" | ||||
if not self.interface.providedBy(value): | ||||
raise TypeError( | ||||
"'{name}' must provide {interface!r} which {value!r} " | ||||
Matt Harbison
|
r50538 | "doesn't.".format( | ||
name=attr.name, interface=self.interface, value=value | ||||
), | ||||
attr, | ||||
self.interface, | ||||
value, | ||||
Siddharth Agarwal
|
r34398 | ) | ||
def __repr__(self): | ||||
Matt Harbison
|
r50538 | return "<provides validator for interface {interface!r}>".format( | ||
interface=self.interface | ||||
Siddharth Agarwal
|
r34398 | ) | ||
def provides(interface): | ||||
""" | ||||
Matt Harbison
|
r50538 | A validator that raises a `TypeError` if the initializer is called | ||
Siddharth Agarwal
|
r34398 | with an object that does not provide the requested *interface* (checks are | ||
performed using ``interface.providedBy(value)`` (see `zope.interface | ||||
<https://zopeinterface.readthedocs.io/en/latest/>`_). | ||||
Matt Harbison
|
r50538 | :param interface: The interface to check for. | ||
:type interface: ``zope.interface.Interface`` | ||||
Siddharth Agarwal
|
r34398 | |||
:raises TypeError: With a human readable error message, the attribute | ||||
Matt Harbison
|
r50538 | (of type `attrs.Attribute`), the expected interface, and the | ||
Siddharth Agarwal
|
r34398 | value it got. | ||
""" | ||||
return _ProvidesValidator(interface) | ||||
Matt Harbison
|
r50538 | @attrs(repr=False, slots=True, hash=True) | ||
class _OptionalValidator: | ||||
validator = attrib() | ||||
Siddharth Agarwal
|
r34398 | |||
def __call__(self, inst, attr, value): | ||||
if value is None: | ||||
return | ||||
self.validator(inst, attr, value) | ||||
def __repr__(self): | ||||
Matt Harbison
|
r50538 | return "<optional validator for {what} or None>".format( | ||
what=repr(self.validator) | ||||
Siddharth Agarwal
|
r34398 | ) | ||
def optional(validator): | ||||
""" | ||||
A validator that makes an attribute optional. An optional attribute is one | ||||
which can be set to ``None`` in addition to satisfying the requirements of | ||||
the sub-validator. | ||||
:param validator: A validator (or a list of validators) that is used for | ||||
non-``None`` values. | ||||
Matt Harbison
|
r50538 | :type validator: callable or `list` of callables. | ||
Siddharth Agarwal
|
r34398 | |||
.. versionadded:: 15.1.0 | ||||
.. versionchanged:: 17.1.0 *validator* can be a list of validators. | ||||
""" | ||||
if isinstance(validator, list): | ||||
return _OptionalValidator(_AndValidator(validator)) | ||||
return _OptionalValidator(validator) | ||||
Matt Harbison
|
r50538 | @attrs(repr=False, slots=True, hash=True) | ||
class _InValidator: | ||||
options = attrib() | ||||
Siddharth Agarwal
|
r34398 | |||
def __call__(self, inst, attr, value): | ||||
Matt Harbison
|
r50538 | try: | ||
in_options = value in self.options | ||||
except TypeError: # e.g. `1 in "abc"` | ||||
in_options = False | ||||
if not in_options: | ||||
Siddharth Agarwal
|
r34398 | raise ValueError( | ||
Matt Harbison
|
r50538 | "'{name}' must be in {options!r} (got {value!r})".format( | ||
name=attr.name, options=self.options, value=value | ||||
), | ||||
attr, | ||||
self.options, | ||||
value, | ||||
Siddharth Agarwal
|
r34398 | ) | ||
def __repr__(self): | ||||
Matt Harbison
|
r50538 | return "<in_ validator with options {options!r}>".format( | ||
options=self.options | ||||
Siddharth Agarwal
|
r34398 | ) | ||
def in_(options): | ||||
""" | ||||
Matt Harbison
|
r50538 | A validator that raises a `ValueError` if the initializer is called | ||
Siddharth Agarwal
|
r34398 | with a value that does not belong in the options provided. The check is | ||
performed using ``value in options``. | ||||
:param options: Allowed options. | ||||
Matt Harbison
|
r50538 | :type options: list, tuple, `enum.Enum`, ... | ||
Siddharth Agarwal
|
r34398 | |||
:raises ValueError: With a human readable error message, the attribute (of | ||||
Matt Harbison
|
r50538 | type `attrs.Attribute`), the expected options, and the value it | ||
Siddharth Agarwal
|
r34398 | got. | ||
.. versionadded:: 17.1.0 | ||||
Matt Harbison
|
r50538 | .. versionchanged:: 22.1.0 | ||
The ValueError was incomplete until now and only contained the human | ||||
readable error message. Now it contains all the information that has | ||||
been promised since 17.1.0. | ||||
Siddharth Agarwal
|
r34398 | """ | ||
return _InValidator(options) | ||||
Matt Harbison
|
r50538 | |||
@attrs(repr=False, slots=False, hash=True) | ||||
class _IsCallableValidator: | ||||
def __call__(self, inst, attr, value): | ||||
""" | ||||
We use a callable class to be able to change the ``__repr__``. | ||||
""" | ||||
if not callable(value): | ||||
message = ( | ||||
"'{name}' must be callable " | ||||
"(got {value!r} that is a {actual!r})." | ||||
) | ||||
raise NotCallableError( | ||||
msg=message.format( | ||||
name=attr.name, value=value, actual=value.__class__ | ||||
), | ||||
value=value, | ||||
) | ||||
def __repr__(self): | ||||
return "<is_callable validator>" | ||||
def is_callable(): | ||||
""" | ||||
A validator that raises a `attr.exceptions.NotCallableError` if the | ||||
initializer is called with a value for this particular attribute | ||||
that is not callable. | ||||
.. versionadded:: 19.1.0 | ||||
:raises `attr.exceptions.NotCallableError`: With a human readable error | ||||
message containing the attribute (`attrs.Attribute`) name, | ||||
and the value it got. | ||||
""" | ||||
return _IsCallableValidator() | ||||
@attrs(repr=False, slots=True, hash=True) | ||||
class _DeepIterable: | ||||
member_validator = attrib(validator=is_callable()) | ||||
iterable_validator = attrib( | ||||
default=None, validator=optional(is_callable()) | ||||
) | ||||
def __call__(self, inst, attr, value): | ||||
""" | ||||
We use a callable class to be able to change the ``__repr__``. | ||||
""" | ||||
if self.iterable_validator is not None: | ||||
self.iterable_validator(inst, attr, value) | ||||
for member in value: | ||||
self.member_validator(inst, attr, member) | ||||
def __repr__(self): | ||||
iterable_identifier = ( | ||||
"" | ||||
if self.iterable_validator is None | ||||
else " {iterable!r}".format(iterable=self.iterable_validator) | ||||
) | ||||
return ( | ||||
"<deep_iterable validator for{iterable_identifier}" | ||||
" iterables of {member!r}>" | ||||
).format( | ||||
iterable_identifier=iterable_identifier, | ||||
member=self.member_validator, | ||||
) | ||||
def deep_iterable(member_validator, iterable_validator=None): | ||||
""" | ||||
A validator that performs deep validation of an iterable. | ||||
:param member_validator: Validator(s) to apply to iterable members | ||||
:param iterable_validator: Validator to apply to iterable itself | ||||
(optional) | ||||
.. versionadded:: 19.1.0 | ||||
:raises TypeError: if any sub-validators fail | ||||
""" | ||||
if isinstance(member_validator, (list, tuple)): | ||||
member_validator = and_(*member_validator) | ||||
return _DeepIterable(member_validator, iterable_validator) | ||||
@attrs(repr=False, slots=True, hash=True) | ||||
class _DeepMapping: | ||||
key_validator = attrib(validator=is_callable()) | ||||
value_validator = attrib(validator=is_callable()) | ||||
mapping_validator = attrib(default=None, validator=optional(is_callable())) | ||||
def __call__(self, inst, attr, value): | ||||
""" | ||||
We use a callable class to be able to change the ``__repr__``. | ||||
""" | ||||
if self.mapping_validator is not None: | ||||
self.mapping_validator(inst, attr, value) | ||||
for key in value: | ||||
self.key_validator(inst, attr, key) | ||||
self.value_validator(inst, attr, value[key]) | ||||
def __repr__(self): | ||||
return ( | ||||
"<deep_mapping validator for objects mapping {key!r} to {value!r}>" | ||||
).format(key=self.key_validator, value=self.value_validator) | ||||
def deep_mapping(key_validator, value_validator, mapping_validator=None): | ||||
""" | ||||
A validator that performs deep validation of a dictionary. | ||||
:param key_validator: Validator to apply to dictionary keys | ||||
:param value_validator: Validator to apply to dictionary values | ||||
:param mapping_validator: Validator to apply to top-level mapping | ||||
attribute (optional) | ||||
.. versionadded:: 19.1.0 | ||||
:raises TypeError: if any sub-validators fail | ||||
""" | ||||
return _DeepMapping(key_validator, value_validator, mapping_validator) | ||||
@attrs(repr=False, frozen=True, slots=True) | ||||
class _NumberValidator: | ||||
bound = attrib() | ||||
compare_op = attrib() | ||||
compare_func = attrib() | ||||
def __call__(self, inst, attr, value): | ||||
""" | ||||
We use a callable class to be able to change the ``__repr__``. | ||||
""" | ||||
if not self.compare_func(value, self.bound): | ||||
raise ValueError( | ||||
"'{name}' must be {op} {bound}: {value}".format( | ||||
name=attr.name, | ||||
op=self.compare_op, | ||||
bound=self.bound, | ||||
value=value, | ||||
) | ||||
) | ||||
def __repr__(self): | ||||
return "<Validator for x {op} {bound}>".format( | ||||
op=self.compare_op, bound=self.bound | ||||
) | ||||
def lt(val): | ||||
""" | ||||
A validator that raises `ValueError` if the initializer is called | ||||
with a number larger or equal to *val*. | ||||
:param val: Exclusive upper bound for values | ||||
.. versionadded:: 21.3.0 | ||||
""" | ||||
return _NumberValidator(val, "<", operator.lt) | ||||
def le(val): | ||||
""" | ||||
A validator that raises `ValueError` if the initializer is called | ||||
with a number greater than *val*. | ||||
:param val: Inclusive upper bound for values | ||||
.. versionadded:: 21.3.0 | ||||
""" | ||||
return _NumberValidator(val, "<=", operator.le) | ||||
def ge(val): | ||||
""" | ||||
A validator that raises `ValueError` if the initializer is called | ||||
with a number smaller than *val*. | ||||
:param val: Inclusive lower bound for values | ||||
.. versionadded:: 21.3.0 | ||||
""" | ||||
return _NumberValidator(val, ">=", operator.ge) | ||||
def gt(val): | ||||
""" | ||||
A validator that raises `ValueError` if the initializer is called | ||||
with a number smaller or equal to *val*. | ||||
:param val: Exclusive lower bound for values | ||||
.. versionadded:: 21.3.0 | ||||
""" | ||||
return _NumberValidator(val, ">", operator.gt) | ||||
@attrs(repr=False, frozen=True, slots=True) | ||||
class _MaxLengthValidator: | ||||
max_length = attrib() | ||||
def __call__(self, inst, attr, value): | ||||
""" | ||||
We use a callable class to be able to change the ``__repr__``. | ||||
""" | ||||
if len(value) > self.max_length: | ||||
raise ValueError( | ||||
"Length of '{name}' must be <= {max}: {len}".format( | ||||
name=attr.name, max=self.max_length, len=len(value) | ||||
) | ||||
) | ||||
def __repr__(self): | ||||
return "<max_len validator for {max}>".format(max=self.max_length) | ||||
def max_len(length): | ||||
""" | ||||
A validator that raises `ValueError` if the initializer is called | ||||
with a string or iterable that is longer than *length*. | ||||
:param int length: Maximum length of the string or iterable | ||||
.. versionadded:: 21.3.0 | ||||
""" | ||||
return _MaxLengthValidator(length) | ||||
@attrs(repr=False, frozen=True, slots=True) | ||||
class _MinLengthValidator: | ||||
min_length = attrib() | ||||
def __call__(self, inst, attr, value): | ||||
""" | ||||
We use a callable class to be able to change the ``__repr__``. | ||||
""" | ||||
if len(value) < self.min_length: | ||||
raise ValueError( | ||||
"Length of '{name}' must be => {min}: {len}".format( | ||||
name=attr.name, min=self.min_length, len=len(value) | ||||
) | ||||
) | ||||
def __repr__(self): | ||||
return "<min_len validator for {min}>".format(min=self.min_length) | ||||
def min_len(length): | ||||
""" | ||||
A validator that raises `ValueError` if the initializer is called | ||||
with a string or iterable that is shorter than *length*. | ||||
:param int length: Minimum length of the string or iterable | ||||
.. versionadded:: 22.1.0 | ||||
""" | ||||
return _MinLengthValidator(length) | ||||