Skip to content

Commit

Permalink
converters: allow wrapping and passing self and fields (#1267)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people authored Jul 28, 2024
1 parent 0f045cd commit f5683b8
Show file tree
Hide file tree
Showing 12 changed files with 497 additions and 143 deletions.
3 changes: 3 additions & 0 deletions changelog.d/1267.change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
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.

Note that this is not supported by any type checker, yet.
21 changes: 21 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,27 @@ Validators can be both globally and locally disabled:
Converters
----------

.. autoclass:: attrs.Converter

For example:

.. doctest::

>>> def complicated(value, self_, field):
... return int(value) * self_.factor + field.metadata["offset"]
>>> @define
... class C:
... factor = 5 # not an *attrs* field
... x = field(
... metadata={"offset": 200},
... converter=attrs.Converter(
... complicated,
... takes_self=True, takes_field=True
... ))
>>> C("42")
C(x=410)


.. module:: attrs.converters

All objects from ``attrs.converters`` are also available from ``attr.converters`` (it's the same module in a different namespace).
Expand Down
18 changes: 18 additions & 0 deletions docs/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,24 @@ A converter will override an explicit type annotation or `type` argument.
{'return': None, 'x': <class 'str'>}
```

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:

```{doctest}
>>> def complicated(value, self_, field):
... return int(value) * self_.factor + field.metadata["offset"]
>>> @define
... class C:
... factor = 5 # not an *attrs* field
... x = field(
... metadata={"offset": 200},
... converter=attrs.Converter(
... complicated,
... takes_self=True, takes_field=True
... ))
>>> C("42")
C(x=410)
```


## Hooking Yourself Into Initialization

Expand Down
2 changes: 2 additions & 0 deletions src/attr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ._make import (
NOTHING,
Attribute,
Converter,
Factory,
attrib,
attrs,
Expand All @@ -39,6 +40,7 @@ class AttrsInstance(Protocol):
__all__ = [
"Attribute",
"AttrsInstance",
"Converter",
"Factory",
"NOTHING",
"asdict",
Expand Down
37 changes: 33 additions & 4 deletions src/attr/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,35 @@ else:
takes_self: bool = ...,
) -> _T: ...

In = TypeVar("In")
Out = TypeVar("Out")

class Converter(Generic[In, Out]):
@overload
def __init__(self, converter: Callable[[In], Out]) -> None: ...
@overload
def __init__(
self,
converter: Callable[[In, AttrsInstance, Attribute], Out],
*,
takes_self: Literal[True],
takes_field: Literal[True],
) -> None: ...
@overload
def __init__(
self,
converter: Callable[[In, Attribute], Out],
*,
takes_field: Literal[True],
) -> None: ...
@overload
def __init__(
self,
converter: Callable[[In, AttrsInstance], Out],
*,
takes_self: Literal[True],
) -> None: ...

class Attribute(Generic[_T]):
name: str
default: _T | None
Expand All @@ -110,7 +139,7 @@ class Attribute(Generic[_T]):
order: _EqOrderType
hash: bool | None
init: bool
converter: _ConverterType | None
converter: _ConverterType | Converter[Any, _T] | None
metadata: dict[Any, Any]
type: type[_T] | None
kw_only: bool
Expand Down Expand Up @@ -174,7 +203,7 @@ def attrib(
init: bool = ...,
metadata: Mapping[Any, Any] | None = ...,
type: type[_T] | None = ...,
converter: _ConverterType | None = ...,
converter: _ConverterType | Converter[Any, _T] | None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool = ...,
eq: _EqOrderType | None = ...,
Expand All @@ -194,7 +223,7 @@ def attrib(
init: bool = ...,
metadata: Mapping[Any, Any] | None = ...,
type: type[_T] | None = ...,
converter: _ConverterType | None = ...,
converter: _ConverterType | Converter[Any, _T] | None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool = ...,
eq: _EqOrderType | None = ...,
Expand All @@ -214,7 +243,7 @@ def attrib(
init: bool = ...,
metadata: Mapping[Any, Any] | None = ...,
type: object = ...,
converter: _ConverterType | None = ...,
converter: _ConverterType | Converter[Any, _T] | None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool = ...,
eq: _EqOrderType | None = ...,
Expand Down
Loading

0 comments on commit f5683b8

Please sign in to comment.