Skip to content

Commit 69a3ed9

Browse files
committed
feat: strategy for using class methods
1 parent 5f5c116 commit 69a3ed9

File tree

4 files changed

+147
-2
lines changed

4 files changed

+147
-2
lines changed

docs/strategies.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ Without the application of the strategy, in both unstructure and structure opera
172172

173173
```{note}
174174
The handling of subclasses is an opt-in feature for two main reasons:
175-
- Performance. While small and probably negligeable in most cases the subclass handling incurs more function calls and has a performance impact.
175+
- Performance. While small and probably negligeable in most cases the subclass handling incurs more function calls and has a performance impact.
176176
- Customization. The specific handling of subclasses can be different from one situation to the other. In particular there is not apparent universal good defaults for disambiguating the union type. Consequently the decision is left to the user.
177177
```
178178

@@ -258,3 +258,46 @@ Child(a=1, b='foo')
258258
```{versionadded} 23.1.0
259259
260260
```
261+
262+
263+
264+
### Using Class-Specific Structure and Unstructure Methods
265+
266+
_Found at {py:func}`cattrs.strategies.use_class_methods`._
267+
268+
The following strategy can be applied for both structuring and unstructuring (also simultaneously).
269+
270+
If a class requires special handling for (un)structuring, you can add a dedicated (un)structuring
271+
method:
272+
273+
```{doctest} class_methods
274+
275+
>>> from attrs import define
276+
>>> from cattrs import Converter
277+
>>> from cattrs.strategies import use_class_methods
278+
279+
>>> @define
280+
... class MyClass:
281+
... a: int
282+
...
283+
... @classmethod
284+
... def _structure(cls, data: dict):
285+
... return cls(data["b"] + 1) # expecting "b", not "a"
286+
...
287+
... def _unstructure(self):
288+
... return {"c": self.a - 1} # unstructuring as "c", not "a"
289+
290+
>>> converter = Converter()
291+
>>> use_class_methods(converter, "_structure", "_unstructure")
292+
>>> print(converter.structure({"b": 42}, MyClass))
293+
MyClass(a=43)
294+
>>> print(converter.unstructure(MyClass(42)))
295+
{'c': 41}
296+
```
297+
298+
Any class without a `_structure` or `_unstructure` method will use the default strategy for
299+
structuring or unstructuring, respectively. Feel free to use other names.
300+
301+
```{versionadded} 23.2.0
302+
303+
```

src/cattrs/strategies/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""High level strategies for converters."""
2+
from ._class_methods import use_class_methods
23
from ._subclasses import include_subclasses
34
from ._unions import configure_tagged_union
45

5-
__all__ = ["configure_tagged_union", "include_subclasses"]
6+
__all__ = ["configure_tagged_union", "include_subclasses", "use_class_methods"]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Strategy for using class-specific (un)structuring methods."""
2+
from typing import Optional
3+
4+
from cattrs import BaseConverter
5+
6+
7+
def use_class_methods(
8+
converter: BaseConverter,
9+
structure_method_name: Optional[str] = None,
10+
unstructure_method_name: Optional[str] = None,
11+
) -> None:
12+
"""
13+
Configure the converter such that dedicated methods are used for (un)structuring
14+
the instance of a class if such methods are available. The default (un)structuring
15+
will be applied if such an (un)structuring methods cannot be found.
16+
17+
:param converter: The `Converter` on which this strategy is applied. You can use
18+
:class:`cattrs.BaseConverter` or any other derived class.
19+
:param structure_method_name: Optional string with the name of the class method
20+
which should be used for structuring. If not provided, no class method will be
21+
used for structuring.
22+
:param unstructure_method_name: Optional string with the name of the class method
23+
which should be used for unstructuring. If not provided, no class method will
24+
be used for unstructuring.
25+
26+
.. versionadded:: 23.2.0
27+
"""
28+
if structure_method_name:
29+
converter.register_structure_hook_func(
30+
lambda t: hasattr(t, structure_method_name),
31+
lambda v, t: getattr(t, structure_method_name)(v),
32+
)
33+
34+
if unstructure_method_name:
35+
converter.register_unstructure_hook_func(
36+
lambda t: hasattr(t, unstructure_method_name),
37+
lambda v: getattr(v, unstructure_method_name)(),
38+
)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import itertools
2+
3+
import pytest
4+
from attrs import define
5+
6+
from cattrs import BaseConverter
7+
from cattrs.strategies import use_class_methods
8+
9+
10+
@define
11+
class Base:
12+
a: int
13+
14+
15+
class Structure(Base):
16+
@classmethod
17+
def _structure(cls, data: dict):
18+
return cls(data["b"]) # expecting "b", not "a"
19+
20+
21+
class Unstructure(Base):
22+
def _unstructure(self):
23+
return {"c": self.a} # unstructuring as "c", not "a"
24+
25+
26+
class Both(Structure, Unstructure):
27+
pass
28+
29+
30+
@pytest.fixture
31+
def get_converter(converter: BaseConverter):
32+
def aux(structure: str, unstructure: str) -> BaseConverter:
33+
use_class_methods(converter, structure, unstructure)
34+
return converter
35+
36+
return aux
37+
38+
39+
@pytest.mark.parametrize(
40+
"cls,structure_method,unstructure_method",
41+
itertools.product(
42+
[Structure, Unstructure, Both],
43+
["_structure", "_undefined", None],
44+
["_unstructure", "_undefined", None],
45+
),
46+
)
47+
def test(get_converter, structure_method, unstructure_method, cls) -> None:
48+
converter = get_converter(structure_method, unstructure_method)
49+
50+
assert converter.structure(
51+
{
52+
"b"
53+
if structure_method == "_structure" and hasattr(cls, "_structure")
54+
else "a": 42
55+
},
56+
cls,
57+
) == cls(42)
58+
59+
assert converter.unstructure(cls(42)) == {
60+
"c"
61+
if unstructure_method == "_unstructure" and hasattr(cls, "_unstructure")
62+
else "a": 42
63+
}

0 commit comments

Comments
 (0)