Skip to content

Commit

Permalink
Add additonal validators (#845)
Browse files Browse the repository at this point in the history
* Add additonal validators

* Python 2 \o/

* Add changelog entry

* Add "versionadded" tags

* More python 2

* Add doctests and rename maxlen to max_len

Co-authored-by: Hynek Schlawack <[email protected]>
  • Loading branch information
sscherfke and hynek authored Sep 24, 2021
1 parent f57b6a6 commit 52fcad2
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 1 deletion.
1 change: 1 addition & 0 deletions changelog.d/845.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added new validators: ``lt(val)`` (< val), ``le(va)`` (≤ val), ``ge(val)`` (≥ val), ``gt(val)`` (> val), and `maxlen(n)`.
80 changes: 80 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,86 @@ Validators
``attrs`` comes with some common validators in the ``attrs.validators`` module:


.. autofunction:: attr.validators.lt

For example:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.lt(42))
>>> C(41)
C(x=41)
>>> C(42)
Traceback (most recent call last):
...
ValueError: ("'x' must be < 42: 42")

.. autofunction:: attr.validators.le

For example:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.le(42))
>>> C(42)
C(x=42)
>>> C(43)
Traceback (most recent call last):
...
ValueError: ("'x' must be <= 42: 43")

.. autofunction:: attr.validators.ge

For example:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.ge(42))
>>> C(42)
C(x=42)
>>> C(41)
Traceback (most recent call last):
...
ValueError: ("'x' must be => 42: 41")

.. autofunction:: attr.validators.gt

For example:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.gt(42))
>>> C(43)
C(x=43)
>>> C(42)
Traceback (most recent call last):
...
ValueError: ("'x' must be > 42: 42")

.. autofunction:: attr.validators.max_len

For example:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.max_len(4))
>>> C("spam")
C(x='spam')
>>> C("bacon")
Traceback (most recent call last):
...
ValueError: ("Length of 'x' must be <= 4: 5")

.. autofunction:: attr.validators.instance_of


Expand Down
111 changes: 111 additions & 0 deletions src/attr/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import absolute_import, division, print_function

import operator
import re

from ._make import _AndValidator, and_, attrib, attrs
Expand All @@ -14,10 +15,15 @@
"and_",
"deep_iterable",
"deep_mapping",
"ge",
"gt",
"in_",
"instance_of",
"is_callable",
"le",
"lt",
"matches_re",
"max_len",
"optional",
"provides",
]
Expand Down Expand Up @@ -377,3 +383,108 @@ def deep_mapping(key_validator, value_validator, mapping_validator=None):
:raises TypeError: if any sub-validators fail
"""
return _DeepMapping(key_validator, value_validator, mapping_validator)


@attrs(repr=False, frozen=True, slots=True)
class _NumberValidator(object):
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(object):
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)
5 changes: 5 additions & 0 deletions src/attr/validators.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,8 @@ def deep_mapping(
mapping_validator: Optional[_ValidatorType[_M]] = ...,
) -> _ValidatorType[_M]: ...
def is_callable() -> _ValidatorType[_T]: ...
def lt(val: _T) -> _ValidatorType[_T]: ...
def le(val: _T) -> _ValidatorType[_T]: ...
def ge(val: _T) -> _ValidatorType[_T]: ...
def gt(val: _T) -> _ValidatorType[_T]: ...
def max_len(length: int) -> _ValidatorType[_T]: ...
Loading

0 comments on commit 52fcad2

Please sign in to comment.