Skip to content

Commit

Permalink
Merge pull request #569 from yukinarit/improve-error-message
Browse files Browse the repository at this point in the history
Improve error message for coercing
  • Loading branch information
yukinarit authored Jul 5, 2024
2 parents 0587025 + 586d02a commit b5db8a3
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 117 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ select = [
"C", # flake8-comprehensions
"B", # flake8-bugbear
]
ignore = ["B904"]
line-length = 100

[tool.ruff.lint.mccabe]
Expand Down
9 changes: 7 additions & 2 deletions serde/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
79 changes: 4 additions & 75 deletions serde/de.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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:
Expand All @@ -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})"
Expand All @@ -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})"
Expand All @@ -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})"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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})"
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
43 changes: 5 additions & 38 deletions serde/se.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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),
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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})"
Expand Down
106 changes: 104 additions & 2 deletions tests/test_de.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
Loading

0 comments on commit b5db8a3

Please sign in to comment.