Skip to content

Commit f5683b8

Browse files
hynekTinchepre-commit-ci[bot]
authored
converters: allow wrapping and passing self and fields (#1267)
* converters: allow wrapping & takes_self * Add takes field * Add news fragment * Refactor name and call creation * Make argument names more idiomatic * Add missing hints * Make 3-arg converters zero-overhead at runtime * Remove unnecessary changes * More idiomatic name * Explain method * Make pickle test more meaningful * Add unit tests for fmt_converter_call * Check our eq works too * Convert Converter docstring to Napoleon * Fix rebase fubar * Add ~types~ * Comment out failing mypy tests for now * Fix mypy tests * Add warning * Add narrative docs * Fix converter overloads, add tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Tin Tvrtković <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 0f045cd commit f5683b8

File tree

12 files changed

+497
-143
lines changed

12 files changed

+497
-143
lines changed

changelog.d/1267.change.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
It is now possible to wrap a converter into an `attrs.Converter` and get the current instance and/or the current field definition passed into the converter callable.
2+
3+
Note that this is not supported by any type checker, yet.

docs/api.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,27 @@ Validators can be both globally and locally disabled:
630630
Converters
631631
----------
632632

633+
.. autoclass:: attrs.Converter
634+
635+
For example:
636+
637+
.. doctest::
638+
639+
>>> def complicated(value, self_, field):
640+
... return int(value) * self_.factor + field.metadata["offset"]
641+
>>> @define
642+
... class C:
643+
... factor = 5 # not an *attrs* field
644+
... x = field(
645+
... metadata={"offset": 200},
646+
... converter=attrs.Converter(
647+
... complicated,
648+
... takes_self=True, takes_field=True
649+
... ))
650+
>>> C("42")
651+
C(x=410)
652+
653+
633654
.. module:: attrs.converters
634655

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

docs/init.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,24 @@ A converter will override an explicit type annotation or `type` argument.
361361
{'return': None, 'x': <class 'str'>}
362362
```
363363

364+
If you need more control over the conversion process, you can wrap the converter with a {class}`attrs.Converter` and ask for the instance and/or the field that are being initialized:
365+
366+
```{doctest}
367+
>>> def complicated(value, self_, field):
368+
... return int(value) * self_.factor + field.metadata["offset"]
369+
>>> @define
370+
... class C:
371+
... factor = 5 # not an *attrs* field
372+
... x = field(
373+
... metadata={"offset": 200},
374+
... converter=attrs.Converter(
375+
... complicated,
376+
... takes_self=True, takes_field=True
377+
... ))
378+
>>> C("42")
379+
C(x=410)
380+
```
381+
364382

365383
## Hooking Yourself Into Initialization
366384

src/attr/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ._make import (
1616
NOTHING,
1717
Attribute,
18+
Converter,
1819
Factory,
1920
attrib,
2021
attrs,
@@ -39,6 +40,7 @@ class AttrsInstance(Protocol):
3940
__all__ = [
4041
"Attribute",
4142
"AttrsInstance",
43+
"Converter",
4244
"Factory",
4345
"NOTHING",
4446
"asdict",

src/attr/__init__.pyi

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,35 @@ else:
100100
takes_self: bool = ...,
101101
) -> _T: ...
102102

103+
In = TypeVar("In")
104+
Out = TypeVar("Out")
105+
106+
class Converter(Generic[In, Out]):
107+
@overload
108+
def __init__(self, converter: Callable[[In], Out]) -> None: ...
109+
@overload
110+
def __init__(
111+
self,
112+
converter: Callable[[In, AttrsInstance, Attribute], Out],
113+
*,
114+
takes_self: Literal[True],
115+
takes_field: Literal[True],
116+
) -> None: ...
117+
@overload
118+
def __init__(
119+
self,
120+
converter: Callable[[In, Attribute], Out],
121+
*,
122+
takes_field: Literal[True],
123+
) -> None: ...
124+
@overload
125+
def __init__(
126+
self,
127+
converter: Callable[[In, AttrsInstance], Out],
128+
*,
129+
takes_self: Literal[True],
130+
) -> None: ...
131+
103132
class Attribute(Generic[_T]):
104133
name: str
105134
default: _T | None
@@ -110,7 +139,7 @@ class Attribute(Generic[_T]):
110139
order: _EqOrderType
111140
hash: bool | None
112141
init: bool
113-
converter: _ConverterType | None
142+
converter: _ConverterType | Converter[Any, _T] | None
114143
metadata: dict[Any, Any]
115144
type: type[_T] | None
116145
kw_only: bool
@@ -174,7 +203,7 @@ def attrib(
174203
init: bool = ...,
175204
metadata: Mapping[Any, Any] | None = ...,
176205
type: type[_T] | None = ...,
177-
converter: _ConverterType | None = ...,
206+
converter: _ConverterType | Converter[Any, _T] | None = ...,
178207
factory: Callable[[], _T] | None = ...,
179208
kw_only: bool = ...,
180209
eq: _EqOrderType | None = ...,
@@ -194,7 +223,7 @@ def attrib(
194223
init: bool = ...,
195224
metadata: Mapping[Any, Any] | None = ...,
196225
type: type[_T] | None = ...,
197-
converter: _ConverterType | None = ...,
226+
converter: _ConverterType | Converter[Any, _T] | None = ...,
198227
factory: Callable[[], _T] | None = ...,
199228
kw_only: bool = ...,
200229
eq: _EqOrderType | None = ...,
@@ -214,7 +243,7 @@ def attrib(
214243
init: bool = ...,
215244
metadata: Mapping[Any, Any] | None = ...,
216245
type: object = ...,
217-
converter: _ConverterType | None = ...,
246+
converter: _ConverterType | Converter[Any, _T] | None = ...,
218247
factory: Callable[[], _T] | None = ...,
219248
kw_only: bool = ...,
220249
eq: _EqOrderType | None = ...,

0 commit comments

Comments
 (0)