Skip to content

Commit

Permalink
Support pep695 type alias
Browse files Browse the repository at this point in the history
Closes #611
  • Loading branch information
yukinarit committed Dec 18, 2024
1 parent 3a77152 commit 471972f
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
2 changes: 2 additions & 0 deletions examples/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions examples/type_alias_pep695.py
Original file line number Diff line number Diff line change
@@ -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()
13 changes: 12 additions & 1 deletion serde/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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):
Expand Down Expand Up @@ -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]]:
"""
Expand Down
3 changes: 3 additions & 0 deletions serde/de.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
is_tuple,
is_union,
is_variable_tuple,
is_pep695_type_alias,
iter_literals,
iter_types,
iter_unions,
Expand Down Expand Up @@ -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))
Expand Down
7 changes: 7 additions & 0 deletions serde/se.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
is_tuple,
is_union,
is_variable_tuple,
is_pep695_type_alias,
iter_types,
iter_unions,
type_args,
Expand Down Expand Up @@ -803,6 +804,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})"

Expand Down
36 changes: 36 additions & 0 deletions tests/test_type_alias.py
Original file line number Diff line number Diff line change
@@ -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))

0 comments on commit 471972f

Please sign in to comment.