Skip to content

Commit

Permalink
Only turn optional into a Converter if needed
Browse files Browse the repository at this point in the history
  • Loading branch information
filbranden committed Nov 13, 2024
1 parent 9b22892 commit cc8e100
Show file tree
Hide file tree
Showing 3 changed files with 28 additions and 18 deletions.
10 changes: 8 additions & 2 deletions src/attr/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,19 @@ def optional_converter(val, inst, field):
return None
return converter(val, inst, field)

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

else:

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

result = optional_converter

xtr = _AnnotationExtractor(converter)

t = xtr.get_first_param_type()
Expand All @@ -57,7 +63,7 @@ def optional_converter(val, inst, field):
if rt:
optional_converter.__annotations__["return"] = typing.Optional[rt]

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


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

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

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

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

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

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

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

@pytest.mark.skipif(
sys.version_info[:2] < (3, 11),
Expand Down
14 changes: 11 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", None, None) == 42
assert c("42") == 42

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

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

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

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

def test_converter_instance(self):
"""
Works when passed a Converter instance as argument.
"""
c = optional(Converter(to_bool))

assert True is c("yes", None, None)


class TestDefaultIfNone:
Expand Down

0 comments on commit cc8e100

Please sign in to comment.