Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: if field has custom decoder, schema takes it into account #462

Merged
merged 15 commits into from
Aug 7, 2023
35 changes: 25 additions & 10 deletions dataclasses_json/mm.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# flake8: noqa

import dataclasses
import typing
import warnings
import sys
Expand Down Expand Up @@ -116,6 +116,7 @@ class _TupleVarLen(fields.List):
"""
variable-length homogeneous tuples
"""

def _deserialize(self, value, attr, data, **kwargs):
optional_list = super()._deserialize(value, attr, data, **kwargs)
return None if optional_list is None else tuple(optional_list)
Expand Down Expand Up @@ -162,15 +163,16 @@ def __init__(self, *args, **kwargs):
raise NotImplementedError()

@typing.overload
def dump(self, obj: typing.List[A], many: typing.Optional[bool] = None) -> typing.List[TEncoded]: # type: ignore
def dump(self, obj: typing.List[A], # type: ignore
deansg marked this conversation as resolved.
Show resolved Hide resolved
many: typing.Optional[bool] = None) -> typing.List[TEncoded]:
# mm has the wrong return type annotation (dict) so we can ignore the mypy error
pass

@typing.overload
def dump(self, obj: A, many: typing.Optional[bool] = None) -> TEncoded:
pass

def dump(self, obj: TOneOrMulti, # type: ignore
def dump(self, obj: TOneOrMulti, # type: ignore
many: typing.Optional[bool] = None) -> TOneOrMultiEncoded:
pass

Expand All @@ -183,7 +185,7 @@ def dumps(self, obj: typing.List[A], many: typing.Optional[bool] = None, *args,
def dumps(self, obj: A, many: typing.Optional[bool] = None, *args, **kwargs) -> str:
pass

def dumps(self, obj: TOneOrMulti, many: typing.Optional[bool] = None, *args, # type: ignore
def dumps(self, obj: TOneOrMulti, many: typing.Optional[bool] = None, *args, # type: ignore
**kwargs) -> str:
pass

Expand All @@ -208,16 +210,16 @@ def load(self, data: TOneOrMultiEncoded,

@typing.overload # type: ignore
def loads(self, json_data: JsonData, # type: ignore
many: typing.Optional[bool] = True, partial: typing.Optional[bool] = None, unknown: typing.Optional[str] = None,
**kwargs) -> typing.List[A]:
many: typing.Optional[bool] = True, partial: typing.Optional[bool] = None,
unknown: typing.Optional[str] = None, **kwargs) -> typing.List[A]:
# ignore the mypy error of the decorator because mm does not define bytes as correct input data
# mm has the wrong return type annotation (dict) so we can ignore the mypy error
# for the return type overlap
pass

def loads(self, json_data: JsonData,
many: typing.Optional[bool] = None, partial: typing.Optional[bool] = None, unknown: typing.Optional[str] = None,
**kwargs) -> TOneOrMulti:
many: typing.Optional[bool] = None, partial: typing.Optional[bool] = None,
unknown: typing.Optional[str] = None, **kwargs) -> TOneOrMulti:
pass


Expand Down Expand Up @@ -253,10 +255,10 @@ def inner(type_, options):
origin = getattr(type_, '__origin__', type_)
args = [inner(a, {}) for a in getattr(type_, '__args__', []) if
a is not type(None)]

if type_ == Ellipsis:
return type_

if _is_optional(type_):
options["allow_none"] = True
if origin is tuple:
Expand Down Expand Up @@ -318,13 +320,26 @@ def schema(cls, mixin, infer_missing):
options['data_key'] = metadata.letter_case(field.name)

t = build_type(type_, options, mixin, field, cls)
_account_for_decoder_if_necessary(field, t)
deansg marked this conversation as resolved.
Show resolved Hide resolved

# if type(t) is not fields.Field: # If we use `isinstance` we would return nothing.
if field.type != typing.Optional[CatchAllVar]:
schema[field.name] = t

return schema


def _account_for_decoder_if_necessary(field: dataclasses.Field, t: fields.Field):
decoder = field.metadata.get('dataclasses_json', {}).get('decoder')
if decoder:
# Used in order to avoid mypy error. See https://github.com/python/mypy/issues/2427 for more details
t._deserialize = _dummy_deserialize # type: ignore


def _dummy_deserialize(value: typing.Any, *_args, **_kwargs):
return value


deansg marked this conversation as resolved.
Show resolved Hide resolved
def build_schema(cls: typing.Type[A],
mixin,
infer_missing,
Expand Down
10 changes: 10 additions & 0 deletions tests/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,16 @@ class DataClassWithErroneousDecode:
id: float = field(metadata=config(decoder=lambda: None))


def split_str(data: str, *_args, **_kwargs):
return data.split(',')


@dataclass_json
@dataclass
class DataClassDifferentTypeDecode:
lst: List[str] = field(default=None, metadata=config(decoder=split_str))


@dataclass_json
@dataclass
class DataClassMappingBadDecode:
Expand Down
7 changes: 6 additions & 1 deletion tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import pytest

from .entities import (DataClassDefaultListStr, DataClassDefaultOptionalList, DataClassList, DataClassOptional,
DataClassWithNestedOptional, DataClassWithNestedOptionalAny, DataClassWithNestedAny)
DataClassWithNestedOptional, DataClassWithNestedOptionalAny, DataClassWithNestedAny,
DataClassDifferentTypeDecode)
from .test_letter_case import CamelCasePerson, KebabCasePerson, SnakeCasePerson, FieldNamePerson

test_do_list = """[{}, {"children": [{"name": "a"}, {"name": "b"}]}]"""
Expand Down Expand Up @@ -47,3 +48,7 @@ def test_nested_optional_any(self):
def test_nested_any_accepts_optional(self):
DataClassWithNestedAny.schema().loads(nested_optional_data)
assert True

def test_accounts_for_decode(self):
assert DataClassDifferentTypeDecode.schema().load({'lst': '1,2,3'}) == \
DataClassDifferentTypeDecode(lst=['1', '2', '3'])