diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e8a4d778479ca1..3971045a4b0284 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -57,6 +57,9 @@ Changes: - Validators can now be defined conveniently inline by using the attribute as a decorator. Check out the `examples `_ to see it in action! `#143 `_ +- Conversion can now be made optional using ``attr.converters.optional()``. + `#105 `_ + `#173 `_ - ``attr.make_class()`` now accepts the keyword argument ``bases`` which allows for subclassing. `#152 `_ - Metaclasses are now preserved with ``slots=True``. diff --git a/docs/api.rst b/docs/api.rst index 27c942f0b4c844..3a5b89634bccec 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -284,6 +284,24 @@ Validators C(x=None) +Converters +---------- + +.. autofunction:: attr.converters.optional + + For example: + + .. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib(convert=attr.converters.optional(int)) + >>> C(None) + C(x=None) + >>> C(42) + C(x=42) + + Deprecated APIs --------------- diff --git a/src/attr/__init__.py b/src/attr/__init__.py index 0087f7e428b647..ec9b380b60f9b1 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -23,6 +23,7 @@ ) from . import exceptions from . import filters +from . import converters from . import validators @@ -54,6 +55,7 @@ "attrib", "attributes", "attrs", + "converters", "evolve", "exceptions", "fields", diff --git a/src/attr/converters.py b/src/attr/converters.py new file mode 100644 index 00000000000000..3b3bac92be524a --- /dev/null +++ b/src/attr/converters.py @@ -0,0 +1,24 @@ +""" +Commonly useful converters. +""" + +from __future__ import absolute_import, division, print_function + + +def optional(converter): + """ + A converter that allows an attribute to be optional. An optional attribute + is one which can be set to ``None``. + + :param callable converter: the converter that is used for non-``None`` + values. + + .. versionadded:: 17.1.0 + """ + + def optional_converter(val): + if val is None: + return None + return converter(val) + + return optional_converter diff --git a/tests/test_converters.py b/tests/test_converters.py new file mode 100644 index 00000000000000..daf39d8ace02a2 --- /dev/null +++ b/tests/test_converters.py @@ -0,0 +1,36 @@ +""" +Tests for `attr.converters`. +""" + +from __future__ import absolute_import + +import pytest + +from attr.converters import optional + + +class TestOptional(object): + """ + Tests for `optional`. + """ + def test_success_with_type(self): + """ + Wrapped converter is used as usual if value is not None. + """ + c = optional(int) + assert c("42") == 42 + + def test_success_with_none(self): + """ + Nothing happens if None. + """ + c = optional(int) + assert c(None) is None + + def test_fail(self): + """ + Propagates the underlying conversion error when conversion fails. + """ + c = optional(int) + with pytest.raises(ValueError): + c("not_an_int")