Skip to content

Commit

Permalink
refactor: Change to pass "type_check" parameter in decorator
Browse files Browse the repository at this point in the history
Because beartype package can change the configuration only in the
decorator.
  • Loading branch information
yukinarit committed Aug 22, 2022
1 parent 413798f commit fbe8dc8
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 143 deletions.
4 changes: 2 additions & 2 deletions examples/simple.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from dataclasses import dataclass

from serde import serde
from serde import Strict, serde
from serde.json import from_json, to_json


@serde
@serde(type_check=Strict)
@dataclass
class Foo:
i: int
Expand Down
8 changes: 3 additions & 5 deletions serde/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,13 +752,11 @@ def __call__(self, **kwargs) -> 'TypeCheck':
Strict = TypeCheck(kind=TypeCheck.Kind.Strict)


def coerce(typ: Type, obj: Any, type_check: TypeCheck) -> Any:
return typ(obj) if is_coercible(typ, obj, type_check) else obj
def coerce(typ: Type, obj: Any) -> Any:
return typ(obj) if is_coercible(typ, obj) else obj


def is_coercible(typ: Type, obj: Any, type_check: TypeCheck) -> bool:
if not type_check.is_coerce():
return False
def is_coercible(typ: Type, obj: Any) -> bool:
if obj is None:
return False
return True
105 changes: 42 additions & 63 deletions serde/de.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def deserialize(
reuse_instances_default: bool = True,
deserializer: Optional[DeserializeFunc] = None,
tagging: Tagging = DefaultTagging,
type_check: TypeCheck = Coerce,
**kwargs,
):
"""
Expand Down Expand Up @@ -271,8 +272,8 @@ def wrap(cls: Type):
if f.deserializer:
g[f.deserializer.name] = f.deserializer

add_func(scope, FROM_ITER, render_from_iter(cls, deserializer), g)
add_func(scope, FROM_DICT, render_from_dict(cls, rename_all, deserializer), g)
add_func(scope, FROM_ITER, render_from_iter(cls, deserializer, type_check), g)
add_func(scope, FROM_DICT, render_from_dict(cls, rename_all, deserializer, type_check), g)
add_func(scope, TYPE_CHECK, render_type_check(cls), g)

logger.debug(f'{typename(cls)}: {SERDE_SCOPE} {scope}')
Expand Down Expand Up @@ -318,16 +319,11 @@ def deserialize(cls, data, **opts):
raise NotImplementedError


def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool, type_check: TypeCheck = Coerce):
def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool):
"""
Deserialize from an object into an instance of the type specified as arg `c`.
`c` can be either primitive type, `List`, `Tuple`, `Dict` or `deserialize` class.
When `type_check`=True, pyserde checks if deserialized field values are equal to the
types declared.
"""
strict = type_check.is_strict()

if is_generic(c):
# Store subscripted generic type such as Foo[Bar] in "maybe_generic",
# and store origin type such as Foo in "c". Since subscripted generics
Expand All @@ -338,17 +334,13 @@ def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool, type_check: Ty
else:
maybe_generic = c
try:
thisfunc = functools.partial(from_obj, named=named, reuse_instances=reuse_instances, type_check=type_check)
thisfunc = functools.partial(from_obj, named=named, reuse_instances=reuse_instances)
if o is None:
return None
if is_deserializable(c):
serde_scope: SerdeScope = getattr(c, SERDE_SCOPE)
func_name = FROM_DICT if named else FROM_ITER
res = serde_scope.funcs[func_name](
c, maybe_generic=maybe_generic, data=o, reuse_instances=reuse_instances, type_check=type_check
)
if strict:
serde_scope.funcs[TYPE_CHECK](res)
res = serde_scope.funcs[func_name](c, maybe_generic=maybe_generic, data=o, reuse_instances=reuse_instances)
return res
elif is_opt(c):
if o is None:
Expand All @@ -368,40 +360,25 @@ def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool, type_check: Ty
if is_bare_list(c):
return [e for e in o]
else:
res = [thisfunc(type_args(c)[0], e, type_check=type_check) for e in o]
if strict:
if not is_list_instance(res, c):
raise SerdeError(f"Failed to deserialize into {typename(c)}")
res = [thisfunc(type_args(c)[0], e) for e in o]
return res
elif is_set(c):
if is_bare_set(c):
return set(e for e in o)
else:
res = set(thisfunc(type_args(c)[0], e, type_check=NoCheck) for e in o)
if strict:
if not is_set_instance(res, c):
raise SerdeError(f"Failed to deserialize into {typename(c)}")
res = set(thisfunc(type_args(c)[0], e) for e in o)
return res
elif is_tuple(c):
if is_bare_tuple(c):
return tuple(e for e in o)
else:
res = tuple(thisfunc(type_args(c)[i], e) for i, e in enumerate(o))
if strict:
if not is_tuple_instance(res, c):
raise SerdeError(f"Failed to deserialize into {typename(c)}")
return res
elif is_dict(c):
if is_bare_dict(c):
return {k: v for k, v in o.items()}
else:
res = {
thisfunc(type_args(c)[0], k, type_check=NoCheck): thisfunc(type_args(c)[1], v, type_check=NoCheck)
for k, v in o.items()
}
if strict:
if not is_dict_instance(res, c):
raise SerdeError(f"Failed to deserialize into {typename(c)}")
res = {thisfunc(type_args(c)[0], k): thisfunc(type_args(c)[1], v) for k, v in o.items()}
return res
elif is_numpy_array(c):
return deserialize_numpy_array_direct(c, o)
Expand All @@ -419,7 +396,7 @@ def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool, type_check: Ty
raise SerdeError(e)


def from_dict(cls, o, reuse_instances: bool = ..., type_check: TypeCheck = Coerce):
def from_dict(cls, o, reuse_instances: bool = ...):
"""
Deserialize dictionary into object.
Expand All @@ -439,10 +416,10 @@ def from_dict(cls, o, reuse_instances: bool = ..., type_check: TypeCheck = Coerc
>>> from_dict(List[Foo], lst)
[Foo(i=10, s='foo', f=100.0, b=True), Foo(i=20, s='foo', f=100.0, b=True)]
"""
return from_obj(cls, o, named=True, reuse_instances=reuse_instances, type_check=type_check)
return from_obj(cls, o, named=True, reuse_instances=reuse_instances)


def from_tuple(cls, o, reuse_instances: bool = ..., type_check: TypeCheck = Coerce):
def from_tuple(cls, o, reuse_instances: bool = ...):
"""
Deserialize tuple into object.
Expand All @@ -462,7 +439,7 @@ def from_tuple(cls, o, reuse_instances: bool = ..., type_check: TypeCheck = Coer
>>> from_tuple(List[Foo], lst)
[Foo(i=10, s='foo', f=100.0, b=True), Foo(i=20, s='foo', f=100.0, b=True)]
"""
return from_obj(cls, o, named=False, reuse_instances=reuse_instances, type_check=type_check)
return from_obj(cls, o, named=False, reuse_instances=reuse_instances)


@dataclass
Expand Down Expand Up @@ -616,7 +593,7 @@ def render(self, arg: DeField) -> str:

if self.custom and not arg.deserializer:
# Rerender the code for default deserializer.
default = Renderer(self.func, self.cls, None).render(arg)
default = Renderer(self.func, self.cls, None, suppress_coerce=self.suppress_coerce).render(arg)
return self.custom_class_deserializer(arg, default)
else:
return res
Expand Down Expand Up @@ -660,13 +637,13 @@ def opt(self, arg: DeField) -> str:
>>> from typing import List
>>> Renderer('foo').render(DeField(Optional[int], 'o', datavar='data'))
'(coerce(int, data["o"], type_check)) if data.get("o") is not None else None'
'(coerce(int, data["o"])) if data.get("o") is not None else None'
>>> Renderer('foo').render(DeField(Optional[List[int]], 'o', datavar='data'))
'([coerce(int, v, type_check) for v in data["o"]]) if data.get("o") is not None else None'
'([coerce(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(int, v, type_check) for v in data["o"]]) if data.get("o") is not None else None'
'([coerce(int, v) for v in data["o"]]) if data.get("o") is not None else None'
>>> @deserialize
... class Foo:
Expand All @@ -691,10 +668,10 @@ def list(self, arg: DeField) -> str:
>>> from typing import List
>>> Renderer('foo').render(DeField(List[int], 'l', datavar='data'))
'[coerce(int, v, type_check) for v in data["l"]]'
'[coerce(int, v) for v in data["l"]]'
>>> Renderer('foo').render(DeField(List[List[int]], 'l', datavar='data'))
'[[coerce(int, v, type_check) for v in v] for v in data["l"]]'
'[[coerce(int, v) for v in v] for v in data["l"]]'
"""
if is_bare_list(arg.type):
return f'list({arg.data})'
Expand All @@ -707,10 +684,10 @@ def set(self, arg: DeField) -> str:
>>> from typing import Set
>>> Renderer('foo').render(DeField(Set[int], 'l', datavar='data'))
'set(coerce(int, v, type_check) for v in data["l"])'
'set(coerce(int, v) for v in data["l"])'
>>> Renderer('foo').render(DeField(Set[Set[int]], 'l', datavar='data'))
'set(set(coerce(int, v, type_check) for v in v) for v in data["l"])'
'set(set(coerce(int, v) for v in v) for v in data["l"])'
"""
if is_bare_set(arg.type):
return f'set({arg.data})'
Expand All @@ -725,14 +702,14 @@ def tuple(self, arg: DeField) -> str:
>>> @deserialize
... class Foo: pass
>>> Renderer('foo').render(DeField(Tuple[str, int, List[int], Foo], 'd', datavar='data'))
'(coerce(str, data["d"][0], type_check), coerce(int, data["d"][1], type_check), \
[coerce(int, v, type_check) for v in data["d"][2]], \
'(coerce(str, data["d"][0]), coerce(int, data["d"][1]), \
[coerce(int, v) for v in data["d"][2]], \
Foo.__serde__.funcs[\\'foo\\'](data=data["d"][3], reuse_instances=reuse_instances),)'
>>> field = DeField(Tuple[str, int, List[int], Foo], 'd', datavar='data', index=0, iterbased=True)
>>> Renderer('foo').render(field)
"(coerce(str, data[0][0], type_check), coerce(int, data[0][1], type_check), \
[coerce(int, v, type_check) for v in data[0][2]], Foo.__serde__.funcs['foo'](data=data[0][3], \
"(coerce(str, data[0][0]), coerce(int, data[0][1]), \
[coerce(int, v) for v in data[0][2]], Foo.__serde__.funcs['foo'](data=data[0][3], \
reuse_instances=reuse_instances),)"
"""
if is_bare_tuple(arg.type):
Expand All @@ -750,7 +727,7 @@ def dict(self, arg: DeField) -> str:
>>> from typing import List
>>> Renderer('foo').render(DeField(Dict[str, int], 'd', datavar='data'))
'{coerce(str, k, type_check): coerce(int, v, type_check) for k, v in data["d"].items()}'
'{coerce(str, k): coerce(int, v) for k, v in data["d"].items()}'
>>> @deserialize
... class Foo: pass
Expand All @@ -773,20 +750,20 @@ def primitive(self, arg: DeField, suppress_coerce: bool = False) -> str:
Render rvalue for primitives.
>>> Renderer('foo').render(DeField(int, 'i', datavar='data'))
'coerce(int, data["i"], type_check)'
'coerce(int, data["i"])'
>>> Renderer('foo').render(DeField(int, 'int_field', datavar='data', case='camelcase'))
'coerce(int, data["intField"], type_check)'
'coerce(int, data["intField"])'
>>> Renderer('foo').render(DeField(int, 'i', datavar='data', index=1, iterbased=True))
'coerce(int, data[1], type_check)'
'coerce(int, data[1])'
"""
typ = typename(arg.type)
dat = arg.data
if self.suppress_coerce:
return dat
else:
return f'coerce({typ}, {dat}, type_check)'
return f'coerce({typ}, {dat})'

def c_tor(self, arg: DeField) -> str:
return f"{typename(arg.type)}({arg.data})"
Expand Down Expand Up @@ -827,10 +804,9 @@ def to_iter_arg(f: DeField, *args, **kwargs) -> DeField:
return f


def render_from_iter(cls: Type, custom: Optional[DeserializeFunc] = None) -> str:
def render_from_iter(cls: Type, custom: Optional[DeserializeFunc] = None, type_check: TypeCheck = Coerce) -> str:
template = """
def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_scope.reuse_instances_default}},
type_check: TypeCheck=Coerce):
def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_scope.reuse_instances_default}}):
if reuse_instances is Ellipsis:
reuse_instances = {{serde_scope.reuse_instances_default}}
Expand All @@ -851,7 +827,7 @@ def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_s
raise UserError(e)
"""

renderer = Renderer(FROM_ITER, cls=cls, custom=custom)
renderer = Renderer(FROM_ITER, cls=cls, custom=custom, suppress_coerce=(not type_check.is_coerce()))
env = jinja2.Environment(loader=jinja2.DictLoader({'iter': template}))
env.filters.update({'rvalue': renderer.render})
env.filters.update({'arg': to_iter_arg})
Expand All @@ -863,11 +839,15 @@ def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_s
return res


def render_from_dict(cls: Type, rename_all: Optional[str] = None, custom: Optional[DeserializeFunc] = None) -> str:
def render_from_dict(
cls: Type,
rename_all: Optional[str] = None,
custom: Optional[DeserializeFunc] = None,
type_check: TypeCheck = Coerce,
) -> str:
template = """
def {{func}}(cls=cls, maybe_generic=None, data=None,
reuse_instances = {{serde_scope.reuse_instances_default}},
type_check: TypeCheck = Coerce):
reuse_instances = {{serde_scope.reuse_instances_default}}):
if reuse_instances is Ellipsis:
reuse_instances = {{serde_scope.reuse_instances_default}}
Expand All @@ -890,7 +870,7 @@ def {{func}}(cls=cls, maybe_generic=None, data=None,
return rv
"""

renderer = Renderer(FROM_DICT, cls=cls, custom=custom)
renderer = Renderer(FROM_DICT, cls=cls, custom=custom, suppress_coerce=(not type_check.is_coerce()))
env = jinja2.Environment(loader=jinja2.DictLoader({'dict': template}))
env.filters.update({'rvalue': renderer.render})
env.filters.update({'arg': functools.partial(to_arg, rename_all=rename_all)})
Expand All @@ -904,8 +884,7 @@ def {{func}}(cls=cls, maybe_generic=None, data=None,

def render_union_func(cls: Type, union_args: List[Type], tagging: Tagging = DefaultTagging) -> str:
template = """
def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_scope.reuse_instances_default}},
type_check: TypeCheck=Coerce):
def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_scope.reuse_instances_default}}):
errors = []
{% for t in union_args %}
try:
Expand Down
11 changes: 4 additions & 7 deletions serde/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from typing import Any, Type, Union

from .compat import T
from .core import Coerce, TypeCheck
from .de import Deserializer, from_dict
from .numpy import encode_numpy
from .se import Serializer, to_dict
Expand Down Expand Up @@ -50,7 +49,7 @@ def deserialize(cls, s: Union[bytes, str], **opts):
return json_loads(s, **opts)


def to_json(obj: Any, se: Type[Serializer] = JsonSerializer, type_check: TypeCheck = Coerce, **opts) -> str:
def to_json(obj: Any, se: Type[Serializer] = JsonSerializer, **opts) -> str:
"""
Serialize the object into JSON str. [orjson](https://github.com/ijl/orjson) will be used if installed.
Expand All @@ -61,12 +60,10 @@ def to_json(obj: Any, se: Type[Serializer] = JsonSerializer, type_check: TypeChe
If you want to use another json package, you can subclass `JsonSerializer` and implement your own logic.
"""
return se.serialize(to_dict(obj, reuse_instances=False, convert_sets=True, type_check=type_check), **opts)
return se.serialize(to_dict(obj, reuse_instances=False, convert_sets=True), **opts)


def from_json(
c: Type[T], s: Union[str, bytes], de: Type[Deserializer] = JsonDeserializer, type_check: TypeCheck = Coerce, **opts
) -> T:
def from_json(c: Type[T], s: Union[str, bytes], de: Type[Deserializer] = JsonDeserializer, **opts) -> T:
"""
Deserialize from JSON into the object. [orjson](https://github.com/ijl/orjson) will be used if installed.
Expand All @@ -75,4 +72,4 @@ def from_json(
If you want to use another json package, you can subclass `JsonDeserializer` and implement your own logic.
"""
return from_dict(c, de.deserialize(s, **opts), reuse_instances=False, type_check=type_check)
return from_dict(c, de.deserialize(s, **opts), reuse_instances=False)
Loading

0 comments on commit fbe8dc8

Please sign in to comment.