From 527291fc0c8885e4eac415223bab03e90c9f403e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Tue, 7 Jan 2025 10:53:19 +0100 Subject: [PATCH] type aliases: support generics (#618) --- HISTORY.md | 4 +- pdm.lock | 89 +++++++++++++++++------------------- src/cattrs/_compat.py | 26 ----------- src/cattrs/converters.py | 17 +++---- src/cattrs/gen/typeddicts.py | 3 +- src/cattrs/typealiases.py | 57 +++++++++++++++++++++++ tests/test_generics_695.py | 16 +++++++ tests/test_v.py | 3 +- tests/typeddicts.py | 10 +--- 9 files changed, 130 insertions(+), 95 deletions(-) create mode 100644 src/cattrs/typealiases.py diff --git a/HISTORY.md b/HISTORY.md index bf6b8ac3..8b3239f2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -10,7 +10,7 @@ The third number is for emergencies when we need to start branches for older rel Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md). -## 24.2.0 (UNRELEASED) +## 25.1.0 (UNRELEASED) - **Potentially breaking**: The converters raise {class}`StructureHandlerNotFoundError` more eagerly (on hook creation, instead of on hook use). This helps surfacing problems with missing hooks sooner. @@ -22,6 +22,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python - 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)) - Many preconf converters (_bson_, stdlib JSON, _cbor2_, _msgpack_, _msgspec_, _orjson_, _ujson_) skip unstructuring `int` and `str` enums, leaving them to the underlying libraries to handle with greater efficiency. ([#598](https://github.com/python-attrs/cattrs/pull/598)) diff --git a/pdm.lock b/pdm.lock index 2250cdaa..809b6d39 100644 --- a/pdm.lock +++ b/pdm.lock @@ -413,54 +413,51 @@ files = [ [[package]] name = "immutables" -version = "0.20" +version = "0.21" requires_python = ">=3.8.0" summary = "Immutable Collections" -dependencies = [ - "typing-extensions>=3.7.4.3; python_version < \"3.8\"", -] -files = [ - {file = "immutables-0.20-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dea0ae4d7f31b145c18c16badeebc2f039d09411be4a8febb86e1244cf7f1ce0"}, - {file = "immutables-0.20-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2dd0dcef2f8d4523d34dbe1d2b7804b3d2a51fddbd104aad13f506a838a2ea15"}, - {file = "immutables-0.20-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:393dde58ffd6b4c089ffdf4cef5fe73dad37ce4681acffade5f5d5935ec23c93"}, - {file = "immutables-0.20-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1214b5a175df783662b7de94b4a82db55cc0ee206dd072fa9e279fb8895d8df"}, - {file = "immutables-0.20-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2761e3dc2a6406943ce77b3505e9b3c1187846de65d7247548dc7edaa202fcba"}, - {file = "immutables-0.20-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2bcea81e7516bd823b4ed16f4f794531097888675be13e833b1cc946370d5237"}, - {file = "immutables-0.20-cp310-cp310-win32.whl", hash = "sha256:d828e7580f1fa203ddeab0b5e91f44bf95706e7f283ca9fbbcf0ae08f63d3084"}, - {file = "immutables-0.20-cp310-cp310-win_amd64.whl", hash = "sha256:380e2957ba3d63422b2f3fbbff0547c7bbe6479d611d3635c6411005a4264525"}, - {file = "immutables-0.20-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:532be32c7a25dae6cade28825c76d3004cf4d166a0bfacf04bda16056d59ba26"}, - {file = "immutables-0.20-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5302ce9c7827f8300f3dc34a695abb71e4a32bab09e65e5ad6e454785383347f"}, - {file = "immutables-0.20-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b51aec54b571ae466113509d4dc79a2808dc2ae9263b71fd6b37778cb49eb292"}, - {file = "immutables-0.20-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f56aea56e597ecf6631f24a4e26007b6a5f4fe30278b96eb90bc1f60506164"}, - {file = "immutables-0.20-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:085ac48ee3eef7baf070f181cae574489bbf65930a83ec5bbd65c9940d625db3"}, - {file = "immutables-0.20-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f063f53b5c0e8f541ae381f1d828f3d05bbed766a2d6c817f9218b8b37a4cb66"}, - {file = "immutables-0.20-cp311-cp311-win32.whl", hash = "sha256:b0436cc831b47e26bef637bcf143cf0273e49946cfb7c28c44486d70513a3080"}, - {file = "immutables-0.20-cp311-cp311-win_amd64.whl", hash = "sha256:5bb32aee1ea16fbb90f58f8bd96016bca87aba0a8e574e5fa218d0d83b142851"}, - {file = "immutables-0.20-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ba726b7a3a696b9d4b122fa2c956bc68e866f3df1b92765060c88c64410ff82"}, - {file = "immutables-0.20-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5a88adf1dcc9d8ab07dba5e74deefcd5b5e38bc677815cbf9365dc43b69f1f08"}, - {file = "immutables-0.20-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1009a4e00e2e69a9b40c2f1272795f5a06ad72c9bf4638594d518e9cbd7a721a"}, - {file = "immutables-0.20-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96899994842c37cf4b9d6d2bedf685aae7810bd73f1538f8cba5426e2d65cb85"}, - {file = "immutables-0.20-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a606410b2ccb6ae339c3f26cccc9a92bcb16dc06f935d51edfd8ca68cf687e50"}, - {file = "immutables-0.20-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8e82754f72823085643a2c0e6a4c489b806613e94af205825fa81df2ba147a0"}, - {file = "immutables-0.20-cp312-cp312-win32.whl", hash = "sha256:525fb361bd7edc8a891633928d549713af8090c79c25af5cc06eb90b48cb3c64"}, - {file = "immutables-0.20-cp312-cp312-win_amd64.whl", hash = "sha256:a82afc3945e9ceb9bcd416dc4ed9b72f92760c42787e26de50610a8b81d48120"}, - {file = "immutables-0.20-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f17f25f21e82a1c349a61191cfb13e442a348b880b74cb01b00e0d1e848b63f4"}, - {file = "immutables-0.20-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:65954eb861c61af48debb1507518d45ae7d594b4fba7282785a70b48c5f51f9b"}, - {file = "immutables-0.20-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62f8a7a22939278127b7a206d05679b268b9cf665437125625348e902617cbad"}, - {file = "immutables-0.20-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac86f4372f4cfaa00206c12472fd3a78753092279e0552b7e1880944d71b04fe"}, - {file = "immutables-0.20-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e771198edc11a9e02ffa693911b3918c6cde0b64ad2e6672b076dbe005557ad8"}, - {file = "immutables-0.20-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc739fc07cff5df2e4f31addbd48660b5ac0da56e9f719f8bb45da8ddd632c63"}, - {file = "immutables-0.20-cp38-cp38-win32.whl", hash = "sha256:c086ccb44d9d3824b9bf816365d10b1b82837efc7119f8bab56bd7a27ed805a9"}, - {file = "immutables-0.20-cp38-cp38-win_amd64.whl", hash = "sha256:9cd2ee9c10bf00be3c94eb51854bc0b761326bd0a7ea0dad4272a3f182269ae6"}, - {file = "immutables-0.20-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d4f78cb748261f852953620ed991de74972446fd484ec69377a41e2f1a1beb75"}, - {file = "immutables-0.20-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6449186ea91b7c17ec8e7bd9bf059858298b1db5c053f5d27de8eba077578ce"}, - {file = "immutables-0.20-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85dd9765b068f7beb297553fddfcf7f904bd58a184c520830a106a58f0c9bfb4"}, - {file = "immutables-0.20-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f349a7e0327b92dcefb863e49ace086f2f26e6689a4e022c98720c6e9696e763"}, - {file = "immutables-0.20-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e3a5462f6d3549bbf7d02ce929fb0cb6df9539445f0589105de4e8b99b906e69"}, - {file = "immutables-0.20-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc51a01a64a6d2cd7db210a49ad010c2ac2e9e026745f23fd31e0784096dcfff"}, - {file = "immutables-0.20-cp39-cp39-win32.whl", hash = "sha256:83794712f0507416f2818edc63f84305358b8656a93e5b9e2ab056d9803c7507"}, - {file = "immutables-0.20-cp39-cp39-win_amd64.whl", hash = "sha256:2837b1078abc66d9f009bee9085cf62515d5516af9a5c9ea2751847e16efd236"}, - {file = "immutables-0.20.tar.gz", hash = "sha256:1d2f83e6a6a8455466cd97b9a90e2b4f7864648616dfa6b19d18f49badac3876"}, +files = [ + {file = "immutables-0.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:14cb09d4f4577ad9ab8770a340dc2158e0a5ab5775cb34c75960167a31104212"}, + {file = "immutables-0.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:22ba593f95044ac60d2af463f3dc86cd0e223f8c51df85dff65d663d93e19f51"}, + {file = "immutables-0.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25afc81a7bcf26c8364f85e52a14e0095344343e79493c73b0e9a765310a0bed"}, + {file = "immutables-0.21-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac6e2868567289f88c6810f296940c328a1d38c9abc841eed04963102a27d12"}, + {file = "immutables-0.21-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ba8bca21a1d034f4577ede1e9553a681dd01199c06b563f1a8316f2623b64985"}, + {file = "immutables-0.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:39337bfb42f83dd787a81e2d00e90efa17c4a39a9cf1210b8a50dafe32438aae"}, + {file = "immutables-0.21-cp310-cp310-win32.whl", hash = "sha256:b24aa98f6cdae4ba15baf3aa00e84223bafcd0d3fd7f0443474527ec951845e1"}, + {file = "immutables-0.21-cp310-cp310-win_amd64.whl", hash = "sha256:715f8e5f8e1c35f036f9ac62eaf8b672eec1cdc2b4f9b73864cc64eccc76661c"}, + {file = "immutables-0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5d780c38067047911a2e06a86ba063ba0055618ab5573c8198ef3f368e321303"}, + {file = "immutables-0.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9aab9d0f0016f6e0bfe7e4a4cb831ef20063da6468b1bbc71d06ef285781ee9e"}, + {file = "immutables-0.21-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ff83390b05d3372acb9a0c928f6cc20c78e74ca20ed88eb941f84a63b65e444"}, + {file = "immutables-0.21-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01497713e71509c4481ffccdbe3a47b94969345f4e92f814d6626f7c0a4c304"}, + {file = "immutables-0.21-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc7844c9fbb5bece5bfdf2bf8ea74d308f42f40b0665fd25c58abf56d7db024a"}, + {file = "immutables-0.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:984106fa4345efd9f96de22e9949fc97bac8598bdebee03c20b2497a88bff3b7"}, + {file = "immutables-0.21-cp311-cp311-win32.whl", hash = "sha256:1bdb5200518518601377e4877d5034e7c535e9ea8a9d601ed8b0eedef0c7becd"}, + {file = "immutables-0.21-cp311-cp311-win_amd64.whl", hash = "sha256:dd00c34f431c54c95e7b84bfdbdeacb4f039a6a24eb0c1f7aa4b168bb9a6ad0a"}, + {file = "immutables-0.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1ed262094b755903122c3c3a83ad0e0d5c3ab7887cda12b2fe878769d1ee0d"}, + {file = "immutables-0.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce604f81d9d8f26e60b52ebcb56bb5c0462c8ea50fb17868487d15f048a2f13e"}, + {file = "immutables-0.21-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b48b116aaca4500398058b5a87814857a60c4cb09417fecc12d7da0f5639b73d"}, + {file = "immutables-0.21-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dad7c0c74b285cc0e555ec0e97acbdc6f1862fcd16b99abd612df3243732e741"}, + {file = "immutables-0.21-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e44346e2221a5a676c880ca8e0e6429fa24d1a4ae562573f5c04d7f2e759b030"}, + {file = "immutables-0.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b10139b529a460e53fe8be699ebd848c54c8a33ebe67763bcfcc809a475a26f"}, + {file = "immutables-0.21-cp312-cp312-win32.whl", hash = "sha256:fc512d808662614feb17d2d92e98f611d69669a98c7af15910acf1dc72737038"}, + {file = "immutables-0.21-cp312-cp312-win_amd64.whl", hash = "sha256:461dcb0f58a131045155e52a2c43de6ec2fe5ba19bdced6858a3abb63cee5111"}, + {file = "immutables-0.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:79674b51aa8dd983f9ac55f7f67b433b1df84a6b4f28ab860588389a5659485b"}, + {file = "immutables-0.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93c8350f8f7d0d9693f708229d9d0578e6f3b785ce6da4bced1da97137aacfad"}, + {file = "immutables-0.21-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:583d2a63e444ce1538cc2bda56ae1f4a1a11473dbc0377c82b516bc7eec3b81e"}, + {file = "immutables-0.21-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b274a52da9b106db55eceb93fc1aea858c4e6f4740189e3548e38613eafc2021"}, + {file = "immutables-0.21-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:338bede057250b33716a3e4892e15df0bf5a5ddbf1d67ead996b3e680b49ef9e"}, + {file = "immutables-0.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8781c89583b68f604cf30f0978b722165824c3075888639fde771bf1a3e12dc0"}, + {file = "immutables-0.21-cp313-cp313-win32.whl", hash = "sha256:e97ea83befad873712f283c0cccd630f70cba753e207b4868af28d5b85e9dc54"}, + {file = "immutables-0.21-cp313-cp313-win_amd64.whl", hash = "sha256:cfcb23bd898f5a4ef88692b42c51f52ca7373a35ba4dcc215060a668639eb5da"}, + {file = "immutables-0.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e2aadf3bdd90daa0e8cb9c3cde4070e1021036e3b57f571a007ce24f323e47a9"}, + {file = "immutables-0.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5f8f507731d4d15e0c579aa77d8482471f988dc0f451e4bf3853ec36ccd42627"}, + {file = "immutables-0.21-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb9a378a4480381d7d3d63b0d201cf610eae0bf70e26a9306e3e631c9bd64010"}, + {file = "immutables-0.21-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7b5920bbfcaf038894c8ce4ed2eff0b31c3559810a61806db751be8ab4d703"}, + {file = "immutables-0.21-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8b90702d1fe313e8273ae7abb46fc0f0a87b47c1c9a83aed9a161301146e655c"}, + {file = "immutables-0.21-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:71cbbc6fbe7e7321648047ff9273f4605f8bd5ce456841a65ef151080e9d3481"}, + {file = "immutables-0.21-cp39-cp39-win32.whl", hash = "sha256:c44f286c47dc0d4d7b5bf19fbe975e6d57c56d2878cea413e1ec7a4bfffb2727"}, + {file = "immutables-0.21-cp39-cp39-win_amd64.whl", hash = "sha256:cf15314c39484b8947a4e20c3526021272510592fb2807b5136a2fcd6ab0151b"}, + {file = "immutables-0.21.tar.gz", hash = "sha256:b55ffaf0449790242feb4c56ab799ea7af92801a0a43f9e2f4f8af2ab24dfc4a"}, ] [[package]] diff --git a/src/cattrs/_compat.py b/src/cattrs/_compat.py index 85b41a95..0603bd3c 100644 --- a/src/cattrs/_compat.py +++ b/src/cattrs/_compat.py @@ -25,7 +25,6 @@ Optional, Protocol, Tuple, - TypedDict, Union, _AnnotatedAlias, _GenericAlias, @@ -53,12 +52,9 @@ "fields_dict", "ExceptionGroup", "ExtensionsTypedDict", - "get_type_alias_base", "has", - "is_type_alias", "is_typeddict", "TypeAlias", - "TypedDict", ] try: @@ -112,20 +108,6 @@ def is_typeddict(cls: Any): return _is_typeddict(getattr(cls, "__origin__", cls)) -def is_type_alias(type: Any) -> bool: - """Is this a PEP 695 type alias?""" - return False - - -def get_type_alias_base(type: Any) -> Any: - """ - What is this a type alias of? - - Works only on 3.12+. - """ - return type.__value__ - - def has(cls): return hasattr(cls, "__attrs_attrs__") or hasattr(cls, "__dataclass_fields__") @@ -273,14 +255,6 @@ def is_tuple(type): ) -if sys.version_info >= (3, 12): - from typing import TypeAliasType - - def is_type_alias(type: Any) -> bool: - """Is this a PEP 695 type alias?""" - return isinstance(type, TypeAliasType) - - if sys.version_info >= (3, 10): def is_union_type(obj): diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 2559bca1..80685fdb 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -30,7 +30,6 @@ get_final_base, get_newtype_base, get_origin, - get_type_alias_base, has, has_with_generic, is_annotated, @@ -48,7 +47,6 @@ is_protocol, is_sequence, is_tuple, - is_type_alias, is_typeddict, is_union_type, signature, @@ -92,6 +90,11 @@ from .gen.typeddicts import make_dict_structure_fn as make_typeddict_dict_struct_fn from .gen.typeddicts import make_dict_unstructure_fn as make_typeddict_dict_unstruct_fn from .literals import is_literal_containing_enums +from .typealiases import ( + get_type_alias_base, + is_type_alias, + type_alias_structure_factory, +) from .types import SimpleStructureHook __all__ = ["UnstructureStrategy", "BaseConverter", "Converter", "GenConverter"] @@ -259,7 +262,7 @@ def __init__( ), (is_generic_attrs, self._gen_structure_generic, True), (lambda t: get_newtype_base(t) is not None, self._structure_newtype), - (is_type_alias, self._find_type_alias_structure_hook, True), + (is_type_alias, type_alias_structure_factory, "extended"), ( lambda t: get_final_base(t) is not None, self._structure_final_factory, @@ -699,14 +702,6 @@ def _structure_newtype(self, val: UnstructuredValue, type) -> StructuredValue: base = get_newtype_base(type) return self.get_structure_hook(base)(val, base) - def _find_type_alias_structure_hook(self, type: Any) -> StructureHook: - base = get_type_alias_base(type) - res = self.get_structure_hook(base) - if res == self._structure_call: - # we need to replace the type arg of `structure_call` - return lambda v, _, __base=base: __base(v) - return lambda v, _, __base=base: res(v, __base) - def _structure_final_factory(self, type): base = get_final_base(type) res = self.get_structure_hook(base) diff --git a/src/cattrs/gen/typeddicts.py b/src/cattrs/gen/typeddicts.py index 5fac557e..5dd5d749 100644 --- a/src/cattrs/gen/typeddicts.py +++ b/src/cattrs/gen/typeddicts.py @@ -2,7 +2,7 @@ import re import sys -from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, TypeVar from attrs import NOTHING, Attribute from typing_extensions import _TypedDictMeta @@ -20,7 +20,6 @@ def get_annots(cl) -> dict[str, Any]: from .._compat import ( - TypedDict, get_full_type_hints, get_notrequired_base, get_origin, diff --git a/src/cattrs/typealiases.py b/src/cattrs/typealiases.py new file mode 100644 index 00000000..c153f12e --- /dev/null +++ b/src/cattrs/typealiases.py @@ -0,0 +1,57 @@ +"""Utilities for type aliases.""" + +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING, Any + +from ._compat import is_generic +from ._generics import deep_copy_with +from .dispatch import StructureHook +from .gen._generics import generate_mapping + +if TYPE_CHECKING: + from .converters import BaseConverter + +__all__ = ["is_type_alias", "get_type_alias_base", "type_alias_structure_factory"] + +if sys.version_info >= (3, 12): + from types import GenericAlias + from typing import TypeAliasType + + def is_type_alias(type: Any) -> bool: + """Is this a PEP 695 type alias?""" + return isinstance( + type.__origin__ if type.__class__ is GenericAlias else type, TypeAliasType + ) + +else: + + def is_type_alias(type: Any) -> bool: + """Is this a PEP 695 type alias?""" + return False + + +def get_type_alias_base(type: Any) -> Any: + """ + What is this a type alias of? + + Works only on 3.12+. + """ + return type.__value__ + + +def type_alias_structure_factory(type: Any, converter: BaseConverter) -> StructureHook: + base = get_type_alias_base(type) + if is_generic(type): + mapping = generate_mapping(type) + if base.__name__ in mapping: + # Probably just type T = T + base = mapping[base.__name__] + else: + base = deep_copy_with(base, mapping) + res = converter.get_structure_hook(base) + if res == converter._structure_call: + # we need to replace the type arg of `structure_call` + return lambda v, _, __base=base: __base(v) + return lambda v, _, __base=base: res(v, __base) diff --git a/tests/test_generics_695.py b/tests/test_generics_695.py index 380d8e25..bfb28dc1 100644 --- a/tests/test_generics_695.py +++ b/tests/test_generics_695.py @@ -89,3 +89,19 @@ def structure_testclass(val, type): type TestAlias = TestClass assert converter.structure(None, TestAlias) is TestClass + + +def test_generic_type_alias(converter: BaseConverter): + """Generic type aliases work. + + See https://docs.python.org/3/reference/compound_stmts.html#generic-type-aliases + for details. + """ + + type Gen1[T] = T + + assert converter.structure("1", Gen1[int]) == 1 + + type Gen2[K, V] = dict[K, V] + + assert converter.structure({"a": "1"}, Gen2[str, int]) == {"a": 1} diff --git a/tests/test_v.py b/tests/test_v.py index d2ba3f75..ac361be4 100644 --- a/tests/test_v.py +++ b/tests/test_v.py @@ -8,13 +8,14 @@ Optional, Sequence, Tuple, + TypedDict, ) from attrs import Factory, define, field from pytest import fixture, raises from cattrs import Converter, transform_error -from cattrs._compat import Mapping, TypedDict +from cattrs._compat import Mapping from cattrs.errors import IterableValidationError from cattrs.gen import make_dict_structure_fn from cattrs.v import format_exception diff --git a/tests/typeddicts.py b/tests/typeddicts.py index 21dcfe1f..ff4c93d5 100644 --- a/tests/typeddicts.py +++ b/tests/typeddicts.py @@ -2,7 +2,7 @@ from datetime import datetime, timezone from string import ascii_lowercase -from typing import Any, Generic, List, Optional, TypeVar +from typing import Any, Generic, List, Optional, TypedDict, TypeVar from attrs import NOTHING from hypothesis import note @@ -19,13 +19,7 @@ text, ) -from cattrs._compat import ( - Annotated, - ExtensionsTypedDict, - NotRequired, - Required, - TypedDict, -) +from cattrs._compat import Annotated, ExtensionsTypedDict, NotRequired, Required from .untyped import gen_attr_names