Skip to content

Commit

Permalink
Merge branch 'master' into auto-detect
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek committed Mar 5, 2020
2 parents dc25251 + 55d5ef4 commit 3dedd12
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 5 deletions.
2 changes: 2 additions & 0 deletions changelog.d/618.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added ``attr.converters.chain()``.
The feature allows combining multiple conversion callbacks into one.
9 changes: 9 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,15 @@ Validators
Converters
----------

.. autofunction:: attr.converters.chain

For convenience, it's also possible to pass a list to `attr.ib`'s converter argument.

Thus the following two statements are equivalent::

x = attr.ib(converter=attr.converter.chain(c1, c2, c3))
x = attr.ib(converter=[c1, c2, c3])

.. autofunction:: attr.converters.optional

For example:
Expand Down
27 changes: 25 additions & 2 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -2097,12 +2097,15 @@ def __init__(
self._validator = and_(*validator)
else:
self._validator = validator
if converter and isinstance(converter, (list, tuple)):
self.converter = chain(*converter)
else:
self.converter = converter
self.repr = repr
self.eq = eq
self.order = order
self.hash = hash
self.init = init
self.converter = converter
self.metadata = metadata
self.type = type
self.kw_only = kw_only
Expand Down Expand Up @@ -2236,7 +2239,7 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):


# These are required by within this module so we define them here and merely
# import into .validators.
# import into .validators / .converters.


@attrs(slots=True, hash=True)
Expand Down Expand Up @@ -2272,3 +2275,23 @@ def and_(*validators):
)

return _AndValidator(tuple(vals))


def chain(*converters):
"""
A converter that composes multiple converters into one.
When called on a value, it runs all wrapped converters.
:param converters: Arbitrary number of converters.
:type converters: callables
.. versionadded:: 20.1.0
"""

def chain_converter(val):
for converter in converters:
val = converter(val)
return val

return chain_converter
9 changes: 8 additions & 1 deletion src/attr/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@

from __future__ import absolute_import, division, print_function

from ._make import NOTHING, Factory
from ._make import NOTHING, Factory, chain


__all__ = [
"chain",
"optional",
"default_if_none",
]


def optional(converter):
Expand Down
1 change: 1 addition & 0 deletions src/attr/converters.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ from . import _ConverterType

_T = TypeVar("_T")

def chain(*validators: _ConverterType[_T]) -> _ConverterType[_T]: ...
def optional(
converter: _ConverterType[_T],
) -> _ConverterType[Optional[_T]]: ...
Expand Down
45 changes: 43 additions & 2 deletions tests/test_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

from __future__ import absolute_import

from distutils.util import strtobool

import pytest

from attr import Factory
from attr.converters import default_if_none, optional
import attr

from attr import Factory, attrib
from attr.converters import chain, default_if_none, optional


class TestOptional(object):
Expand Down Expand Up @@ -95,3 +99,40 @@ def test_none_factory(self):
c = default_if_none(default=Factory(list))

assert [] == c(None)


class TestChain(object):
def test_success(self):
"""
Succeeds if all wrapped converters succeed.
"""
c = chain(str, strtobool, bool)

assert True is c("True") is c(True)

def test_fail(self):
"""
Fails if any wrapped converter fails.
"""
c = chain(str, strtobool)

# First wrapped converter fails:
with pytest.raises(ValueError):
c(33)

# Last wrapped converter fails:
with pytest.raises(ValueError):
c("33")

def test_sugar(self):
"""
`chain(c1, c2, c3)` and `[c1, c2, c3]` are equivalent.
"""

@attr.s
class C(object):
a1 = attrib(default="True", converter=chain(str, strtobool, bool))
a2 = attrib(default=True, converter=[str, strtobool, bool])

c = C()
assert True is c.a1 is c.a2

0 comments on commit 3dedd12

Please sign in to comment.