Skip to content

Commit

Permalink
Merge pull request #286 from yukinarit/defaultdict
Browse files Browse the repository at this point in the history
feat: Support DefaultDict
  • Loading branch information
yukinarit authored Nov 29, 2022
2 parents c81d1cd + d329f0c commit de087ab
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 7 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Foo(i=10, s='foo', f=100.0, b=True)
- Primitives (`int`, `float`, `str`, `bool`)
- Containers
- `List`, `Set`, `Tuple`, `Dict`
- `FrozenSet`
- [`FrozenSet`](https://docs.python.org/3/library/stdtypes.html#frozenset), [`DefaultDict`](https://docs.python.org/3/library/collections.html#collections.defaultdict)
- [`typing.Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)
- [`typing.Union`](https://docs.python.org/3/library/typing.html#typing.Union)
- User defined class with [`@dataclass`](https://docs.python.org/3/library/dataclasses.html)
Expand Down
39 changes: 39 additions & 0 deletions examples/default_dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from collections import defaultdict
from dataclasses import dataclass
from typing import DefaultDict, List, Optional

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


@serde
@dataclass
class Bar:
v: Optional[int] = None


@serde
@dataclass
class Foo:
a: DefaultDict[str, List[int]]
b: DefaultDict[str, int]
c: DefaultDict[str, Bar]


def main():
a = defaultdict(list)
a["a"].append(1)
b = defaultdict(int)
b["b"]
c = defaultdict(Bar)
c["c"].v = 10

f = Foo(a, b, c)
print(f"Into Json: {to_json(f)}")

s = '{"a": {"a": [1, 2]}, "b": {"b": 10}, "c": {"c": {}}}'
print(f"From Json: {from_json(Foo, s)}")


if __name__ == '__main__':
main()
23 changes: 23 additions & 0 deletions examples/frozen_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from dataclasses import dataclass
from typing import FrozenSet

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


@serde
@dataclass
class Foo:
i: FrozenSet[int]


def main():
f = Foo(i={1, 2})
print(f"Into Json: {to_json(f)}")

s = '{"i": [1, 2]}'
print(f"From Json: {from_json(Foo, s)}")


if __name__ == '__main__':
main()
4 changes: 4 additions & 0 deletions examples/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import custom_class_serializer
import custom_field_serializer
import default
import default_dict
import ellipsis
import env
import flatten
import forward_reference
import frozen_set
import generics
import generics_nested
import init_var
Expand Down Expand Up @@ -39,9 +41,11 @@
def run_all():
run(any)
run(simple)
run(frozen_set)
run(newtype)
run(collection)
run(default)
run(default_dict)
run(env)
run(flatten)
run(jsonfile)
Expand Down
25 changes: 22 additions & 3 deletions serde/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
import types
import typing
import uuid
from collections import defaultdict
from dataclasses import is_dataclass
from typing import Any, Dict, FrozenSet, Generic, Iterator, List, Optional, Set, Tuple, TypeVar, Union
from typing import Any, DefaultDict, Dict, FrozenSet, Generic, Iterator, List, Optional, Set, Tuple, TypeVar, Union

import typing_inspect
from typing_extensions import Type
Expand Down Expand Up @@ -580,11 +581,13 @@ def is_dict(typ: Type[Any]) -> bool:
True
>>> is_dict(Dict)
True
>>> is_dict(DefaultDict[int, int])
True
"""
try:
return issubclass(get_origin(typ), dict) # type: ignore
return issubclass(get_origin(typ), (dict, defaultdict)) # type: ignore
except TypeError:
return typ in (Dict, dict)
return typ in (Dict, dict, DefaultDict, defaultdict)


def is_bare_dict(typ: Type[Any]) -> bool:
Expand All @@ -600,6 +603,22 @@ def is_bare_dict(typ: Type[Any]) -> bool:
return typ in (Dict, dict)


def is_default_dict(typ: Type[Any]) -> bool:
"""
Test if the type is `typing.DefaultDict`.
>>> from typing import Dict
>>> is_default_dict(DefaultDict[int, int])
True
>>> is_default_dict(Dict[int, int])
False
"""
try:
return issubclass(get_origin(typ), defaultdict) # type: ignore
except TypeError:
return typ in (DefaultDict, defaultdict)


def is_none(typ: Type[Any]) -> bool:
"""
>>> is_none(int)
Expand Down
26 changes: 25 additions & 1 deletion serde/de.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import abc
import collections
import dataclasses
import functools
import typing
Expand All @@ -29,6 +30,7 @@
is_bare_set,
is_bare_tuple,
is_datetime,
is_default_dict,
is_dict,
is_enum,
is_frozen_set,
Expand Down Expand Up @@ -236,6 +238,7 @@ def wrap(cls: Type):
g['typename'] = typename # used in union functions
g['ensure'] = ensure
g['typing'] = typing
g['collections'] = collections
g['Literal'] = Literal
g['from_obj'] = from_obj
g['get_generic_arg'] = get_generic_arg
Expand Down Expand Up @@ -388,9 +391,17 @@ def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool):
elif is_dict(c):
if is_bare_dict(c):
return {k: v for k, v in o.items()}
elif is_default_dict(c):
f = DeField(c, "")
v = f.value_field()
origin = get_origin(v.type)
res = collections.defaultdict(
origin if origin else v.type,
{thisfunc(type_args(c)[0], k): thisfunc(type_args(c)[1], v) for k, v in o.items()},
)
else:
res = {thisfunc(type_args(c)[0], k): thisfunc(type_args(c)[1], v) for k, v in o.items()}
return res
return res
elif is_numpy_array(c):
return deserialize_numpy_array_direct(c, o)
elif is_datetime(c):
Expand Down Expand Up @@ -748,6 +759,19 @@ def dict(self, arg: DeField) -> str:
"""
if is_bare_dict(arg.type):
return arg.data
elif is_default_dict(arg.type):
k = arg.key_field()
v = arg.value_field()
origin = get_origin(v.type)
if origin:
# When the callable type is of generic type e.g List.
# Get origin type "list" from "List[X]".
callable = origin.__name__
else:
# When the callable type is non generic type e.g int, Foo.
callable = v.type.__name__
return f'collections.defaultdict({callable}, \
{{{self.render(k)}: {self.render(v)} for k, v in {arg.data}.items()}})'
else:
k = arg.key_field()
v = arg.value_field()
Expand Down
6 changes: 5 additions & 1 deletion tests/common.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import collections
import datetime
import decimal
import ipaddress
Expand All @@ -6,7 +7,7 @@
import pathlib
import sys
import uuid
from typing import Any, Callable, Dict, FrozenSet, Generic, List, NewType, Optional, Set, Tuple, TypeVar
from typing import Any, Callable, DefaultDict, Dict, FrozenSet, Generic, List, NewType, Optional, Set, Tuple, TypeVar

import more_itertools

Expand Down Expand Up @@ -85,6 +86,9 @@ def toml_not_supported(se, de, opt) -> bool:
param({'a': 1}, Dict),
param({'a': 1}, dict),
param({}, Dict[str, int]),
param({'a': 1}, Dict[str, int]),
param({'a': 1}, DefaultDict[str, int]),
param({'a': [1]}, DefaultDict[str, List[int]]),
param(data.Pri(10, 'foo', 100.0, True), data.Pri), # dataclass
param(data.Pri(10, 'foo', 100.0, True), Optional[data.Pri]),
param(None, Optional[data.Pri], toml_not_supported),
Expand Down
31 changes: 30 additions & 1 deletion tests/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
import pathlib
import uuid
from typing import Dict, FrozenSet, List, Optional, Set, Tuple, Union
from typing import DefaultDict, Dict, FrozenSet, List, Optional, Set, Tuple, Union

import pytest

Expand Down Expand Up @@ -932,3 +932,32 @@ class Foo:

fs = serde.json.from_json(FrozenSet[int], '[1,2]')
assert fs == frozenset([1, 2])


def test_defaultdict() -> None:
from collections import defaultdict

@serde.serde
@dataclasses.dataclass
class Foo:
v: DefaultDict[str, List[int]]

f = Foo(defaultdict(list, {"k": [1, 2]}))
assert '{"v":{"k":[1,2]}}' == serde.json.to_json(f)

ff = serde.json.from_json(Foo, '{"v":{"k":[1,2]}}')
assert f == ff
assert isinstance(f.v, defaultdict)
assert isinstance(ff.v, defaultdict)

dd = serde.json.from_json(DefaultDict[str, List[int]], '{"k":[1,2]}')
assert isinstance(dd, defaultdict)
assert dd == defaultdict(list, {"k": [1, 2]})


def test_defaultdict_invalid_value_type() -> None:
with pytest.raises(Exception):
serde.json.from_json(DefaultDict[str, ...], '{"k":[1,2]}')

with pytest.raises(Exception):
serde.json.from_json(DefaultDict, '{"k":[1,2]}')

0 comments on commit de087ab

Please sign in to comment.