Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Tweak tagged unions #443

Merged
merged 3 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@
([#405](https://github.com/python-attrs/cattrs/pull/405))
- The `omit` parameter of {py:func}`cattrs.override` is now of type `bool | None` (from `bool`).
`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.
- Converters can now be initialized with custom fallback hook factories for un/structuring.
- Converters can now be initialized with [custom fallback hook factories](https://catt.rs/en/latest/converters.html#fallback-hook-factories) for un/structuring.
([#331](https://github.com/python-attrs/cattrs/issues/311) [#441](https://github.com/python-attrs/cattrs/pull/441))
- Add support for `date` to preconfigured converters.
([#420](https://github.com/python-attrs/cattrs/pull/420))
- Add support for `datetime.date`s to the PyYAML preconfigured converter.
([#393](https://github.com/python-attrs/cattrs/issues/393))
- Fix {py:func}`format_exception() <cattrs.v.format_exception>` parameter working for recursive calls to {py:func}`transform_error <cattrs.transform_error>`.
([#389](https://github.com/python-attrs/cattrs/issues/389))
- [_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.
Expand All @@ -44,10 +48,8 @@
([#412](https://github.com/python-attrs/cattrs/issues/412))
- Fix certain cases of structuring `Annotated` types.
([#418](https://github.com/python-attrs/cattrs/issues/418))
- Add support for `date` to preconfigured converters.
([#420](https://github.com/python-attrs/cattrs/pull/420))
- Add support for `datetime.date`s to the PyYAML preconfigured converter.
([#393](https://github.com/python-attrs/cattrs/issues/393))
- Fix the [tagged union strategy](https://catt.rs/en/stable/strategies.html#tagged-unions-strategy) to work with `forbid_extra_keys`.
([#402](https://github.com/python-attrs/cattrs/issues/402) [#443](https://github.com/python-attrs/cattrs/pull/443))
- Use [PDM](https://pdm.fming.dev/latest/) instead of Poetry.
- _cattrs_ is now linted with [Ruff](https://beta.ruff.rs/docs/).
- Remove some unused lines in the unstructuring code.
Expand All @@ -68,7 +70,8 @@

## 23.1.0 (2023-05-30)

- Introduce the `tagged_union` strategy. ([#318](https://github.com/python-attrs/cattrs/pull/318) [#317](https://github.com/python-attrs/cattrs/issues/317))
- Introduce the [`tagged_union` strategy](https://catt.rs/en/stable/strategies.html#tagged-unions-strategy).
([#318](https://github.com/python-attrs/cattrs/pull/318) [#317](https://github.com/python-attrs/cattrs/issues/317))
- 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))
- Add support for [`typing.TypedDict` and `typing_extensions.TypedDict`](https://peps.python.org/pep-0589/).
([#296](https://github.com/python-attrs/cattrs/issues/296) [#364](https://github.com/python-attrs/cattrs/pull/364))
Expand Down
6 changes: 4 additions & 2 deletions src/cattrs/strategies/_unions.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ def unstructure_tagged_union(
def structure_tagged_union(
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
) -> union:
return _tag_to_cl[val[_tag_name]](val)
val = val.copy()
return _tag_to_cl[val.pop(_tag_name)](val)

else:

Expand All @@ -101,7 +102,8 @@ def structure_tagged_union(
_default=default,
) -> union:
if _tag_name in val:
return _tag_to_hook[val[_tag_name]](val)
val = val.copy()
return _tag_to_hook[val.pop(_tag_name)](val)
return _dh(val, _default)

converter.register_unstructure_hook(union, unstructure_tagged_union)
Expand Down
38 changes: 37 additions & 1 deletion tests/strategies/test_tagged_unions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from attrs import define

from cattrs import BaseConverter
from cattrs import BaseConverter, Converter
from cattrs.strategies import configure_tagged_union


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

# A.a should be coerced to an int.
assert converter.structure({"_type": "A", "a": "1"}, union) == A(1)


def test_forbid_extra_keys():
"""The strategy works when converters forbid extra keys."""

@define
class A:
pass

@define
class B:
pass

c = Converter(forbid_extra_keys=True)
configure_tagged_union(Union[A, B], c)

data = c.unstructure(A(), Union[A, B])
c.structure(data, Union[A, B])


def test_forbid_extra_keys_default():
"""The strategy works when converters forbid extra keys."""

@define
class A:
pass

@define
class B:
pass

c = Converter(forbid_extra_keys=True)
configure_tagged_union(Union[A, B], c, default=A)

data = c.unstructure(A(), Union[A, B])
c.structure(data, Union[A, B])