diff --git a/attr/__init__.py b/attr/__init__.py index f3e6f19d..c0e3be03 100644 --- a/attr/__init__.py +++ b/attr/__init__.py @@ -7,6 +7,7 @@ from ._funcs import ( assoc, fields, has, + valid, ) from ._make import ( Attribute, @@ -40,5 +41,6 @@ __all__ = [ "ib", "make_class", "s", + "valid", "validators", ] diff --git a/attr/_funcs.py b/attr/_funcs.py index cc8103ef..4bb3baf1 100644 --- a/attr/_funcs.py +++ b/attr/_funcs.py @@ -92,3 +92,15 @@ def assoc(inst, **changes): ) setattr(new, k, v) return new + + +def valid(inst): + """ + Validate all attributes on *inst* that have a validator. + + Leaves all exceptions through. + + :param inst: Instance of a class with ``attrs`` attributes. + """ + for a in fields(inst.__class__): + a.validator(a, getattr(inst, a.name)) diff --git a/docs/api.rst b/docs/api.rst index 2e4603ef..387d77b6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -150,6 +150,23 @@ Helpers False +.. autofunction:: valid + + For example: + + .. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib(validator=attr.validators.instance_of(int)) + >>> i = C(1) + >>> i.x = "1" + >>> attr.valid(i) + Traceback (most recent call last): + ... + TypeError: ("'x' must be (got '1' that is a ).", Attribute(name='x', default=NOTHING, validator=>, no_repr=False, no_cmp=False, no_hash=False, no_init=False), , '1') + + .. _api_validators: Validators diff --git a/docs/examples.rst b/docs/examples.rst index 72ea2573..e1fb9b88 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -120,6 +120,17 @@ If the value does not pass the validator's standards, it just raises an appropri ... ValueError: 'x' has to be smaller than 5! +``attrs`` won't intercept your changes to those attributes but you can always call :func:``attr.valid`` on any instance to verify, that it's still valid: + +.. doctest:: + + >>> i = C(4) + >>> i.x = 5 # works, no magic here + >>> attr.valid(i) + Traceback (most recent call last): + ... + ValueError: 'x' has to be smaller than 5! + ``attrs`` ships with a bunch of validators, make sure to :ref:`check them out ` before writing your own: .. doctest:: diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 3f772353..82a16df7 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -13,11 +13,13 @@ from attr._funcs import ( assoc, fields, has, + valid, ) from attr._make import ( Attribute, attr, attributes, + make_class, ) @@ -167,3 +169,30 @@ class TestAssoc(object): assert ( "y is not an attrs attribute on {cl!r}.".format(cl=C), ) == e.value.args + + +class TestValid(object): + """ + Tests for `valid`. + """ + def test_success(self): + """ + If the validator suceeds, nothing gets raised. + """ + C = make_class("C", {"x": attr(validator=lambda _, __: None)}) + valid(C(1)) + + def test_propagates(self): + """ + The exception of the validator is handed through. + """ + def raiser(_, value): + if value == 42: + raise FloatingPointError + + C = make_class("C", {"x": attr(validator=raiser)}) + i = C(1) + i.x = 42 + + with pytest.raises(FloatingPointError): + valid(i)