From 3604f7b559a9d37520a399a920d3ddead14a808c Mon Sep 17 00:00:00 2001 From: yukinarit Date: Wed, 26 Jun 2024 22:53:04 +0900 Subject: [PATCH] Fix deserialization when internal tagging is used for non dataclass Closes #472 --- serde/compat.py | 9 ++++++++- serde/de.py | 8 ++++++-- tests/test_compat.py | 2 +- tests/test_union.py | 16 ++++++++++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/serde/compat.py b/serde/compat.py index 2b3ccb9d..77b15bff 100644 --- a/serde/compat.py +++ b/serde/compat.py @@ -214,7 +214,7 @@ def typename(typ: Any, with_typing_module: bool = False) -> str: args = type_args(typ) if not args: raise TypeError("Literal type requires at least one literal argument") - return f'Literal[{", ".join(str(e) for e in args)}]' + return f'Literal[{", ".join(stringify_literal(e) for e in args)}]' elif typ is Any: return f"{mod}Any" elif is_ellipsis(typ): @@ -236,6 +236,13 @@ def typename(typ: Any, with_typing_module: bool = False) -> str: raise SerdeError(f"Could not get a type name from: {typ}") +def stringify_literal(v: Any) -> str: + if isinstance(v, str): + return f"'{v}'" + else: + return str(v) + + def type_args(typ: Any) -> tuple[type[Any], ...]: """ Wrapper to suppress type error for accessing private members. diff --git a/serde/de.py b/serde/de.py index e51458ca..6356bfd2 100644 --- a/serde/de.py +++ b/serde/de.py @@ -13,6 +13,7 @@ import jinja2 from collections.abc import Callable, Sequence, Iterable from beartype import beartype, BeartypeConf +from beartype.door import is_bearable from beartype.roar import BeartypeCallHintParamViolation from dataclasses import dataclass, is_dataclass from typing import overload, TypeVar, Generic, Any, Optional, Union, Literal @@ -263,6 +264,7 @@ def wrap(cls: type[T]) -> type[T]: g["_get_by_aliases"] = _get_by_aliases g["class_deserializers"] = class_deserializers g["BeartypeCallHintParamViolation"] = BeartypeCallHintParamViolation + g["is_bearable"] = is_bearable if deserializer: g["serde_legacy_custom_class_deserializer"] = functools.partial( serde_legacy_custom_class_deserializer, custom=deserializer @@ -1158,9 +1160,11 @@ def {{func}}(cls=cls, maybe_generic=None, maybe_generic_type_vars=None, data=Non if not isinstance(fake_dict["fake_key"], {{typename(t)}}): raise Exception("Not a type of {{typename(t)}}") {% endif %} - return {{rvalue(arg(t))}} + res = {{rvalue(arg(t))}} + ensure(is_bearable(res, {{typename(t)}}), "object is not of type '{{typename(t)}}'") + return res except Exception as e: - errors.append(f' Failed to deserialize into {{typename(t)}}: {e}') + errors.append(f" Failed to deserialize into {{typename(t)}}: {e}") {% endfor %} raise SerdeError("Can not deserialize " + repr(data) + " of type " + \ typename(type(data)) + " into {{union_name}}.\\nReasons:\\n" + "\\n".join(errors)) diff --git a/tests/test_compat.py b/tests/test_compat.py index a1fd8888..265f1970 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -83,7 +83,7 @@ class Foo(Generic[T]): assert typename(dict[str, Foo]) == "dict[str, Foo]" # type: ignore assert typename(set) == "set" assert typename(set[int]) == "set[int]" - assert typename(Literal[1, True, "Hey"]) == "Literal[1, True, Hey]" + assert typename(Literal[1, True, "Hey"]) == "Literal[1, True, 'Hey']" def test_iter_types() -> None: diff --git a/tests/test_union.py b/tests/test_union.py index 75fa80d6..04edf35d 100644 --- a/tests/test_union.py +++ b/tests/test_union.py @@ -822,3 +822,19 @@ class Foo: s = '{"a":"1","b":{"Expr":{"expr":"2"}},"c":"3"}' assert s == to_json(f) assert f == from_json(Foo, s) + + +def test_union_internal_tagging_for_non_dataclass() -> None: + @serde + class Bar: + a: int + + @serde(tagging=InternalTagging("type")) + class Foo: + x: Union[list[int], Bar] + + f = Foo(Bar(1)) + assert f == from_json(Foo, to_json(f)) + + f = Foo([10]) + assert f == from_json(Foo, to_json(f))