"""Data validation.
.. versionadded:: 0.9.0
.. _def-validator-function:
In this module a ``validator function`` is a callable that takes
a value as its only argument and returns normally if the value
is considered valid or raises a :exc:`ValueError` otherwise. It
may raise a :exc:`TypeError` if the value is not of the right type.
.. |VF| replace:: :ref:`validator function <def-validator-function>`
"""
import inspect
import itertools
import math
import re
import string
from collections.abc import Container, Sequence, Mapping, Set
from contextlib import suppress
from .strings import TranslationTable
from .utils import check_type
__all__ = ['chain_validator', 'float_validator', 'func2validator',
'in_validator', 'int_validator', 'interval_validator',
'is_valid_ean13', 'is_valid_iban', 'is_valid_isbn', 'is_valid_luhn',
'length_validator', 'mapping_validator', 'object_validator',
'pattern_validator', 'properties_validator', 'sequence_validator',
'set_validator']
# https://en.wikipedia.org/wiki/Bookland
_BOOKLAND = ('978', '979')
def _check_validator_callable(validator):
if not callable(validator):
raise TypeError(f'{validator!r} is not callable')
with suppress(ValueError):
sig = inspect.signature(validator)
parameters = sig.parameters
if len(parameters) != 1:
raise TypeError('validator must take 1 argument,'
f' not {len(parameters)}')
[docs]def object_validator(*, validator=None, value_type=None, strict_type=False,
allow_none=False):
"""Create a function that checks whether an object is valid.
>>> # validator for values of type str and length 5-10
>>> len_vf = length_validator(min_len=5, max_len=10)
>>> vf = object_validator(validator=len_vf, value_type=str)
>>> vf('abcde')
>>> vf('abc')
Traceback (most recent call last):
...
ValueError: invalid object: 'abc' (length must be in [5, 10], got 3)
>>> vf(b'abcde')
Traceback (most recent call last):
...
TypeError: value must be of type 'str', got 'bytes'
:param validator: |VF|
:type validator: callable or None
:param value_type: the type of the value (not checked if ``None``)
:type value_type: type or None
:param bool strict_type: if ``True`` instances of subclasses of
``value_type`` are not allowed
:param bool allow_none: if ``True`` ``None`` values are valid even
when ``value_type`` is set
:return: |VF|
:raises TypeError: if ``validator`` is not callable or
``value_type`` is not a type-object
"""
if validator:
_check_validator_callable(validator)
if value_type:
check_type(value_type, type,
msg='the value_type argument must be a type-object')
def f(value):
if allow_none and value is None:
return
if (value_type and (strict_type and type(value) is not value_type or
not isinstance(value, value_type))):
raise TypeError(f'value must be of type {value_type.__name__!r},'
f' got {value.__class__.__name__!r}')
if validator:
try:
validator(value)
except ValueError as ex:
raise ValueError(f'invalid object: {value!r} ({ex})')
return f
[docs]def interval_validator(*, min_value=None, max_value=None,
min_incl=True, max_incl=True):
"""Create a function that checks whether a value is in an interval.
The type of the checked values must at least support the operators
``<`` (for ``*_incl=False``) or ``<=`` (for ``*_incl=True``).
:param min_value: minimum value (``None`` means no limit)
:param max_value: maximum value (``None`` means no limit)
:param bool min_incl: if ``True`` ``min_value`` is included
:param bool max_incl: if ``True`` ``max_value`` is included
:return: |VF|
:raises ValueError: if ``min_value > max_value``
"""
if (min_value is not None and max_value is not None and
not (min_value <= max_value)):
raise ValueError('min_value is greater than max_value'
f' ({min_value} > {max_value})')
def f(value):
in_range = True
if min_value is not None:
if min_incl:
in_range = min_value <= value
else:
in_range = min_value < value
if in_range and max_value is not None:
if max_incl:
in_range = value <= max_value
else:
in_range = value < max_value
if not in_range:
interval = ''.join([
'[' if min_incl else ']',
f'{min_value}, {max_value}',
']' if max_incl else '['
])
raise ValueError(f'value {value!r} is not in {interval}')
return f
[docs]def int_validator(*, min_value=None, max_value=None, allow_floats=False):
"""Create a function that checks whether a value is a valid integer.
If ``allow_floats`` is ``True``, a float value as the argument of the
returned |VF| will not raise a :exc:`TypeError`. Instead a value that
represents an integer (such as ``1.0``) and is in the interval
``[min_value, max_value]`` will be considered valid. All other cases will
raise a :exc:`ValueError`.
:param min_value: minimum value (inclusive, ``None`` means no limit)
:type min_value: int or None
:param max_value: maximum value (inclusive, ``None`` means no limit)
:type max_value: int or None
:param bool allow_floats: see function description
:return: |VF|
:raises ValueError: if ``min_value > max_value``
:raises TypeError: if ``min_value`` or ``max_value`` are not
of type :class:`int`
"""
if min_value is not None:
check_type(min_value, int, 'min_value')
if max_value is not None:
check_type(max_value, int, 'max_value')
vf = interval_validator(min_value=min_value, max_value=max_value)
def f(value):
if allow_floats and isinstance(value, float):
if value.is_integer():
value = int(value)
else:
raise ValueError(f'value {value!r} is not in interval')
else:
check_type(value, int, 'value')
vf(value)
return f
[docs]def float_validator(*, min_value=None, max_value=None,
min_incl=True, max_incl=True,
allow_nan=False, allow_inf=False, allow_ints=False):
"""Create a function that checks whether a value is a valid float.
:param min_value: minimum value (``None`` means no limit)
:type min_value: float or None
:param max_value: maximum value (``None`` means no limit)
:type max_value: float or None
:param bool min_incl: if ``True`` ``min_value`` is included
:param bool max_incl: if ``True`` ``max_value`` is included
:param bool allow_nan: if ``True`` :data:`math.nan` is allowed as the
argument of the returned |VF|
:param bool allow_inf: if ``True`` :data:`math.inf` is allowed as the
argument of the returned |VF|
:param bool allow_ints: if ``True`` an integer value as the argument of the
returned |VF| will not raise a :exc:`TypeError`.
:return: |VF|
:raises ValueError: if ``min_value > max_value``
:raises TypeError: if ``min_value`` or ``max_value`` are not
of type :class:`float`
"""
if min_value is not None:
check_type(min_value, float, 'min_value')
if max_value is not None:
check_type(max_value, float, 'max_value')
vf = interval_validator(min_value=min_value, max_value=max_value,
min_incl=min_incl, max_incl=max_incl)
def f(value):
if value is math.nan:
if allow_nan:
return
raise ValueError('value NaN is not allowed')
if abs(value) == math.inf:
if allow_inf:
return
raise ValueError(
f'value {"+" if value > 0 else "-"}Inf is not allowed')
if allow_ints and isinstance(value, int):
value = float(value)
else:
check_type(value, float, 'value')
vf(value)
return f
[docs]def pattern_validator(pattern):
"""Create a function that checks a value with a pattern.
The type of the argument for the returned |VF| can be either
:class:`str` or :class:`bytes`. It must be the same type that
is used for the pattern.
The check is done by using :meth:`re.Pattern.search`.
>>> # validator for values of type str and length 5-10
>>> vf = pattern_validator(r'^.{5,10}$')
>>> vf('abcde')
>>> vf('abc')
Traceback (most recent call last):
...
ValueError: invalid value: 'abc'
>>> vf(b'abcde')
Traceback (most recent call last):
...
TypeError: the type of 'value' must be 'str', got 'bytes'
:param pattern: regular expression pattern (see: module :mod:`re`)
:type pattern: bytes or str or compiled pattern
:raises TypeError: if ``pattern`` has the wrong type
"""
check_type(pattern, (bytes, str, re.Pattern), 'pattern')
if not isinstance(pattern, re.Pattern):
pattern = re.compile(pattern)
value_type = type(pattern.pattern)
def f(value):
check_type(value, value_type, 'value')
if not pattern.search(value):
raise ValueError(f'invalid value: {value!r}')
return f
[docs]def sequence_validator(validator):
"""Create a function that checks a sequence.
The check is done by applying the ``validator`` to
each item in the sequence.
>>> vf = sequence_validator(func2validator(str.isupper))
>>> vf('ABC')
>>> vf('AbC')
Traceback (most recent call last):
...
ValueError: error at sequence index 1: invalid value: 'b'
:param validator: |VF|
:return: |VF|
:raises TypeError: if ``validator`` is not callable
"""
_check_validator_callable(validator)
def f(value):
check_type(value, Sequence, 'value')
for i, item in enumerate(value):
try:
validator(item)
except ValueError as ex:
raise ValueError(f'error at sequence index {i}: {ex}')
return f
[docs]def set_validator(validator):
"""Create a function that checks a set.
The check is done by applying the ``validator`` to
each element in the set.
:param validator: |VF|
:return: |VF|
:raises TypeError: if ``validator`` is not callable
"""
_check_validator_callable(validator)
def f(value):
check_type(value, Set, 'value')
for elem in value:
try:
validator(elem)
except ValueError as ex:
raise ValueError(f'error in set: {ex}')
return f
[docs]def mapping_validator(validator, what='values'):
"""Create a function that checks a mapping.
The check is done by applying the ``validator`` to each key
(if ``what='keys'``), value (if ``what='values'``) or
(key, value)-tuple (if ``what='items'``).
:param validator: |VF|
:param str what: see function description
:return: |VF|
:raises TypeError: if ``validator`` is not callable
"""
_check_validator_callable(validator)
check_type(what, str, 'what')
if what not in ('keys', 'values', 'items'):
raise ValueError("what must be one of 'keys', 'values', 'items'")
def f(value):
check_type(value, Mapping, 'value')
for key, value in value.items():
if what == 'keys':
try:
validator(key)
except ValueError as ex:
raise ValueError(f'error in mapping key: {ex}')
elif what == 'values':
try:
validator(value)
except ValueError as ex:
raise ValueError('error in mapping: value for key'
f' {key!r}: {ex}')
else:
try:
validator((key, value))
except ValueError as ex:
raise ValueError(f'error in mapping item: {ex}')
return f
_str_object = object_validator(value_type=str)
[docs]def properties_validator(validators, mapping=False):
"""Create a function that checks the properties of an object.
The ``validators`` argument must be a mapping from a property name
to a |VF| or ``None`` if only the existence of a property should be
checked. If the argument value for the returned
|VF| is missing a property, the value is considered invalid (no
:exc:`AttributeError` will be raised).
If ``mapping`` is ``True`` the value must be a mapping with string keys
that are used as properties.
>>> validators = dict(a=func2validator(str.isupper), b=None)
>>> vf = properties_validator(validators, True)
>>> vf({'a': 'ABC', 'b': 1})
>>> vf({'a': 'ABC'})
Traceback (most recent call last):
...
ValueError: missing property 'b'
>>> vf({'a': 'abc', 'b': 1})
Traceback (most recent call last):
...
ValueError: invalid property 'a': invalid value: 'abc'
:param dict validators: |VF| for each property
:param bool mapping: if ``True`` the value must be a mapping
:raises TypeError: if ``validators`` is not a mapping or keys are not
strings or values are not callable or ``None``
"""
check_type(validators, Mapping, 'validators')
mapping_validator(_str_object, 'keys')(validators)
for validator in validators.values():
validator is None or _check_validator_callable(validator)
def f(value):
for name, validator in validators.items():
try:
if mapping:
p = value[name]
else:
p = getattr(value, name)
try:
if validator:
validator(p)
except ValueError as ex:
raise ValueError(f'invalid property {name!r}: {ex}')
except (AttributeError, KeyError):
raise ValueError(f'missing property {name!r}')
return f
_positive_int = int_validator(min_value=0)
[docs]def length_validator(min_len=0, max_len=None):
"""Create a function that checks the length.
The type of the checked values must support the
:func:`len` function.
:param int min_len: the minimum length
:param max_len: the maximum length (``None`` means no limit)
:type max_len: int or None
:raises ValueError: if ``min_len`` or ``max_len`` are negative integers
or ``min_len`` > ``max_len``
:raises TypeError: if ``min_len`` or ``max_len`` are not
of type :class:`int`
"""
check_type(min_len, int, 'min_len')
_positive_int(min_len)
if max_len is not None:
check_type(max_len, int, 'max_len')
_positive_int(max_len)
vf = int_validator(min_value=min_len, max_value=max_len)
def f(value):
try:
vf(len(value))
except ValueError:
raise ValueError(
f'length must be in [{min_len}, {max_len}], got {len(value)}')
return f
[docs]def in_validator(container, negate=False):
"""Create a function that checks for membership.
:param container: container object (must support the ``in`` operator)
:param bool negate: if ``True`` the value is valid if it is ``not in``
the container.
:return: |VF|
:raises TypeError: if ``container`` does not support the ``in`` operator
"""
if not isinstance(container, Container):
iter(container)
def f(value):
if (negate and value in container or
not negate and value not in container):
raise ValueError(f'invalid value: {value!r}')
return f
[docs]def func2validator(func, err_result=False):
"""Convert a function to a |VF|.
The returned |VF| raises a :exc:`ValueError` if ``func()`` returns
``err_result``.
>>> vf = func2validator(str.isupper)
>>> vf('A')
>>> vf('a')
Traceback (most recent call last):
...
ValueError: invalid value: 'a'
:param func: callable that takes a value as its only argument
:param bool err_result: the result, that will raise the ValueError
:return: |VF|
"""
def f(value):
if func(value) == err_result:
raise ValueError(f'invalid value: {value!r}')
return f
[docs]def chain_validator(*validators):
"""Chain some validators.
>>> str_vf = object_validator(value_type=str)
>>> len_vf = length_validator(min_len=5, max_len=10)
>>> # validator for values of type str and length 5-10
>>> vf = chain_validator(str_vf, len_vf)
>>> vf('abcde')
>>> vf('abc')
Traceback (most recent call last):
...
ValueError: length must in [5, 10], got 3
>>> vf(b'abcde')
Traceback (most recent call last):
...
TypeError: value must be of type 'str', got 'bytes'
:param validators: validator functions
:return: |VF|
:raises TypeError: if one of the ``validators`` is not callable
"""
for validator in validators:
_check_validator_callable(validator)
def f(value):
for validator in validators:
validator(value)
return f
_iban_trans_table = TranslationTable(dict(zip(string.ascii_uppercase,
map(str, range(10, 36)))),
string.digits)
[docs]def is_valid_iban(s):
"""Check whether a string is a valid IBAN.
IBAN = `International Bank Account Number
<https://en.wikipedia.org/wiki/International_Bank_Account_Number>`_
The string must not contain any separators;
only the characters ``A-Z`` and ``0-9`` are allowed.
:param str s: the string
:return: ``True`` if the string is a valid IBAN
:rtype: bool
:raise ValueError: if a character is not allowed
"""
return int((s[4:] + s[:4]).translate(_iban_trans_table)) % 97 == 1
[docs]def is_valid_ean13(s):
"""Check whether a string is a valid EAN-13.
EAN = `European Article Number
<https://en.wikipedia.org/wiki/European_Article_Number>`_
The string must not contain any separators; only the characters ``0-9``
are allowed and the length of the string must be 13.
:param str s: the string
:return: ``True`` if the string is a valid EAN-13
:rtype: bool
:raise ValueError: if a character is not allowed or the length is wrong
"""
if len(s) != 13:
raise ValueError(f'length of string must be 13 not {len(s)}')
if not s.isdecimal():
raise ValueError('only characters 0-9 are allowed')
return sum(int(c) * w for c, w in zip(s, itertools.cycle((1, 3)))) % 10 == 0
[docs]def is_valid_isbn(s):
"""Check whether a string is a valid ISBN.
ISBN = `International Standard Book Number
<https://en.wikipedia.org/wiki/International_Standard_Book_Number>`_
The string must not contain any separators; only the characters ``0-9``
plus ``X`` for ISBN-10 are allowed and the length of the string must be
either 10 or 13.
:param str s: the string
:return: ``True`` if the string is a valid ISBN
:rtype: bool
:raise ValueError: if a character is not allowed or the length is wrong
"""
if len(s) == 13:
if s[0:3] not in _BOOKLAND:
raise ValueError(
f'ISBN-13 must start with {" or ".join(_BOOKLAND)}')
return is_valid_ean13(s)
if len(s) not in (10, 13):
raise ValueError(f'length of string must be 10 or 13 not {len(s)}')
if not (s[:-1].isdecimal() and (s[-1].isdecimal() or s[-1] == 'X')):
raise ValueError('only characters 0-9 and X are allowed')
return ((sum(int(c) * w for c, w in zip(s[:-1], range(10, 1, -1))) +
(10 if s[-1] == 'X' else int(s[-1]))) % 11 == 0)
[docs]def is_valid_luhn(s):
"""Check whether a string is valid according to the Luhn algorithm.
The `Luhn algorithm
<https://en.wikipedia.org/wiki/Luhn_algorithm>`_ is used to validate
a variety of identification numbers, e.g. credit card numbers.
The string must not contain any separators; only the characters ``0-9``
are allowed.
:param str s: the string
:return: ``True`` if the string is valid
:rtype: bool
:raise ValueError: if a character is not allowed
"""
if not s.isdecimal():
raise ValueError('only characters 0-9 are allowed')
return sum(sum(divmod(int(c) * w, 10))
for c, w in zip(s[::-1], itertools.cycle((1, 2)))) % 10 == 0