Skip to content

Commit e21793e

Browse files
filbrandenhynek
andauthored
Allow converter.optional to take a Converter such as converter.pipe as its argument (#1372)
* Allow converter.optional to take a converter such as converter.pipe as its argument * Only turn optional into a Converter if needed * Move call to Converter constructor to the end of optional() The constructor consumes __annotations__, so move the constructor call to after those have been set on the optional_converter function * Update tests/test_converters.py * Update tests/test_converters.py --------- Co-authored-by: Hynek Schlawack <hs@ox.cx>
1 parent ee0f19b commit e21793e

File tree

3 files changed

+68
-5
lines changed

3 files changed

+68
-5
lines changed

changelog.d/1372.change.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`attrs.converters.optional()` works again when taking `attrs.converters.pipe()` or another Converter as its argument.

src/attr/converters.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import typing
88

99
from ._compat import _AnnotationExtractor
10-
from ._make import NOTHING, Factory, pipe
10+
from ._make import NOTHING, Converter, Factory, pipe
1111

1212

1313
__all__ = [
@@ -33,10 +33,19 @@ def optional(converter):
3333
.. versionadded:: 17.1.0
3434
"""
3535

36-
def optional_converter(val):
37-
if val is None:
38-
return None
39-
return converter(val)
36+
if isinstance(converter, Converter):
37+
38+
def optional_converter(val, inst, field):
39+
if val is None:
40+
return None
41+
return converter(val, inst, field)
42+
43+
else:
44+
45+
def optional_converter(val):
46+
if val is None:
47+
return None
48+
return converter(val)
4049

4150
xtr = _AnnotationExtractor(converter)
4251

@@ -48,6 +57,9 @@ def optional_converter(val):
4857
if rt:
4958
optional_converter.__annotations__["return"] = typing.Optional[rt]
5059

60+
if isinstance(converter, Converter):
61+
return Converter(optional_converter, takes_self=True, takes_field=True)
62+
5163
return optional_converter
5264

5365

tests/test_converters.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ def test_fail(self):
143143
with pytest.raises(ValueError):
144144
c("not_an_int")
145145

146+
def test_converter_instance(self):
147+
"""
148+
Works when passed a Converter instance as argument.
149+
"""
150+
c = optional(Converter(to_bool))
151+
152+
assert True is c("yes", None, None)
153+
146154

147155
class TestDefaultIfNone:
148156
def test_missing_default(self):
@@ -272,6 +280,48 @@ class C:
272280
)
273281

274282

283+
class TestOptionalPipe:
284+
def test_optional(self):
285+
"""
286+
Nothing happens if None.
287+
"""
288+
c = optional(pipe(str, Converter(to_bool), bool))
289+
290+
assert None is c.converter(None, None, None)
291+
292+
def test_pipe(self):
293+
"""
294+
A value is given, run it through all wrapped converters.
295+
"""
296+
c = optional(pipe(str, Converter(to_bool), bool))
297+
298+
assert (
299+
True
300+
is c.converter("True", None, None)
301+
is c.converter(True, None, None)
302+
)
303+
304+
def test_instance(self):
305+
"""
306+
Should work when set as an attrib.
307+
"""
308+
309+
@attr.s
310+
class C:
311+
x = attrib(
312+
converter=optional(pipe(str, Converter(to_bool), bool)),
313+
default=None,
314+
)
315+
316+
c1 = C()
317+
318+
assert None is c1.x
319+
320+
c2 = C("True")
321+
322+
assert True is c2.x
323+
324+
275325
class TestToBool:
276326
def test_unhashable(self):
277327
"""

0 commit comments

Comments
 (0)