Skip to content

Public API to check a declared attribute type and to check if it is mandatory/optional #159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,42 @@ Helpers
False


.. autofunction:: attr.validators.is_mandatory

For example:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.instance_of(int))
... y = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(int)))
>>> attr.validators.is_mandatory(attr.fields(C).x)
True
>>> attr.validators.is_mandatory(attr.fields(C).y)
False

.. versionadded:: ??? (to complete)


.. autofunction:: attr.validators.guess_type_from_validators

For example:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.instance_of(int))
... y = attr.ib()
>>> attr.validators.guess_type_from_validators(attr.fields(C).x)
int
>>> attr.validators.guess_type_from_validators(attr.fields(C).y) is None
True

.. versionadded:: ??? (to complete)


.. autofunction:: attr.asdict

For example:
Expand Down Expand Up @@ -277,6 +313,32 @@ Validators
>>> C(None)
C(x=None)

.. autofunction:: attr.validators.chain

For example:

.. doctest::

>>> def custom_validator(instance, attribute, value):
... allowed = {'+', '*'}
... if value not in allowed:
... raise ValueError('\'op\' has to be a string in ' + str(allowed) + '!')
>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.chain(custom_validator, attr.validators.instance_of(str)))
>>> C("+")
C(x='+')
>>> C("-")
Traceback (most recent call last):
...
ValueError: 'op' has to be a string in {'+', '*'}!
>>> C(None)
Traceback (most recent call last):
...
ValueError: 'op' has to be a string in {'+', '*'}!

.. versionadded:: ??? (to complete)


Deprecated APIs
---------------
Expand Down
80 changes: 80 additions & 0 deletions src/attr/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,83 @@ def optional(validator):
:param validator: A validator that is used for non-``None`` values.
"""
return _OptionalValidator(validator)


@attributes(repr=False, slots=True)
class _AndValidator(object):
validators = attr()

def __call__(self, inst, attr, value):
for v in self.validators:
v(inst, attr, value)
return

def __repr__(self):
return (
"<validator sequence : {seq}>".format(seq=repr(self.validators))
)


def chain(*validators):
"""
A validator that applies several validators in order

:param validators: A sequence of validators
"""
return _AndValidator(validators)


def _guess_type_from_validator(validator):
"""
Utility method to return the declared type of an attribute or None. It handles _OptionalValidator and _AndValidator
in order to unpack the validators.

:param validator:

:return: the type of attribute declared in an inner 'instance_of' validator (if any is found, the first one is used)
or None if no inner 'instance_of' validator is found
"""
if isinstance(validator, _OptionalValidator):
# Optional : look inside
return _guess_type_from_validator(validator.validator)

elif isinstance(validator, _AndValidator):
# Sequence : try each of them
for v in validator.validators:
typ = _guess_type_from_validator(v)
if typ is not None:
return typ
return None

elif isinstance(validator, _InstanceOfValidator):
# InstanceOf validator : found it !
return validator.type

else:
# we could not find the type
return None


def guess_type_from_validators(att):
"""
Utility method to return the declared type of an attribute or None. It handles _OptionalValidator and _AndValidator
in order to unpack the validators.

:param att: the attribute for which

:return: the type of attribute declared in an inner 'instance_of' validator (if any is found, the first one is used)
or None if no inner 'instance_of' validator is found
"""
return _guess_type_from_validator(att.validator)


def is_mandatory(att):
"""
Helper method to find if an attribute is mandatory, by checking if its validator is 'optional' or not.
Note that this does not check for the presence of a default value

:param att:

:return: a boolean indicating if the attribute is mandatory
"""
return not isinstance(att.validator, _OptionalValidator)
128 changes: 126 additions & 2 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
import pytest
import zope.interface

from attr.validators import instance_of, provides, optional
from attr._compat import TYPE

from attr.validators import instance_of, provides, optional, chain, guess_type_from_validators, is_mandatory
from .utils import simple_attr


Expand Down Expand Up @@ -154,3 +153,128 @@ def test_repr(self):
"<{type} 'int'>> or None>")
.format(type=TYPE)
) == repr(v)


class TestChain(object):
"""
Tests for `chain`.
"""
def test_success_with_type(self):
"""
Nothing happens if types match.
"""
v = chain(instance_of(int))
v(None, simple_attr("test"), 42)

def test_fail(self):
"""
Raises `TypeError` on wrong types.
"""
v = chain(instance_of(int))
a = simple_attr("test")
with pytest.raises(TypeError) as e:
v(None, a, "42")
assert (
"'test' must be <{type} 'int'> (got '42' that is a <{type} "
"'str'>).".format(type=TYPE),
a, int, "42",

) == e.value.args

def test_repr(self):
"""
Returned validator has a useful `__repr__`.
"""
v = chain(instance_of(int))
assert (
("<validator sequence : (<instance_of validator for type "
"<{type} 'int'>>,)>")
.format(type=TYPE)
) == repr(v)


class TestIsMandatory(object):
"""
Tests for utility method `is_mandatory`.
"""
def test_simple(self):
"""
if validator is a simple instance_of it works
"""
att = simple_attr("test", validator=instance_of(int))
assert is_mandatory(att) == True

def test_optional(self):
"""
if validator is an optional containing is_instance it works
"""
att = simple_attr("test", validator=optional(instance_of(int)))
assert is_mandatory(att) == False


def custom_validator(instance, attribute, value):
allowed = {'+', '*'}
if value not in allowed:
raise ValueError('\'op\' has to be a string in ' + str(allowed) + '!')


class TestGuessType(object):
"""
Tests for utility method `guess_type_from_validators`
"""

def test_simple(self):
"""
if validator is a simple instance_of it works
"""
att = simple_attr("test", validator=instance_of(int))
assert guess_type_from_validators(att) == int

def test_simple_not_found(self):
"""
if validator is a simple instance_of it works
"""
att = simple_attr("test", validator=custom_validator)
assert guess_type_from_validators(att) == None

def test_optional(self):
"""
if validator is an optional containing is_instance it works
"""
att = simple_attr("test", validator=optional(instance_of(int)))
assert guess_type_from_validators(att) == int

def test_optional_not_found(self):
"""
if validator is a simple instance_of it works
"""
att = simple_attr("test", validator=optional(custom_validator))
assert guess_type_from_validators(att) == None

def test_chain(self):
"""
if validator is a chain containing is_instance it also works
"""
att = simple_attr("test", validator=chain(custom_validator, instance_of(str)))
assert guess_type_from_validators(att) == str

def test_chain_not_found(self):
"""
if validator is a chain containing is_instance it also works
"""
att = simple_attr("test", validator=chain(custom_validator, custom_validator))
assert guess_type_from_validators(att) == None

def test_optional_chain(self):
"""
if validator is an optional containing a chain containing an is_instance it also works
"""
att = simple_attr("test", validator=optional(chain(custom_validator, instance_of(str))))
assert guess_type_from_validators(att) == str

def test_optional_chain_not_found(self):
"""
if validator is an optional containing a chain containing an is_instance it also works
"""
att = simple_attr("test", validator=optional(chain(custom_validator, custom_validator)))
assert guess_type_from_validators(att) == None