Skip to content

Commit 15570ad

Browse files
committed
Add takes field
1 parent 55676d0 commit 15570ad

File tree

4 files changed

+90
-43
lines changed

4 files changed

+90
-43
lines changed

docs/api.rst

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,6 @@ Core
9393
>>> C([1, 2, 3])
9494
C(x=[1, 2, 3], y={1, 2, 3})
9595

96-
.. autoclass:: Converter
97-
98-
For example:
99-
100-
.. doctest::
101-
102-
>>> def complicated(value, self_):
103-
... return int(value) * self_.factor
104-
>>> @define
105-
... class C:
106-
... factor = 5 # not an *attrs* field
107-
... x = field(converter=attrs.Converter(complicated, takes_self=True))
108-
>>> C("42")
109-
C(x=210)
110-
11196

11297
Exceptions
11398
----------
@@ -622,6 +607,27 @@ Validators can be both globally and locally disabled:
622607
Converters
623608
----------
624609

610+
.. autoclass:: attrs.Converter
611+
612+
For example:
613+
614+
.. doctest::
615+
616+
>>> def complicated(value, self_, field):
617+
... return int(value) * self_.factor + field.metadata["offset"]
618+
>>> @define
619+
... class C:
620+
... factor = 5 # not an *attrs* field
621+
... x = field(
622+
... metadata={"offset": 200},
623+
... converter=attrs.Converter(
624+
... complicated,
625+
... takes_self=True, takes_field=True
626+
... ))
627+
>>> C("42")
628+
C(x=410)
629+
630+
625631
.. module:: attrs.converters
626632

627633
All objects from ``attrs.converters`` are also available from ``attr.converters`` (it's the same module in a different namespace).

src/attr/_make.py

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2214,10 +2214,11 @@ def _setattr_with_converter(attr_name, value_var, has_on_setattr):
22142214
Use the cached object.setattr to set *attr_name* to *value_var*, but run
22152215
its converter first.
22162216
"""
2217-
return "_setattr('%s', %s(%s, self))" % (
2217+
return "_setattr('%s', %s(%s, self, attr_dict['%s']))" % (
22182218
attr_name,
22192219
_INIT_CONVERTER_PAT % (attr_name,),
22202220
value_var,
2221+
attr_name,
22212222
)
22222223

22232224

@@ -2240,10 +2241,11 @@ def _assign_with_converter(attr_name, value_var, has_on_setattr):
22402241
if has_on_setattr:
22412242
return _setattr_with_converter(attr_name, value_var, True)
22422243

2243-
return "self.%s = %s(%s, self)" % (
2244+
return "self.%s = %s(%s, self, attr_dict['%s'])" % (
22442245
attr_name,
22452246
_INIT_CONVERTER_PAT % (attr_name,),
22462247
value_var,
2248+
attr_name,
22472249
)
22482250

22492251

@@ -2273,10 +2275,11 @@ def fmt_setter_with_converter(attr_name, value_var, has_on_setattr):
22732275
attr_name, value_var, has_on_setattr
22742276
)
22752277

2276-
return "_inst_dict['%s'] = %s(%s, self)" % (
2278+
return "_inst_dict['%s'] = %s(%s, self, attr_dict['%s'])" % (
22772279
attr_name,
22782280
_INIT_CONVERTER_PAT % (attr_name,),
22792281
value_var,
2282+
attr_name,
22802283
)
22812284

22822285
return (
@@ -2351,7 +2354,7 @@ def _attrs_to_init_script(
23512354
maybe_self = "self" if has_factory and a.default.takes_self else ""
23522355

23532356
if a.converter and not isinstance(a.converter, Converter):
2354-
converter = Converter(a.converter, takes_self=False)
2357+
converter = Converter(a.converter)
23552358
else:
23562359
converter = a.converter
23572360

@@ -2989,44 +2992,61 @@ class Converter:
29892992
"""
29902993
Stores a converter callable.
29912994
2992-
Allows for the wrapped converter to take additional arguments.
2995+
Allows for the wrapped converter to take additional arguments. The
2996+
arguments are passed in the order they are documented.
29932997
29942998
:param Callable converter: A callable that converts a value.
29952999
:param bool takes_self: Pass the partially initialized instance that is
2996-
being initialized as a positional argument. (default: `True`)
3000+
being initialized as a positional argument. (default: `False`)
3001+
:param bool takes_field: Pass the field definition (an `Attribute`) into
3002+
the converter as a positional argument. (default: `False`)
29973003
29983004
.. versionadded:: 24.1.0
29993005
"""
30003006

3001-
__slots__ = ("converter", "takes_self", "_first_param_type", "__call__")
3007+
__slots__ = (
3008+
"converter",
3009+
"takes_self",
3010+
"takes_field",
3011+
"_first_param_type",
3012+
"__call__",
3013+
)
30023014

3003-
def __init__(self, converter, *, takes_self=True):
3015+
def __init__(self, converter, *, takes_self=False, takes_field=False):
30043016
self.converter = converter
30053017
self.takes_self = takes_self
3006-
3007-
ann = _AnnotationExtractor(converter)
3008-
3009-
self._first_param_type = ann.get_first_param_type()
3018+
self.takes_field = takes_field
30103019

30113020
# Defining __call__ as a regular method leads to __annotations__ being
30123021
# overwritten at a class level.
3013-
def __call__(value, inst):
3014-
if not self.takes_self:
3015-
return self.converter(value)
3016-
3017-
return self.converter(value, inst)
3022+
def __call__(value, inst, field):
3023+
return self.converter(
3024+
*{
3025+
(False, False): (value,),
3026+
(True, False): (value, inst),
3027+
(False, True): (value, field),
3028+
(True, True): (value, inst, field),
3029+
}[(takes_self, takes_field)]
3030+
)
30183031

3032+
ann = _AnnotationExtractor(converter)
30193033
__call__.__annotations__.update(
30203034
ann.get_annotations_for_converter_callable()
30213035
)
30223036
self.__call__ = __call__
30233037

3038+
self._first_param_type = ann.get_first_param_type()
3039+
30243040
def __getstate__(self):
30253041
"""
30263042
Return a dict containing only converter and takes_self -- the rest gets
30273043
computed when loading.
30283044
"""
3029-
return {"converter": self.converter, "takes_self": self.takes_self}
3045+
return {
3046+
"converter": self.converter,
3047+
"takes_self": self.takes_self,
3048+
"takes_field": self.takes_field,
3049+
}
30303050

30313051
def __setstate__(self, state):
30323052
"""
@@ -3048,7 +3068,7 @@ def __setstate__(self, state):
30483068
init=True,
30493069
inherited=False,
30503070
)
3051-
for name in ("converter", "takes_self")
3071+
for name in ("converter", "takes_self", "takes_field")
30523072
]
30533073

30543074
Converter = _add_hash(
@@ -3188,10 +3208,10 @@ def pipe(*converters):
31883208
.. versionadded:: 20.1.0
31893209
"""
31903210

3191-
def pipe_converter(val, inst):
3211+
def pipe_converter(val, inst, field):
31923212
for converter in converters:
31933213
if isinstance(converter, Converter):
3194-
val = converter(val, inst)
3214+
val = converter(val, inst, field)
31953215
else:
31963216
val = converter(val)
31973217

@@ -3212,4 +3232,4 @@ def pipe_converter(val, inst):
32123232
if rt:
32133233
pipe_converter.__annotations__["return"] = rt
32143234

3215-
return Converter(pipe_converter, takes_self=True)
3235+
return Converter(pipe_converter, takes_self=True, takes_field=True)

tests/test_converters.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,9 @@ def test_success(self):
116116
"""
117117
Succeeds if all wrapped converters succeed.
118118
"""
119-
c = pipe(str, Converter(to_bool, takes_self=False), bool)
119+
c = pipe(str, Converter(to_bool), bool)
120120

121-
assert True is c("True", None) is c(True, None)
121+
assert True is c("True", None, None) is c(True, None, None)
122122

123123
def test_fail(self):
124124
"""
@@ -128,11 +128,11 @@ def test_fail(self):
128128

129129
# First wrapped converter fails:
130130
with pytest.raises(ValueError):
131-
c(33, None)
131+
c(33, None, None)
132132

133133
# Last wrapped converter fails:
134134
with pytest.raises(ValueError):
135-
c("33", None)
135+
c("33", None, None)
136136

137137
def test_sugar(self):
138138
"""
@@ -153,7 +153,7 @@ def test_empty(self):
153153
"""
154154
o = object()
155155

156-
assert o is pipe()(o, None)
156+
assert o is pipe()(o, None, None)
157157

158158

159159
class TestToBool:

tests/test_make.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1284,7 +1284,7 @@ class TestConverter:
12841284
Tests for attribute conversion.
12851285
"""
12861286

1287-
def test_convert(self):
1287+
def test_converter(self):
12881288
"""
12891289
Return value of converter is used as the attribute's value.
12901290
"""
@@ -1315,6 +1315,27 @@ class C:
13151315

13161316
assert 84 == C(2).x
13171317

1318+
def test_converter_wrapped_takes_field(self):
1319+
"""
1320+
When wrapped and passed `takes_field`, the converter receives the field
1321+
definition -- and the return value is used as the field's value.
1322+
"""
1323+
1324+
def converter_with_field(v, field):
1325+
assert isinstance(field, attr.Attribute)
1326+
return v * field.metadata["x"]
1327+
1328+
@attr.define
1329+
class C:
1330+
x: int = attr.field(
1331+
converter=attr.Converter(
1332+
converter_with_field, takes_field=True
1333+
),
1334+
metadata={"x": 42},
1335+
)
1336+
1337+
assert 84 == C(2).x
1338+
13181339
@given(integers(), booleans())
13191340
def test_convert_property(self, val, init):
13201341
"""

0 commit comments

Comments
 (0)