Skip to content

Commit 6fda0a4

Browse files
authored
Make Converter a kind of adapter, fix converters.pipe (#1328)
* Make Converter a kind of adapter, fix converters.pipe * Fix import cycle on 3.7/8 * stray space * Create static __call__ on Converter instantiation * Add tests for adapters doing passing correct args * Add news fragment
1 parent 53e632c commit 6fda0a4

5 files changed

Lines changed: 97 additions & 16 deletions

File tree

changelog.d/1328.change.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`attrs.converters.pipe()` (and it's syntactic sugar of passing a list for `attrs.field()`'s / `attr.ib()`'s *converter* argument) works again when passing `attrs.setters.convert` to *on_setattr* (which is default for `attrs.define`).

src/attr/_make.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2709,6 +2709,25 @@ def __init__(self, converter, *, takes_self=False, takes_field=False):
27092709
converter
27102710
).get_first_param_type()
27112711

2712+
self.__call__ = {
2713+
(False, False): self._takes_only_value,
2714+
(True, False): self._takes_instance,
2715+
(False, True): self._takes_field,
2716+
(True, True): self._takes_both,
2717+
}[self.takes_self, self.takes_field]
2718+
2719+
def _takes_only_value(self, value, instance, field):
2720+
return self.converter(value)
2721+
2722+
def _takes_instance(self, value, instance, field):
2723+
return self.converter(value, instance)
2724+
2725+
def _takes_field(self, value, instance, field):
2726+
return self.converter(value, field)
2727+
2728+
def _takes_both(self, value, instance, field):
2729+
return self.converter(value, instance, field)
2730+
27122731
@staticmethod
27132732
def _get_global_name(attr_name: str) -> str:
27142733
"""
@@ -2915,18 +2934,7 @@ def pipe(*converters):
29152934

29162935
def pipe_converter(val, inst, field):
29172936
for c in converters:
2918-
if isinstance(c, Converter):
2919-
val = c.converter(
2920-
val,
2921-
*{
2922-
(False, False): (),
2923-
(True, False): (c.takes_self,),
2924-
(False, True): (c.takes_field,),
2925-
(True, True): (c.takes_self, c.takes_field),
2926-
}[c.takes_self, c.takes_field],
2927-
)
2928-
else:
2929-
val = c(val)
2937+
val = c(val, inst, field) if isinstance(c, Converter) else c(val)
29302938

29312939
return val
29322940

src/attr/setters.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
Commonly used hooks for on_setattr.
55
"""
66

7-
87
from . import _config
98
from .exceptions import FrozenAttributeError
109

@@ -56,14 +55,20 @@ def validate(instance, attrib, new_value):
5655

5756
def convert(instance, attrib, new_value):
5857
"""
59-
Run *attrib*'s converter -- if it has one -- on *new_value* and return the
58+
Run *attrib*'s converter -- if it has one -- on *new_value* and return the
6059
result.
6160
6261
.. versionadded:: 20.1.0
6362
"""
6463
c = attrib.converter
6564
if c:
66-
return c(new_value)
65+
# This can be removed once we drop 3.8 and use attrs.Converter instead.
66+
from ._make import Converter
67+
68+
if not isinstance(c, Converter):
69+
return c(new_value)
70+
71+
return c(new_value, instance, attrib)
6772

6873
return new_value
6974

tests/test_converters.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def test_pickle(self, takes_self, takes_field):
2828
assert c == new_c
2929
assert takes_self == new_c.takes_self
3030
assert takes_field == new_c.takes_field
31+
assert c.__call__.__name__ == new_c.__call__.__name__
3132

3233
@pytest.mark.parametrize(
3334
"scenario",
@@ -58,6 +59,38 @@ def test_fmt_converter_call(self, scenario):
5859

5960
assert expect == c._fmt_converter_call("le_name", "le_value")
6061

62+
def test_works_as_adapter(self):
63+
"""
64+
Converter instances work as adapters and pass the correct arguments to
65+
the wrapped converter callable.
66+
"""
67+
taken = None
68+
instance = object()
69+
field = object()
70+
71+
def save_args(*args):
72+
nonlocal taken
73+
taken = args
74+
return args[0]
75+
76+
Converter(save_args)(42, instance, field)
77+
78+
assert (42,) == taken
79+
80+
Converter(save_args, takes_self=True)(42, instance, field)
81+
82+
assert (42, instance) == taken
83+
84+
Converter(save_args, takes_field=True)(42, instance, field)
85+
86+
assert (42, field) == taken
87+
88+
Converter(save_args, takes_self=True, takes_field=True)(
89+
42, instance, field
90+
)
91+
92+
assert (42, instance, field) == taken
93+
6194

6295
class TestOptional:
6396
"""

tests/test_setattr.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,34 @@ def test_pipe(self):
109109
used. They can be supplied using the pipe functions or by passing a
110110
list to on_setattr.
111111
"""
112+
taken = None
113+
114+
def takes_all(val, instance, attrib):
115+
nonlocal taken
116+
taken = val, instance, attrib
117+
118+
return val
112119

113120
s = [setters.convert, lambda _, __, nv: nv + 1]
114121

115122
@attr.s
116123
class Piped:
117-
x1 = attr.ib(converter=int, on_setattr=setters.pipe(*s))
124+
x1 = attr.ib(
125+
converter=[
126+
attr.Converter(
127+
takes_all, takes_field=True, takes_self=True
128+
),
129+
int,
130+
],
131+
on_setattr=setters.pipe(*s),
132+
)
118133
x2 = attr.ib(converter=int, on_setattr=s)
119134

120135
p = Piped("41", "22")
121136

137+
assert ("41", p) == taken[:-1]
138+
assert "x1" == taken[-1].name
139+
122140
assert 41 == p.x1
123141
assert 22 == p.x2
124142

@@ -417,3 +435,19 @@ def test_docstring(self):
417435
"Method generated by attrs for class WithOnSetAttrHook."
418436
== WithOnSetAttrHook.__setattr__.__doc__
419437
)
438+
439+
def test_setattr_converter_piped(self):
440+
"""
441+
If a converter is used, it is piped through the on_setattr hooks.
442+
443+
Regression test for https://github.com/python-attrs/attrs/issues/1327
444+
"""
445+
446+
@attr.define # converter on setattr is implied in NG
447+
class C:
448+
x = attr.field(converter=[int])
449+
450+
c = C("1")
451+
c.x = "2"
452+
453+
assert 2 == c.x

0 commit comments

Comments
 (0)