Skip to content

Commit

Permalink
Merge pull request #509 from yukinarit/empty-tuple
Browse files Browse the repository at this point in the history
Handle empty tuple more properly
  • Loading branch information
yukinarit authored Apr 2, 2024
2 parents 31289c8 + 9a7a484 commit ad8c148
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 20 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,5 @@ select = [
]
line-length = 100

[tool.ruff.mccabe]
[tool.ruff.lint.mccabe]
max-complexity = 30
36 changes: 28 additions & 8 deletions serde/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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


Expand Down
22 changes: 11 additions & 11 deletions tests/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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])

Expand All @@ -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, ...])

Expand All @@ -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]]]
Expand Down

0 comments on commit ad8c148

Please sign in to comment.