Skip to content

Commit

Permalink
Allow converter.optional to take a converter such as converter.pipe a…
Browse files Browse the repository at this point in the history
…s its argument
  • Loading branch information
filbranden committed Nov 13, 2024
1 parent 13e9a6a commit 2c43211
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 18 deletions.
21 changes: 15 additions & 6 deletions src/attr/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import typing

from ._compat import _AnnotationExtractor
from ._make import NOTHING, Factory, pipe
from ._make import NOTHING, Converter, Factory, pipe


__all__ = [
Expand All @@ -33,10 +33,19 @@ def optional(converter):
.. versionadded:: 17.1.0
"""

def optional_converter(val):
if val is None:
return None
return converter(val)
if isinstance(converter, Converter):

def optional_converter(val, inst, field):
if val is None:
return None
return converter(val, inst, field)

else:

def optional_converter(val, inst, field):
if val is None:
return None
return converter(val)

xtr = _AnnotationExtractor(converter)

Expand All @@ -48,7 +57,7 @@ def optional_converter(val):
if rt:
optional_converter.__annotations__["return"] = typing.Optional[rt]

return optional_converter
return Converter(optional_converter, takes_self=True, takes_field=True)


def default_if_none(default=NOTHING, factory=None):
Expand Down
22 changes: 13 additions & 9 deletions tests/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,22 +351,26 @@ def strify(x) -> str:
def identity(x):
return x

assert attr.converters.optional(int2str).__annotations__ == {
assert attr.converters.optional(int2str).converter.__annotations__ == {
"val": typing.Optional[int],
"return": typing.Optional[str],
}
assert attr.converters.optional(int_identity).__annotations__ == {
"val": typing.Optional[int]
}
assert attr.converters.optional(strify).__annotations__ == {
assert attr.converters.optional(
int_identity
).converter.__annotations__ == {"val": typing.Optional[int]}
assert attr.converters.optional(strify).converter.__annotations__ == {
"return": typing.Optional[str]
}
assert attr.converters.optional(identity).__annotations__ == {}
assert (
attr.converters.optional(identity).converter.__annotations__ == {}
)

def int2str_(x: int, y: int = 0) -> str:
return str(x)

assert attr.converters.optional(int2str_).__annotations__ == {
assert attr.converters.optional(
int2str_
).converter.__annotations__ == {
"val": typing.Optional[int],
"return": typing.Optional[str],
}
Expand All @@ -377,7 +381,7 @@ def test_optional_non_introspectable(self):
converter.
"""

assert attr.converters.optional(print).__annotations__ == {}
assert attr.converters.optional(print).converter.__annotations__ == {}

def test_optional_nullary(self):
"""
Expand All @@ -387,7 +391,7 @@ def test_optional_nullary(self):
def noop():
pass

assert attr.converters.optional(noop).__annotations__ == {}
assert attr.converters.optional(noop).converter.__annotations__ == {}

@pytest.mark.skipif(
sys.version_info[:2] < (3, 11),
Expand Down
46 changes: 43 additions & 3 deletions tests/test_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,15 @@ def test_success_with_type(self):
"""
c = optional(int)

assert c("42") == 42
assert c("42", None, None) == 42

def test_success_with_none(self):
"""
Nothing happens if None.
"""
c = optional(int)

assert c(None) is None
assert c(None, None, None) is None

def test_fail(self):
"""
Expand All @@ -141,7 +141,7 @@ def test_fail(self):
c = optional(int)

with pytest.raises(ValueError):
c("not_an_int")
c("not_an_int", None, None)


class TestDefaultIfNone:
Expand Down Expand Up @@ -272,6 +272,46 @@ class C:
)


class TestOptionalPipe:
def test_optional(self):
"""
Nothing happens if None.
"""
c = optional(pipe(str, Converter(to_bool), bool))

assert None is c.converter(None, None, None)

def test_pipe(self):
"""
A value is given, run it through all wrapped converters.
"""
c = optional(pipe(str, Converter(to_bool), bool))

assert (
True
is c.converter("True", None, None)
is c.converter(True, None, None)
)

def test_instance(self):
"""
Should work when set as an attrib.
"""

@attr.s
class C:
x = attrib(
converter=optional(pipe(str, Converter(to_bool), bool)),
default=None,
)

c1 = C()
assert None is c1.x

c2 = C("True")
assert True is c2.x


class TestToBool:
def test_unhashable(self):
"""
Expand Down

0 comments on commit 2c43211

Please sign in to comment.