From 9a7a48471b950d81bce18d79c9ffafd5141b6f99 Mon Sep 17 00:00:00 2001 From: yukinarit Date: Mon, 1 Apr 2024 00:24:58 +0900 Subject: [PATCH] Handle empty tuple more properly --- pyproject.toml | 2 +- serde/core.py | 36 ++++++++++++++++++++++++++++-------- tests/test_compat.py | 22 +++++++++++----------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2cc4da8a..a4da3031 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -156,5 +156,5 @@ select = [ ] line-length = 100 -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 30 diff --git a/serde/core.py b/serde/core.py index 883e688f..71f47e05 100644 --- a/serde/core.py +++ b/serde/core.py @@ -11,6 +11,7 @@ import re import casefy from dataclasses import dataclass +from beartype.door import is_bearable from collections.abc import Mapping, Sequence, Callable from typing import ( overload, @@ -343,8 +344,8 @@ def add_func(serde_scope: Scope, func_name: str, func_code: str, globals: dict[s def is_instance(obj: Any, typ: Any) -> bool: """ - Type check function that works like `isinstance` but it accepts - Subscripted Generics e.g. `list[int]`. + pyserde's own `isinstance` helper. It accepts subscripted generics e.g. `list[int]` and + deeply check object against declared type. """ if dataclasses.is_dataclass(typ): return isinstance(obj, typ) @@ -375,7 +376,7 @@ def is_instance(obj: Any, typ: Any) -> bool: elif typ is Ellipsis: return True else: - return isinstance(obj, typ) + return is_bearable(obj, typ) def is_opt_instance(obj: Any, typ: type[Any]) -> bool: @@ -413,18 +414,37 @@ def is_set_instance(obj: Any, typ: type[Any]) -> bool: def is_tuple_instance(obj: Any, typ: type[Any]) -> bool: + args = type_args(typ) + if not isinstance(obj, tuple): return False - if is_variable_tuple(typ): + + # empty tuple + if len(args) == 0 and len(obj) == 0: + return True + + # In the form of tuple[T, ...] + elif is_variable_tuple(typ): + # Get the first type arg. Since tuple[T, ...] is homogeneous tuple, + # all the elements should be of this type. arg = type_args(typ)[0] for v in obj: if not is_instance(v, arg): return False - if len(obj) == 0 or is_bare_tuple(typ): return True - for i, arg in enumerate(type_args(typ)): - if not is_instance(obj[i], arg): - return False + + # bare tuple "tuple" is equivalent to tuple[Any, ...] + if is_bare_tuple(typ) and isinstance(obj, tuple): + return True + + # All the other tuples e.g. tuple[int, str] + if len(obj) == len(args): + for element, arg in zip(obj, args): + if not is_instance(element, arg): + return False + else: + return False + return True diff --git a/tests/test_compat.py b/tests/test_compat.py index b48565c0..b99e95d6 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -158,7 +158,7 @@ def test_union_args() -> None: def test_is_instance() -> None: - # Primitive + # primitive assert is_instance(10, int) assert is_instance("str", str) assert is_instance(1.0, float) @@ -167,13 +167,13 @@ def test_is_instance() -> None: assert not is_instance("10", int) assert is_instance(True, int) # see why this is true https://stackoverflow.com/a/37888668 - # Dataclass + # dataclass p = Pri(i=10, s="foo", f=100.0, b=True) assert is_instance(p, Pri) p.i = 10.0 # type: ignore assert is_instance(p, Pri) # there is no way to check mulated properties. - # Dataclass (Nested) + # dataclass (Nested) @dataclass class Foo: p: Pri @@ -190,7 +190,7 @@ class Foo: assert is_instance([10], list[int]) assert not is_instance([10.0], list[int]) - # list of dataclasses + # list of dataclass assert is_instance([Int(n) for n in range(1, 10)], list[Int]) assert not is_instance([Str("foo")], list[Int]) @@ -200,18 +200,18 @@ class Foo: assert is_instance({10}, set[int]) assert not is_instance({10.0}, set[int]) - # set of dataclasses + # set of dataclass assert is_instance({Int(n) for n in range(1, 10)}, set[Int]) assert not is_instance({Str("foo")}, set[Int]) # tuple - assert is_instance((), tuple[int, str, float, bool]) - assert is_instance((10, "a"), tuple) + assert not is_instance((), tuple[int, str, float, bool]) assert is_instance((10, "a"), tuple) assert is_instance((10, "foo", 100.0, True), tuple[int, str, float, bool]) assert not is_instance((10, "foo", 100.0, "last-type-is-wrong"), tuple[int, str, float, bool]) assert is_instance((10, 20), tuple[int, ...]) assert is_instance((10, 20, 30), tuple[int, ...]) + assert is_instance((), tuple[()]) assert is_instance((), tuple[int, ...]) assert not is_instance((10, "a"), tuple[int, ...]) @@ -230,21 +230,21 @@ class Foo: assert is_instance({"foo": 10, "bar": 20}, dict[str, int]) assert not is_instance({"foo": 10.0, "bar": 20}, dict[str, int]) - # dict of dataclasses + # dict of dataclass assert is_instance({Str("foo"): Int(10), Str("bar"): Int(20)}, dict[Str, Int]) assert not is_instance({Str("foo"): Str("wrong-type"), Str("bar"): Int(10)}, dict[Str, Int]) - # Optional + # optional assert is_instance(None, type(None)) assert is_instance(10, Optional[int]) assert is_instance(None, Optional[int]) assert not is_instance("wrong-type", Optional[int]) - # Optional of dataclass + # optional of dataclass assert is_instance(Int(10), Optional[Int]) assert not is_instance("wrong-type", Optional[Int]) - # Nested containers + # nested containers assert is_instance([({"a": "b"}, 10, [True])], list[tuple[dict[str, str], int, list[bool]]]) assert not is_instance( [({"a": "b"}, 10, ["wrong-type"])], list[tuple[dict[str, str], int, list[bool]]]