diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8ebc7fa..78e37aaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ $ git clone git@github.com:your_name_here/cattrs.git ```shell $ cd cattrs/ -$ poetry install +$ poetry install --all-extras ``` 4. Create a branch for local development:: diff --git a/HISTORY.md b/HISTORY.md index b0a9b01b..743022a8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -20,6 +20,7 @@ ([#350](https://github.com/python-attrs/cattrs/issues/350) [#353](https://github.com/python-attrs/cattrs/pull/353)) - Subclasses structuring and unstructuring is now supported via a custom `include_subclasses` strategy. ([#312](https://github.com/python-attrs/cattrs/pull/312)) +- Add support for `typing_extensions.Annotated` when the python version is less than `3.9`. ([#366](https://github.com/python-attrs/cattrs/pull/366)) - Add unstructuring and structuring support for the standard library `deque`. ([#355](https://github.com/python-attrs/cattrs/pull/355)) diff --git a/src/cattrs/_compat.py b/src/cattrs/_compat.py index fdeb61b4..ee8f0e20 100644 --- a/src/cattrs/_compat.py +++ b/src/cattrs/_compat.py @@ -174,6 +174,8 @@ def get_final_base(type) -> Optional[type]: from collections import Counter as ColCounter from typing import Counter, Union, _GenericAlias + from typing_extensions import Annotated, NotRequired, Required + from typing_extensions import get_origin as te_get_origin if is_py38: from typing import TypedDict, _TypedDictMeta @@ -181,10 +183,8 @@ def get_final_base(type) -> Optional[type]: _TypedDictMeta = None TypedDict = ExtensionsTypedDict - from typing_extensions import NotRequired, Required - - def is_annotated(_): - return False + def is_annotated(type) -> bool: + return te_get_origin(type) is Annotated def is_tuple(type): return type in (Tuple, tuple) or ( diff --git a/tests/test_converter.py b/tests/test_converter.py index eeff70d5..e774d0bd 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -653,3 +653,38 @@ class Outer: structured = converter.structure(raw, Outer) assert structured == Outer(Inner(2), [Inner(2)]) + + +def test_annotated_with_typing_extensions_attrs(): + """Annotation support works for attrs classes.""" + from typing_extensions import Annotated + from typing import List + + converter = Converter() + + @attr.define + class Inner: + a: int + + @attr.define + class Outer: + i: Annotated[Inner, "test"] # noqa + j: List[Annotated[Inner, "test"]] # noqa + + orig = Outer(Inner(1), [Inner(1)]) + raw = converter.unstructure(orig) + + assert raw == {"i": {"a": 1}, "j": [{"a": 1}]} + + structured = converter.structure(raw, Outer) + assert structured == orig + + # Now register a hook and rerun the test. + converter.register_unstructure_hook(Inner, lambda v: {"a": 2}) + + raw = converter.unstructure(Outer(Inner(1), [Inner(1)])) + + assert raw == {"i": {"a": 2}, "j": [{"a": 2}]} + + structured = converter.structure(raw, Outer) + assert structured == Outer(Inner(2), [Inner(2)])