Skip to content

Commit 4df0c9e

Browse files
authored
Tweak tagged unions (#443)
* Tweak tagged unions * Tagged unions: Fix to not change input dict * Old style unions
1 parent 1352676 commit 4df0c9e

File tree

3 files changed

+50
-9
lines changed

3 files changed

+50
-9
lines changed

HISTORY.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@
1818
([#405](https://github.com/python-attrs/cattrs/pull/405))
1919
- The `omit` parameter of {py:func}`cattrs.override` is now of type `bool | None` (from `bool`).
2020
`None` is the new default and means to apply default _cattrs_ handling to the attribute, which is to omit the attribute if it's marked as `init=False`, and keep it otherwise.
21-
- Converters can now be initialized with custom fallback hook factories for un/structuring.
21+
- Converters can now be initialized with [custom fallback hook factories](https://catt.rs/en/latest/converters.html#fallback-hook-factories) for un/structuring.
2222
([#331](https://github.com/python-attrs/cattrs/issues/311) [#441](https://github.com/python-attrs/cattrs/pull/441))
23+
- Add support for `date` to preconfigured converters.
24+
([#420](https://github.com/python-attrs/cattrs/pull/420))
25+
- Add support for `datetime.date`s to the PyYAML preconfigured converter.
26+
([#393](https://github.com/python-attrs/cattrs/issues/393))
2327
- Fix {py:func}`format_exception() <cattrs.v.format_exception>` parameter working for recursive calls to {py:func}`transform_error <cattrs.transform_error>`.
2428
([#389](https://github.com/python-attrs/cattrs/issues/389))
2529
- [_attrs_ aliases](https://www.attrs.org/en/stable/init.html#private-attributes-and-aliases) are now supported, although aliased fields still map to their attribute name instead of their alias by default when un/structuring.
@@ -44,10 +48,8 @@
4448
([#412](https://github.com/python-attrs/cattrs/issues/412))
4549
- Fix certain cases of structuring `Annotated` types.
4650
([#418](https://github.com/python-attrs/cattrs/issues/418))
47-
- Add support for `date` to preconfigured converters.
48-
([#420](https://github.com/python-attrs/cattrs/pull/420))
49-
- Add support for `datetime.date`s to the PyYAML preconfigured converter.
50-
([#393](https://github.com/python-attrs/cattrs/issues/393))
51+
- Fix the [tagged union strategy](https://catt.rs/en/stable/strategies.html#tagged-unions-strategy) to work with `forbid_extra_keys`.
52+
([#402](https://github.com/python-attrs/cattrs/issues/402) [#443](https://github.com/python-attrs/cattrs/pull/443))
5153
- Use [PDM](https://pdm.fming.dev/latest/) instead of Poetry.
5254
- _cattrs_ is now linted with [Ruff](https://beta.ruff.rs/docs/).
5355
- Remove some unused lines in the unstructuring code.
@@ -68,7 +70,8 @@
6870

6971
## 23.1.0 (2023-05-30)
7072

71-
- Introduce the `tagged_union` strategy. ([#318](https://github.com/python-attrs/cattrs/pull/318) [#317](https://github.com/python-attrs/cattrs/issues/317))
73+
- Introduce the [`tagged_union` strategy](https://catt.rs/en/stable/strategies.html#tagged-unions-strategy).
74+
([#318](https://github.com/python-attrs/cattrs/pull/318) [#317](https://github.com/python-attrs/cattrs/issues/317))
7275
- Introduce the `cattrs.transform_error` helper function for formatting validation exceptions. ([258](https://github.com/python-attrs/cattrs/issues/258) [342](https://github.com/python-attrs/cattrs/pull/342))
7376
- Add support for [`typing.TypedDict` and `typing_extensions.TypedDict`](https://peps.python.org/pep-0589/).
7477
([#296](https://github.com/python-attrs/cattrs/issues/296) [#364](https://github.com/python-attrs/cattrs/pull/364))

src/cattrs/strategies/_unions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ def unstructure_tagged_union(
8888
def structure_tagged_union(
8989
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
9090
) -> union:
91-
return _tag_to_cl[val[_tag_name]](val)
91+
val = val.copy()
92+
return _tag_to_cl[val.pop(_tag_name)](val)
9293

9394
else:
9495

@@ -101,7 +102,8 @@ def structure_tagged_union(
101102
_default=default,
102103
) -> union:
103104
if _tag_name in val:
104-
return _tag_to_hook[val[_tag_name]](val)
105+
val = val.copy()
106+
return _tag_to_hook[val.pop(_tag_name)](val)
105107
return _dh(val, _default)
106108

107109
converter.register_unstructure_hook(union, unstructure_tagged_union)

tests/strategies/test_tagged_unions.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from attrs import define
44

5-
from cattrs import BaseConverter
5+
from cattrs import BaseConverter, Converter
66
from cattrs.strategies import configure_tagged_union
77

88

@@ -102,3 +102,39 @@ def test_default_member_validation(converter: BaseConverter) -> None:
102102

103103
# A.a should be coerced to an int.
104104
assert converter.structure({"_type": "A", "a": "1"}, union) == A(1)
105+
106+
107+
def test_forbid_extra_keys():
108+
"""The strategy works when converters forbid extra keys."""
109+
110+
@define
111+
class A:
112+
pass
113+
114+
@define
115+
class B:
116+
pass
117+
118+
c = Converter(forbid_extra_keys=True)
119+
configure_tagged_union(Union[A, B], c)
120+
121+
data = c.unstructure(A(), Union[A, B])
122+
c.structure(data, Union[A, B])
123+
124+
125+
def test_forbid_extra_keys_default():
126+
"""The strategy works when converters forbid extra keys."""
127+
128+
@define
129+
class A:
130+
pass
131+
132+
@define
133+
class B:
134+
pass
135+
136+
c = Converter(forbid_extra_keys=True)
137+
configure_tagged_union(Union[A, B], c, default=A)
138+
139+
data = c.unstructure(A(), Union[A, B])
140+
c.structure(data, Union[A, B])

0 commit comments

Comments
 (0)