diff --git a/pyproject.toml b/pyproject.toml index 2fca6459..a8dd7172 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -153,6 +153,7 @@ select = [ "C", # flake8-comprehensions "B", # flake8-bugbear ] +ignore = ["B904"] line-length = 100 [tool.ruff.lint.mccabe] diff --git a/serde/core.py b/serde/core.py index aed13013..ad1f17c8 100644 --- a/serde/core.py +++ b/serde/core.py @@ -937,8 +937,13 @@ def __call__(self, **kwargs: Any) -> TypeCheck: strict = TypeCheck(kind=TypeCheck.Kind.Strict) -def coerce_object(typ: type[Any], obj: Any) -> Any: - return typ(obj) if is_coercible(typ, obj) else obj +def coerce_object(cls: str, field: str, typ: type[Any], obj: Any) -> Any: + try: + return typ(obj) if is_coercible(typ, obj) else obj + except Exception as e: + raise SerdeError( + f"failed to coerce the field {cls}.{field} value {obj} into {typename(typ)}: {e}" + ) def is_coercible(typ: type[Any], obj: Any) -> bool: diff --git a/serde/de.py b/serde/de.py index 6356bfd2..047be4de 100644 --- a/serde/de.py +++ b/serde/de.py @@ -701,6 +701,7 @@ class Renderer: suppress_coerce: bool = False """ Disable type coercing in codegen """ class_deserializer: Optional[ClassDeserializer] = None + class_name: Optional[str] = None def render(self, arg: DeField[Any]) -> str: """ @@ -844,23 +845,6 @@ def dataclass(self, arg: DeField[Any]) -> str: def opt(self, arg: DeField[Any]) -> str: """ Render rvalue for Optional. - - >>> Renderer('foo').render(DeField(Optional[int], 'o', datavar='data')) - '(coerce_object(int, data["o"])) if data.get("o") is not None else None' - - >>> Renderer('foo').render(DeField(Optional[list[int]], 'o', datavar='data')) - '([coerce_object(int, v) for v in data["o"]]) if data.get("o") is not None else None' - - >>> Renderer('foo').render(DeField(Optional[list[int]], 'o', datavar='data')) - '([coerce_object(int, v) for v in data["o"]]) if data.get("o") is not None else None' - - >>> @deserialize - ... class Foo: - ... o: Optional[list[int]] - >>> Renderer('foo').render(DeField(Optional[Foo], 'f', datavar='data')) - '(Foo.__serde__.funcs[\\'foo\\'](data=data["f"], maybe_generic=maybe_generic, \ -maybe_generic_type_vars=maybe_generic_type_vars, variable_type_args=None, \ -reuse_instances=reuse_instances)) if data.get("f") is not None else None' """ inner = arg[0] if arg.iterbased: @@ -886,12 +870,6 @@ def opt(self, arg: DeField[Any]) -> str: def list(self, arg: DeField[Any]) -> str: """ Render rvalue for list. - - >>> Renderer('foo').render(DeField(list[int], 'l', datavar='data')) - '[coerce_object(int, v) for v in data["l"]]' - - >>> Renderer('foo').render(DeField(list[list[int]], 'l', datavar='data')) - '[[coerce_object(int, v) for v in v] for v in data["l"]]' """ if is_bare_list(arg.type): return f"list({arg.data})" @@ -901,13 +879,6 @@ def list(self, arg: DeField[Any]) -> str: def set(self, arg: DeField[Any]) -> str: """ Render rvalue for set. - - >>> from typing import Set - >>> Renderer('foo').render(DeField(Set[int], 'l', datavar='data')) - 'set(coerce_object(int, v) for v in data["l"])' - - >>> Renderer('foo').render(DeField(Set[Set[int]], 'l', datavar='data')) - 'set(set(coerce_object(int, v) for v in v) for v in data["l"])' """ if is_bare_set(arg.type): return f"set({arg.data})" @@ -919,26 +890,6 @@ def set(self, arg: DeField[Any]) -> str: def tuple(self, arg: DeField[Any]) -> str: """ Render rvalue for tuple. - - >>> @deserialize - ... class Foo: pass - >>> Renderer('foo').render(DeField(tuple[str, int, list[int], Foo], 'd', datavar='data')) - '(coerce_object(str, data["d"][0]), coerce_object(int, data["d"][1]), \ -[coerce_object(int, v) for v in data["d"][2]], \ -Foo.__serde__.funcs[\\'foo\\'](data=data["d"][3], maybe_generic=maybe_generic, \ -maybe_generic_type_vars=maybe_generic_type_vars, variable_type_args=None, \ -reuse_instances=reuse_instances),)' - - >>> field = DeField(tuple[str, int, list[int], Foo], - ... 'd', - ... datavar='data', - ... index=0, - ... iterbased=True) - >>> Renderer('foo').render(field) - "(coerce_object(str, data[0][0]), coerce_object(int, data[0][1]), \ -[coerce_object(int, v) for v in data[0][2]], Foo.__serde__.funcs['foo'](data=data[0][3], \ -maybe_generic=maybe_generic, maybe_generic_type_vars=maybe_generic_type_vars, \ -variable_type_args=None, reuse_instances=reuse_instances),)" """ if is_bare_tuple(arg.type): return f"tuple({arg.data})" @@ -956,21 +907,6 @@ def tuple(self, arg: DeField[Any]) -> str: def dict(self, arg: DeField[Any]) -> str: """ Render rvalue for dict. - - >>> Renderer('foo').render(DeField(dict[str, int], 'd', datavar='data')) - '{coerce_object(str, k): coerce_object(int, v) for k, v in data["d"].items()}' - - >>> @deserialize - ... class Foo: pass - >>> Renderer('foo').render(DeField(dict[Foo, list[Foo]], 'f', datavar='data')) - '\ -{Foo.__serde__.funcs[\\'foo\\'](data=k, maybe_generic=maybe_generic, \ -maybe_generic_type_vars=maybe_generic_type_vars, variable_type_args=None, \ -reuse_instances=reuse_instances): \ -[Foo.__serde__.funcs[\\'foo\\'](data=v, maybe_generic=maybe_generic, \ -maybe_generic_type_vars=maybe_generic_type_vars, \ -variable_type_args=None, reuse_instances=reuse_instances) for v in v] \ -for k, v in data["f"].items()}' """ if is_bare_dict(arg.type): return arg.data @@ -1000,15 +936,6 @@ def primitive(self, arg: DeField[Any], suppress_coerce: bool = False) -> str: Render rvalue for primitives. * `suppress_coerce`: Overrides "suppress_coerce" in the Renderer's field - - >>> Renderer('foo').render(DeField(int, 'i', datavar='data')) - 'coerce_object(int, data["i"])' - - >>> Renderer('foo').render(DeField(int, 'int_field', datavar='data', case='camelcase')) - 'coerce_object(int, data["intField"])' - - >>> Renderer('foo').render(DeField(int, 'i', datavar='data', index=1, iterbased=True)) - 'coerce_object(int, data[1])' """ typ = typename(arg.type) dat = arg.data @@ -1018,7 +945,7 @@ def primitive(self, arg: DeField[Any], suppress_coerce: bool = False) -> str: if self.suppress_coerce and suppress_coerce: return dat else: - return f"coerce_object({typ}, {dat})" + return f'coerce_object("{self.class_name}", "{arg.name}", {typ}, {dat})' def c_tor(self, arg: DeField[Any]) -> str: return f"{typename(arg.type)}({arg.data})" @@ -1193,6 +1120,7 @@ def render_from_iter( legacy_class_deserializer=legacy_class_deserializer, suppress_coerce=(not type_check.is_coerce()), class_deserializer=class_deserializer, + class_name=typename(cls), ) fields = list(filter(renderable, defields(cls))) res = jinja2_env.get_template("iter").render( @@ -1223,6 +1151,7 @@ def render_from_dict( legacy_class_deserializer=legacy_class_deserializer, suppress_coerce=(not type_check.is_coerce()), class_deserializer=class_deserializer, + class_name=typename(cls), ) fields = list(filter(renderable, defields(cls))) res = jinja2_env.get_template("dict").render( diff --git a/serde/se.py b/serde/se.py index 52e8702f..1501a51b 100644 --- a/serde/se.py +++ b/serde/se.py @@ -610,6 +610,7 @@ def render_to_tuple( suppress_coerce=(not type_check.is_coerce()), serialize_class_var=serialize_class_var, class_serializer=class_serializer, + class_name=typename(cls), ) return jinja2_env.get_template("iter").render( func=TO_ITER, @@ -633,6 +634,7 @@ def render_to_dict( legacy_class_serializer, suppress_coerce=(not type_check.is_coerce()), class_serializer=class_serializer, + class_name=typename(cls), ) lrenderer = LRenderer(case, serialize_class_var) return jinja2_env.get_template("dict").render( @@ -652,7 +654,7 @@ def render_union_func( Render function that serializes a field with union type. """ union_name = f"Union[{', '.join([typename(a) for a in union_args])}]" - renderer = Renderer(TO_DICT, suppress_coerce=True) + renderer = Renderer(TO_DICT, suppress_coerce=True, class_name=typename(cls)) return jinja2_env.get_template("union").render( func=union_func_name(UNION_SE_PREFIX, union_args), serde_scope=getattr(cls, SERDE_SCOPE), @@ -710,46 +712,11 @@ class Renderer: """ Suppress type coercing because generated union serializer has its own type checking """ serialize_class_var: bool = False class_serializer: Optional[ClassSerializer] = None + class_name: Optional[str] = None def render(self, arg: SeField[Any]) -> str: """ Render rvalue - - >>> Renderer(TO_ITER).render(SeField(int, 'i')) - 'coerce_object(int, i)' - - >>> Renderer(TO_ITER).render(SeField(list[int], 'l')) - '[coerce_object(int, v) for v in l]' - - >>> @serialize - ... @dataclass(unsafe_hash=True) - ... class Foo: - ... val: int - >>> Renderer(TO_ITER).render(SeField(Foo, 'foo')) - "\ -foo.__serde__.funcs['to_iter'](foo, reuse_instances=reuse_instances, convert_sets=convert_sets)" - - >>> Renderer(TO_ITER).render(SeField(list[Foo], 'foo')) - "\ -[v.__serde__.funcs['to_iter'](v, reuse_instances=reuse_instances, \ -convert_sets=convert_sets) for v in foo]" - - >>> Renderer(TO_ITER).render(SeField(dict[str, Foo], 'foo')) - "\ -{coerce_object(str, k): v.__serde__.funcs['to_iter'](v, reuse_instances=reuse_instances, \ -convert_sets=convert_sets) for k, v in foo.items()}" - - >>> Renderer(TO_ITER).render(SeField(dict[Foo, Foo], 'foo')) - "\ -{k.__serde__.funcs['to_iter'](k, reuse_instances=reuse_instances, \ -convert_sets=convert_sets): v.__serde__.funcs['to_iter'](v, reuse_instances=reuse_instances, \ -convert_sets=convert_sets) for k, v in foo.items()}" - - >>> Renderer(TO_ITER).render(SeField(tuple[str, Foo, int], 'foo')) - "\ -(coerce_object(str, foo[0]), foo[1].__serde__.funcs['to_iter'](foo[1], \ -reuse_instances=reuse_instances, convert_sets=convert_sets), \ -coerce_object(int, foo[2]),)" """ implemented_methods: dict[type[Any], int] = {} class_serializers: Iterable[ClassSerializer] = itertools.chain( @@ -925,7 +892,7 @@ def primitive(self, arg: SeField[Any]) -> str: if self.suppress_coerce: return var else: - return f"coerce_object({typ}, {var})" + return f'coerce_object("{self.class_name}", "{arg.name}", {typ}, {var})' def string(self, arg: SeField[Any]) -> str: return f"str({arg.varname})" diff --git a/tests/test_de.py b/tests/test_de.py index 26290b4a..d6e2d10b 100644 --- a/tests/test_de.py +++ b/tests/test_de.py @@ -1,6 +1,6 @@ from decimal import Decimal -from typing import Union -from serde.de import from_obj +from typing import Union, Optional +from serde.de import deserialize, from_obj, Renderer, DeField def test_from_obj() -> None: @@ -23,3 +23,105 @@ def test_from_obj() -> None: dec = from_obj(list[Decimal], ("0.1", 0.1), False, False) assert isinstance(dec[0], Decimal) and dec[0] == Decimal("0.1") assert isinstance(dec[1], Decimal) and dec[1] == Decimal(0.1) + + +kwargs = ( + "maybe_generic=maybe_generic, maybe_generic_type_vars=maybe_generic_type_vars, " + + "variable_type_args=None, reuse_instances=reuse_instances" +) + + +def test_render_primitives() -> None: + + rendered = Renderer("foo").render(DeField(int, "i", datavar="data")) + assert rendered == 'coerce_object("None", "i", int, data["i"])' + + rendered = Renderer("foo").render(DeField(int, "int_field", datavar="data", case="camelcase")) + assert rendered == 'coerce_object("None", "int_field", int, data["intField"])' + + rendered = Renderer("foo").render(DeField(int, "i", datavar="data", index=1, iterbased=True)) + assert rendered == 'coerce_object("None", "i", int, data[1])' + + +def test_render_list() -> None: + rendered = Renderer("foo").render(DeField(list[int], "l", datavar="data")) + assert rendered == '[coerce_object("None", "v", int, v) for v in data["l"]]' + + rendered = Renderer("foo").render(DeField(list[list[int]], "l", datavar="data")) + assert rendered == '[[coerce_object("None", "v", int, v) for v in v] for v in data["l"]]' + + +def test_render_tuple() -> None: + @deserialize + class Foo: + pass + + rendered = Renderer("foo").render(DeField(tuple[str, int, list[int], Foo], "d", datavar="data")) + rendered_str = 'coerce_object("None", "data["d"][0]", str, data["d"][0])' + rendered_int = 'coerce_object("None", "data["d"][1]", int, data["d"][1])' + rendered_lst = '[coerce_object("None", "v", int, v) for v in data["d"][2]]' + rendered_foo = f"Foo.__serde__.funcs['foo'](data=data[\"d\"][3], {kwargs})" + assert rendered == f"({rendered_str}, {rendered_int}, {rendered_lst}, {rendered_foo},)" + + field = DeField(tuple[str, int, list[int], Foo], "d", datavar="data", index=0, iterbased=True) + rendered = Renderer("foo").render(field) + rendered_str = 'coerce_object("None", "data[0][0]", str, data[0][0])' + rendered_int = 'coerce_object("None", "data[0][1]", int, data[0][1])' + rendered_lst = '[coerce_object("None", "v", int, v) for v in data[0][2]]' + rendered_foo = f"Foo.__serde__.funcs['foo'](data=data[0][3], {kwargs})" + assert rendered == f"({rendered_str}, {rendered_int}, {rendered_lst}, {rendered_foo},)" + + +def test_render_dict() -> None: + rendered = Renderer("foo").render(DeField(dict[str, int], "d", datavar="data")) + rendered_key = 'coerce_object("None", "k", str, k)' + rendered_val = 'coerce_object("None", "v", int, v)' + rendered_dct = f'{{{rendered_key}: {rendered_val} for k, v in data["d"].items()}}' + assert rendered == rendered_dct + + @deserialize + class Foo: + pass + + rendered = Renderer("foo").render(DeField(dict[Foo, list[Foo]], "f", datavar="data")) + rendered_key = f"Foo.__serde__.funcs['foo'](data=k, {kwargs})" + rendered_val = f"[Foo.__serde__.funcs['foo'](data=v, {kwargs}) for v in v]" + + assert rendered == f'{{{rendered_key}: {rendered_val} for k, v in data["f"].items()}}' + + +def test_render_set() -> None: + from typing import Set + + rendered = Renderer("foo").render(DeField(Set[int], "l", datavar="data")) + assert rendered == 'set(coerce_object("None", "v", int, v) for v in data["l"])' + + rendered = Renderer("foo").render(DeField(Set[Set[int]], "l", datavar="data")) + assert rendered == 'set(set(coerce_object("None", "v", int, v) for v in v) for v in data["l"])' + + +def test_render_opt() -> None: + rendered = Renderer("foo").render(DeField(Optional[int], "o", datavar="data")) # type: ignore + rendered_opt = ( + '(coerce_object("None", "o", int, data["o"])) if data.get("o") is not None else None' + ) + assert rendered == rendered_opt + + rendered = Renderer("foo").render(DeField(Optional[list[int]], "o", datavar="data")) # type: ignore + rendered_lst = '[coerce_object("None", "v", int, v) for v in data["o"]]' + rendered_opt = f'({rendered_lst}) if data.get("o") is not None else None' + assert rendered == rendered_opt + + rendered = Renderer("foo").render(DeField(Optional[list[int]], "o", datavar="data")) # type: ignore + rendered_lst = '[coerce_object("None", "v", int, v) for v in data["o"]]' + rendered_opt = f'({rendered_lst}) if data.get("o") is not None else None' + assert rendered == rendered_opt + + @deserialize + class Foo: + a: Optional[list[int]] + + rendered = Renderer("foo").render(DeField(Optional[Foo], "f", datavar="data")) # type: ignore + rendered_foo = f"Foo.__serde__.funcs['foo'](data=data[\"f\"], {kwargs})" + rendered_opt = f'({rendered_foo}) if data.get("f") is not None else None' + assert rendered == rendered_opt diff --git a/tests/test_se.py b/tests/test_se.py index 2697c5f5..a82e379a 100644 --- a/tests/test_se.py +++ b/tests/test_se.py @@ -1,6 +1,8 @@ from serde import asdict, astuple, serialize, to_dict, to_tuple from serde.json import to_json from serde.msgpack import to_msgpack +from serde.se import SeField, Renderer +from serde.core import TO_ITER from . import data from .data import ( @@ -128,3 +130,51 @@ class A: assert a_dict == {"v": ["a", "b"]} or a_dict == {"v": ["b", "a"]} assert {"v": {"a", "b"}} == to_dict(a, convert_sets=False) + + +@serialize +class Foo: + val: int + + +kwargs = "reuse_instances=reuse_instances, convert_sets=convert_sets" + + +def test_render_primitives() -> None: + rendered = Renderer(TO_ITER).render(SeField(int, "i")) + assert rendered == 'coerce_object("None", "i", int, i)' + + +def test_render_list() -> None: + + rendered = Renderer(TO_ITER).render(SeField(list[int], "l")) + assert rendered == '[coerce_object("None", "v", int, v) for v in l]' + + rendered = Renderer(TO_ITER).render(SeField(list[Foo], "foo")) + assert rendered == f"[v.__serde__.funcs['to_iter'](v, {kwargs}) for v in foo]" + + +def test_render_dict() -> None: + rendered = Renderer(TO_ITER).render(SeField(dict[str, Foo], "foo")) + rendered_key = 'coerce_object("None", "k", str, k)' + rendered_val = f"v.__serde__.funcs['to_iter'](v, {kwargs})" + assert rendered == f"{{{rendered_key}: {rendered_val} for k, v in foo.items()}}" + + rendered = Renderer(TO_ITER).render(SeField(dict[Foo, Foo], "foo")) + rendered_key = f"k.__serde__.funcs['to_iter'](k, {kwargs})" + rendered_val = f"v.__serde__.funcs['to_iter'](v, {kwargs})" + assert rendered == f"{{{rendered_key}: {rendered_val} for k, v in foo.items()}}" + + +def test_render_tuple() -> None: + rendered = Renderer(TO_ITER).render(SeField(tuple[str, Foo, int], "foo")) + rendered_str = 'coerce_object("None", "foo[0]", str, foo[0])' + rendered_foo = f"foo[1].__serde__.funcs['to_iter'](foo[1], {kwargs})" + rendered_int = 'coerce_object("None", "foo[2]", int, foo[2])' + assert rendered == f"({rendered_str}, {rendered_foo}, {rendered_int},)" + + +def test_render_dataclass() -> None: + rendered = Renderer(TO_ITER).render(SeField(Foo, "foo")) + rendered_foo = f"foo.__serde__.funcs['to_iter'](foo, {kwargs})" + assert rendered_foo == rendered