Repository: python-attrs/attrs (python)

Issue:
Optional `convert`
Is it, or could it be, possible to have optional type conversion using the `convert` argument, with the same logic as validation?

Something like:

``` python
@attr.s
class Thing(object):
    x = attr.ib(default=None, convert=optional(float), validator=optional(validate_x))
```

Apologies if it's already supported; I just don't see how in the docs.


Pull request description:
Allow optional conversion
Fixes #105

... or at least, provides the simplest possible solution to #105.

I.e,
-it adds a simple callable that makes existing converters optional, using the code suggested by @Tinche 
-it does *not* provide a single callable for both converters and validators .  Please let me know if that's necessary

I went through the checklist; the only thing that I think is not done is the addition to the hypothesis testing strategy.  I'm submitting and asking about that ;) 

# Pull Request Check List

This is just a reminder about the most common mistakes.  Please make sure that you tick all *appropriate* boxes.  But please read our [contribution guide](https://attrs.readthedocs.io/en/latest/contributing.html) at least once, it will save you unnecessary review cycles!

- [x] Added **tests** for changed code.
- [ ] New features have been added to our [Hypothesis testing strategy](https://github.com/python-attrs/attrs/blob/master/tests/utils.py).
- [x] Updated **documentation** for changed code.
- [x] Documentation in `.rst` files is written using [semantic newlines](http://rhodesmill.org/brandon/2012/one-sentence-per-line/).
- [x] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded).
- [x] Changes (and possible deprecations) are documented in [`CHANGELOG.rst`](https://github.com/python-attrs/attrs/blob/master/CHANGELOG.rst).

If you have *any* questions to *any* of the points above, just **submit and ask**!  This checklist is here to *help* you, not to deter you from contributing!

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index e8a4d77..3971045 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 <http://www.attrs.org/en/stable/examples.html#validators>`_ to see it in action!
   `#143 <https://github.com/python-attrs/attrs/issues/143>`_
+- Conversion can now be made optional using ``attr.converters.optional()``.
+  `#105 <https://github.com/python-attrs/attrs/issues/105>`_
+  `#173 <https://github.com/python-attrs/attrs/pull/173>`_
 - ``attr.make_class()`` now accepts the keyword argument ``bases`` which allows for subclassing.
   `#152 <https://github.com/python-attrs/attrs/pull/152>`_
 - Metaclasses are now preserved with ``slots=True``.
diff --git a/docs/api.rst b/docs/api.rst
index 27c942f..3a5b896 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 0087f7e..ec9b380 100644
--- a/src/attr/__init__.py
+++ b/src/attr/__init__.py
@@ -23,6 +23,7 @@ from ._config import (
 )
 from . import exceptions
 from . import filters
+from . import converters
 from . import validators
 
 
@@ -54,6 +55,7 @@ __all__ = [
     "attrib",
     "attributes",
     "attrs",
+    "converters",
     "evolve",
     "exceptions",
     "fields",
diff --git a/src/attr/converters.py b/src/attr/converters.py
new file mode 100644
index 0000000..3b3bac9
--- /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


Test patch:
diff --git a/tests/test_converters.py b/tests/test_converters.py
new file mode 100644
index 0000000..daf39d8
--- /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")