diff --git a/HISTORY.md b/HISTORY.md index 7454dbe7..82c963fc 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -22,14 +22,17 @@ Our backwards-compatibility policy can be found [here](https://github.com/python - [`typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) is now supported in _attrs_ classes, dataclasses, TypedDicts and the dict NamedTuple factories. See [`typing.Self`](https://catt.rs/en/latest/defaulthooks.html#typing-self) for details. ([#299](https://github.com/python-attrs/cattrs/issues/299) [#627](https://github.com/python-attrs/cattrs/pull/627)) -- PEP 695 type aliases can now be used with {meth}`Converter.register_structure_hook` and {meth}`Converter.register_unstructure_hook`. - Previously, they required the use of {meth}`Converter.register_structure_hook_func` (which is still supported). +- PEP 695 type aliases can now be used with {meth}`BaseConverter.register_structure_hook` and {meth}`BaseConverter.register_unstructure_hook`. + Previously, they required the use of {meth}`BaseConverter.register_structure_hook_func` (which is still supported). + ([#647](https://github.com/python-attrs/cattrs/pull/647)) - Expose {func}`cattrs.cols.mapping_unstructure_factory` through {mod}`cattrs.cols`. - Some `defaultdicts` are now [supported by default](https://catt.rs/en/latest/defaulthooks.html#defaultdicts), and {func}`cattrs.cols.is_defaultdict` and {func}`cattrs.cols.defaultdict_structure_factory` are exposed through {mod}`cattrs.cols`. ([#519](https://github.com/python-attrs/cattrs/issues/519) [#588](https://github.com/python-attrs/cattrs/pull/588)) - Generic PEP 695 type aliases are now supported. ([#611](https://github.com/python-attrs/cattrs/issues/611) [#618](https://github.com/python-attrs/cattrs/pull/618)) +- The [tagged union strategy](https://catt.rs/en/stable/strategies.html#tagged-unions-strategy) now also supports type aliases of unions. + ([#649](https://github.com/python-attrs/cattrs/pull/649)) - {meth}`Converter.copy` and {meth}`BaseConverter.copy` are correctly annotated as returning `Self`. ([#644](https://github.com/python-attrs/cattrs/pull/644)) - Many preconf converters (_bson_, stdlib JSON, _cbor2_, _msgpack_, _msgspec_, _orjson_, _ujson_) skip unstructuring `int` and `str` enums, diff --git a/docs/strategies.md b/docs/strategies.md index e4ba639a..56b7ddde 100644 --- a/docs/strategies.md +++ b/docs/strategies.md @@ -67,6 +67,10 @@ This also means union members can be reused in multiple unions easily. {'a': 1} ``` +```{versionchanged} 25.1.0 +The strategy can also be called with a type alias of a union. +``` + ### Real-life Case Study The Apple App Store supports [server callbacks](https://developer.apple.com/documentation/appstoreservernotifications), by which Apple sends a JSON payload to a URL of your choice. diff --git a/src/cattrs/strategies/_unions.py b/src/cattrs/strategies/_unions.py index 3ef4ebdf..57e132d0 100644 --- a/src/cattrs/strategies/_unions.py +++ b/src/cattrs/strategies/_unions.py @@ -3,8 +3,9 @@ from attrs import NOTHING, NothingType -from cattrs import BaseConverter -from cattrs._compat import get_newtype_base, is_literal, is_subclass, is_union_type +from .. import BaseConverter +from .._compat import get_newtype_base, is_literal, is_subclass, is_union_type +from ..typealiases import is_type_alias __all__ = [ "configure_tagged_union", @@ -26,8 +27,8 @@ def configure_tagged_union( default: Union[type, NothingType] = NOTHING, ) -> None: """ - Configure the converter so that `union` (which should be a union) is - un/structured with the help of an additional piece of data in the + Configure the converter so that `union` (which should be a union, or a type alias + of one) is un/structured with the help of an additional piece of data in the unstructured payload, the tag. :param converter: The converter to apply the strategy to. @@ -44,7 +45,12 @@ def configure_tagged_union( un/structuring base strategy. .. versionadded:: 23.1.0 + + .. versionchanged:: 25.1 + Type aliases of unions are now also supported. """ + if is_type_alias(union): + union = union.__value__ args = union.__args__ tag_to_hook = {} exact_cl_unstruct_hooks = {} diff --git a/tests/strategies/test_tagged_unions_695.py b/tests/strategies/test_tagged_unions_695.py new file mode 100644 index 00000000..34d6fcc8 --- /dev/null +++ b/tests/strategies/test_tagged_unions_695.py @@ -0,0 +1,21 @@ +import pytest + +from cattrs import BaseConverter +from cattrs.strategies import configure_tagged_union + +from .._compat import is_py312_plus +from .test_tagged_unions import A, B + + +@pytest.mark.skipif(not is_py312_plus, reason="New type alias syntax") +def test_type_alias(converter: BaseConverter): + """Type aliases to unions also work.""" + type AOrB = A | B + + configure_tagged_union(AOrB, converter) + + assert converter.unstructure(A(1), AOrB) == {"_type": "A", "a": 1} + assert converter.unstructure(B("1"), AOrB) == {"_type": "B", "a": "1"} + + assert converter.structure({"_type": "A", "a": 1}, AOrB) == A(1) + assert converter.structure({"_type": "B", "a": 1}, AOrB) == B("1")