diff --git a/README.md b/README.md index a236d444..49e00c67 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Happy coding with pyserde! 🚀 - [PEP585 Type Hinting Generics In Standard Collections](https://github.com/yukinarit/pyserde/blob/main/docs/en/getting-started.md#pep585-and-pep604) - [PEP604 Allow writing union types as X | Y](https://github.com/yukinarit/pyserde/blob/main/docs/en/getting-started.md#pep585-and-pep604) - [PEP681 Data Class Transform](https://github.com/yukinarit/pyserde/blob/main/docs/en/decorators.md#serde) +- [PEP695 Type Parameter Syntax](https://peps.python.org/pep-0695/) - [Case Conversion](https://github.com/yukinarit/pyserde/blob/main/docs/en/class-attributes.md#rename_all) - [Rename](https://github.com/yukinarit/pyserde/blob/main/docs/en/field-attributes.md#rename) - [Alias](https://github.com/yukinarit/pyserde/blob/main/docs/en/field-attributes.md#alias) diff --git a/conftest.py b/conftest.py index 1e318be6..95c776d0 100644 --- a/conftest.py +++ b/conftest.py @@ -3,6 +3,5 @@ collect_ignore = ["setup.py"] -if sys.version_info[:2] == (3, 6): - # skip these tests for python 3.6 because it does not support PEP 563 - collect_ignore.append("tests/test_lazy_type_evaluation.py") +if sys.version_info[:2] < (3, 12): + collect_ignore.append("tests/test_type_alias.py") diff --git a/examples/runner.py b/examples/runner.py index b56daa1c..99b89761 100644 --- a/examples/runner.py +++ b/examples/runner.py @@ -53,6 +53,7 @@ def run_all() -> None: import type_numpy import type_pathlib import type_uuid + import type_alias_pep695 import union import union_tagging import union_directly @@ -88,6 +89,7 @@ def run_all() -> None: run(union_tagging) run(union_directly) run(generics) + run(type_alias_pep695) run(generics_pep695) run(generics_nested) run(nested) diff --git a/examples/type_alias_pep695.py b/examples/type_alias_pep695.py new file mode 100644 index 00000000..a058088e --- /dev/null +++ b/examples/type_alias_pep695.py @@ -0,0 +1,33 @@ +from serde import serde +from serde.json import from_json, to_json + + +@serde +class Bar: + a: int + + +@serde +class Baz: + b: int + + +# BarBaz = Bar | Baz +type BarBaz = Bar | Baz + + +@serde +class Foo: + barbaz: BarBaz + + +def main() -> None: + f = Foo(Baz(10)) + s = to_json(f) + print(f"Into Json: {s}") + ff = from_json(Foo, s) + print(f"From Json: {ff}") + + +if __name__ == "__main__": + main() diff --git a/serde/compat.py b/serde/compat.py index 44b2de10..951c8fd5 100644 --- a/serde/compat.py +++ b/serde/compat.py @@ -20,7 +20,7 @@ from typing import TypeVar, Generic, Any, ClassVar, Optional, NewType, Union, Hashable, Callable import typing_inspect -from typing_extensions import TypeGuard, ParamSpec +from typing_extensions import TypeGuard, ParamSpec, TypeAliasType from .sqlalchemy import is_sqlalchemy_inspectable @@ -351,6 +351,8 @@ def recursive(cls: Union[type[Any], Any]) -> None: if args and len(args) >= 2: recursive(args[0]) recursive(args[1]) + elif is_pep695_type_alias(cls): + recursive(cls.__value__) else: lst.add(cls) @@ -373,6 +375,8 @@ def recursive(cls: TypeLike) -> None: lst.append(cls) for arg in type_args(cls): recursive(arg) + elif is_pep695_type_alias(cls): + recursive(cls.__value__) if is_dataclass(cls): stack.append(cls) for f in dataclass_fields(cls): @@ -864,6 +868,13 @@ def is_ellipsis(typ: Any) -> bool: return typ is Ellipsis +def is_pep695_type_alias(typ: Any) -> bool: + """ + Test if the type is of PEP695 type alias. + """ + return isinstance(typ, TypeAliasType) + + @cache def get_type_var_names(cls: type[Any]) -> Optional[list[str]]: """ diff --git a/serde/de.py b/serde/de.py index 5e3e4789..569148d8 100644 --- a/serde/de.py +++ b/serde/de.py @@ -52,6 +52,7 @@ is_tuple, is_union, is_variable_tuple, + is_pep695_type_alias, iter_literals, iter_types, iter_unions, @@ -772,6 +773,8 @@ def render(self, arg: DeField[Any]) -> str: res = "None" elif is_any(arg.type) or is_ellipsis(arg.type): res = arg.data + elif is_pep695_type_alias(arg.type): + res = self.render(DeField(name=arg.name, type=arg.type.__value__, datavar=arg.datavar)) elif is_primitive(arg.type): # For subclasses for primitives e.g. class FooStr(str), coercing is always enabled res = self.primitive(arg, not is_primitive_subclass(arg.type)) diff --git a/serde/se.py b/serde/se.py index 82ed0890..c1b82502 100644 --- a/serde/se.py +++ b/serde/se.py @@ -46,6 +46,7 @@ is_tuple, is_union, is_variable_tuple, + is_pep695_type_alias, iter_types, iter_unions, type_args, @@ -804,6 +805,12 @@ def render(self, arg: SeField[Any]) -> str: elif is_class_var(arg.type): arg.type = type_args(arg.type)[0] res = self.render(arg) + elif is_pep695_type_alias(arg.type): + res = self.render( + SeField( + name=arg.name, type=arg.type.__value__, parent=SeField(None.__class__, "obj") + ) + ) else: res = f"raise_unsupported_type({arg.varname})" diff --git a/tests/test_type_alias.py b/tests/test_type_alias.py new file mode 100644 index 00000000..6e00fd96 --- /dev/null +++ b/tests/test_type_alias.py @@ -0,0 +1,36 @@ +from serde import serde, from_dict, to_dict + + +type S = str + + +def test_pep695_type_alias() -> None: + + @serde + class Foo: + s: S + + f = Foo("foo") + assert f == from_dict(Foo, to_dict(f)) + + +@serde +class Bar: + a: int + + +@serde +class Baz: + b: int + + +type BarBaz = Bar | Baz + + +def test_pep695_type_alias_union() -> None: + @serde + class Foo: + barbaz: BarBaz + + f = Foo(Baz(10)) + assert f == from_dict(Foo, to_dict(f))