Skip to content

Commit c72cd5c

Browse files
authored
Support collectons abc (#685)
* Support Sequence and MutableSequence * Support Mapping and MutableMapping * Support Set, MutableSet * Update documents for collections.abc suppport
1 parent 08846e8 commit c72cd5c

File tree

19 files changed

+371
-32
lines changed

19 files changed

+371
-32
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ Happy coding with pyserde! 🚀
6565
- Supported types
6666
- Primitives (`int`, `float`, `str`, `bool`)
6767
- Containers
68-
- `list`, `set`, `tuple`, `dict`
68+
- `list`, `collections.abc.Sequence`, `collections.abc.MutableSequence`, `tuple`
69+
- `set`, `collections.abc.Set`, `collections.abc.MutableSet`
70+
- `dict`, `collections.abc.Mapping`, `collections.abc.MutableMapping`
6971
- [`frozenset`](https://docs.python.org/3/library/stdtypes.html#frozenset), [`defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict)
7072
- [`typing.Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)
7173
- [`typing.Union`](https://docs.python.org/3/library/typing.html#typing.Union)

docs/en/types.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ Here is the list of the supported types. See the simple example for each type in
44

55
* Primitives (int, float, str, bool) [^1]
66
* Containers
7-
* `list`, `set`, `tuple`, `dict` [^2]
7+
* `list`, `collections.abc.Sequence`, `collections.abc.MutableSequence`, `tuple` [^2]
8+
* `set`, `collections.abc.Set`, `collections.abc.MutableSet` [^2]
9+
* `dict`, `collections.abc.Mapping`, `collections.abc.MutableMapping` [^2]
810
* [`frozenset`](https://docs.python.org/3/library/stdtypes.html#frozenset), [^3]
911
* [`defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict) [^4]
1012
* [`typing.Optional`](https://docs.python.org/3/library/typing.html#typing.Optional) [^5]

docs/ja/types.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
* プリミティブ(`int`, `float`, `str`, `bool`[^1]
66
* コンテナ
7-
* `list`, `set`, `tuple`, `dict` [^2]
7+
* `list`, `collections.abc.Sequence`, `collections.abc.MutableSequence`, `tuple` [^2]
8+
* `set`, `collections.abc.Set`, `collections.abc.MutableSet` [^2]
9+
* `dict`, `collections.abc.Mapping`, `collections.abc.MutableMapping` [^2]
810
* [`frozenset`](https://docs.python.org/3/library/stdtypes.html#frozenset) [^3]
911
* [`defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict) [^4]
1012
* [`typing.Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[^5]

examples/mapping.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from collections import UserDict
2+
from collections.abc import Mapping, MutableMapping
3+
from types import MappingProxyType
4+
5+
from serde import serde
6+
from serde.json import from_json, to_json
7+
8+
9+
@serde
10+
class Foo:
11+
m: Mapping[str, int]
12+
mm: MutableMapping[str, int]
13+
14+
15+
def main() -> None:
16+
proxy: Mapping[str, int] = MappingProxyType({"a": 1, "b": 2})
17+
userdict: MutableMapping[str, int] = UserDict({"c": 3, "d": 4})
18+
19+
foo = Foo(m=proxy, mm=userdict)
20+
print(f"Into Json: {to_json(foo)}") # -> {"m":{"a":1,"b":2},"mm":{"c":3,"d":4}}
21+
22+
s = '{"m":{"a":1,"b":2},"mm":{"c":3,"d":4}}'
23+
print(f"From Json: {from_json(Foo, s)}") # -> Foo(m={'a': 1, 'b': 2}, mm={'c': 3, 'd': 4})
24+
25+
26+
if __name__ == "__main__":
27+
main()

examples/runner.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ def run_all() -> None:
1313
import class_var
1414
import primitive_subclass
1515
import collection
16+
import mapping
17+
import sequence
18+
import set_abc
1619
import custom_class_serializer
1720
import custom_legacy_class_serializer
1821
import custom_field_serializer
@@ -70,6 +73,9 @@ def run_all() -> None:
7073
run(frozen_set)
7174
run(newtype)
7275
run(collection)
76+
run(mapping)
77+
run(sequence)
78+
run(set_abc)
7379
run(default)
7480
run(default_dict)
7581
run(env)

examples/sequence.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from collections.abc import Sequence
2+
3+
from serde import serde
4+
from serde.json import from_json, to_json
5+
6+
7+
@serde
8+
class Foo:
9+
xs: Sequence[int]
10+
11+
12+
def main() -> None:
13+
# Any non-string Sequence works (e.g. tuple, list).
14+
foo = Foo(xs=(1, 2, 3))
15+
print(f"Into Json: {to_json(foo)}") # -> {"xs":[1,2,3]}
16+
17+
s = '{"xs":[1,2,3]}'
18+
print(f"From Json: {from_json(Foo, s)}") # -> Foo(xs=[1, 2, 3])
19+
20+
21+
if __name__ == "__main__":
22+
main()

examples/set_abc.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from collections.abc import MutableSet, Set
2+
3+
from serde import serde
4+
from serde.json import from_json, to_json
5+
6+
7+
@serde
8+
class Foo:
9+
s: Set[int]
10+
ms: MutableSet[int]
11+
12+
13+
def main() -> None:
14+
f = Foo(s=frozenset({1, 2}), ms=set({3, 4}))
15+
print(f"Into Json: {to_json(f)}")
16+
17+
s = '{"s":[1,2],"ms":[3,4]}'
18+
print(f"From Json: {from_json(Foo, s)}")
19+
20+
21+
if __name__ == "__main__":
22+
main()

serde/compat.py

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
import typing
1616
import typing_extensions
1717
from collections import defaultdict
18-
from collections.abc import Iterator
18+
from collections.abc import Iterator, Sequence, MutableSequence
19+
from collections.abc import Mapping, MutableMapping, Set, MutableSet
1920
from dataclasses import is_dataclass
2021
from typing import TypeVar, Generic, Any, ClassVar, Optional, NewType, Union, Hashable, Callable
2122

@@ -563,30 +564,50 @@ def is_opt_dataclass(typ: Any) -> bool:
563564
@cache
564565
def is_list(typ: type[Any]) -> bool:
565566
"""
566-
Test if the type is `list`.
567+
Test if the type is `list`, `collections.abc.Sequence`, or `collections.abc.MutableSequence`.
567568
568569
>>> is_list(list[int])
569570
True
570571
>>> is_list(list)
571572
True
573+
>>> is_list(Sequence[int])
574+
True
575+
>>> is_list(Sequence)
576+
True
577+
>>> is_list(MutableSequence[int])
578+
True
579+
>>> is_list(MutableSequence)
580+
True
572581
"""
573-
try:
574-
return issubclass(get_origin(typ), list) # type: ignore
575-
except TypeError:
576-
return typ is list
582+
origin = get_origin(typ)
583+
if origin is None:
584+
return typ in (list, Sequence, MutableSequence)
585+
return origin in (list, Sequence, MutableSequence)
577586

578587

579588
@cache
580589
def is_bare_list(typ: type[Any]) -> bool:
581590
"""
582-
Test if the type is `list` without type args.
591+
Test if the type is `list`/`collections.abc.Sequence`/`collections.abc.MutableSequence`
592+
without type args.
583593
584594
>>> is_bare_list(list[int])
585595
False
586596
>>> is_bare_list(list)
587597
True
598+
>>> is_bare_list(Sequence[int])
599+
False
600+
>>> is_bare_list(Sequence)
601+
True
602+
>>> is_bare_list(MutableSequence[int])
603+
False
604+
>>> is_bare_list(MutableSequence)
605+
True
588606
"""
589-
return typ is list
607+
origin = get_origin(typ)
608+
if origin in (list, Sequence, MutableSequence):
609+
return not type_args(typ)
610+
return typ in (list, Sequence, MutableSequence)
590611

591612

592613
@cache
@@ -633,32 +654,49 @@ def is_variable_tuple(typ: type[Any]) -> bool:
633654
@cache
634655
def is_set(typ: type[Any]) -> bool:
635656
"""
636-
Test if the type is `set` or `frozenset`.
657+
Test if the type is set-like.
637658
638659
>>> is_set(set[int])
639660
True
640661
>>> is_set(set)
641662
True
642663
>>> is_set(frozenset[int])
643664
True
665+
>>> from collections.abc import Set, MutableSet
666+
>>> is_set(Set[int])
667+
True
668+
>>> is_set(Set)
669+
True
670+
>>> is_set(MutableSet[int])
671+
True
672+
>>> is_set(MutableSet)
673+
True
644674
"""
645675
try:
646-
return issubclass(get_origin(typ), (set, frozenset)) # type: ignore
676+
return issubclass(get_origin(typ), (set, frozenset, Set, MutableSet)) # type: ignore[arg-type]
647677
except TypeError:
648-
return typ in (set, frozenset)
678+
return typ in (set, frozenset, Set, MutableSet)
649679

650680

651681
@cache
652682
def is_bare_set(typ: type[Any]) -> bool:
653683
"""
654-
Test if the type is `set` without type args.
684+
Test if the type is `set`/`frozenset`/`Set`/`MutableSet` without type args.
655685
656686
>>> is_bare_set(set[int])
657687
False
658688
>>> is_bare_set(set)
659689
True
690+
>>> from collections.abc import Set, MutableSet
691+
>>> is_bare_set(Set)
692+
True
693+
>>> is_bare_set(MutableSet)
694+
True
660695
"""
661-
return typ in (set, frozenset)
696+
origin = get_origin(typ)
697+
if origin in (set, frozenset, Set, MutableSet):
698+
return not type_args(typ)
699+
return typ in (set, frozenset, Set, MutableSet)
662700

663701

664702
@cache
@@ -680,32 +718,52 @@ def is_frozen_set(typ: type[Any]) -> bool:
680718
@cache
681719
def is_dict(typ: type[Any]) -> bool:
682720
"""
683-
Test if the type is dict.
721+
Test if the type is dict-like.
684722
685723
>>> is_dict(dict[int, int])
686724
True
687725
>>> is_dict(dict)
688726
True
689727
>>> is_dict(defaultdict[int, int])
690728
True
729+
>>> from collections.abc import Mapping, MutableMapping
730+
>>> is_dict(Mapping[str, int])
731+
True
732+
>>> is_dict(Mapping)
733+
True
734+
>>> is_dict(MutableMapping[str, int])
735+
True
736+
>>> is_dict(MutableMapping)
737+
True
691738
"""
692739
try:
693-
return issubclass(get_origin(typ), (dict, defaultdict)) # type: ignore
740+
return issubclass(
741+
get_origin(typ), (dict, defaultdict, Mapping, MutableMapping) # type: ignore[arg-type]
742+
)
694743
except TypeError:
695-
return typ in (dict, defaultdict)
744+
return typ in (dict, defaultdict, Mapping, MutableMapping)
696745

697746

747+
@cache
698748
@cache
699749
def is_bare_dict(typ: type[Any]) -> bool:
700750
"""
701-
Test if the type is `dict` without type args.
751+
Test if the type is `dict`/`Mapping`/`MutableMapping` without type args.
702752
703753
>>> is_bare_dict(dict[int, str])
704754
False
705755
>>> is_bare_dict(dict)
706756
True
757+
>>> from collections.abc import Mapping, MutableMapping
758+
>>> is_bare_dict(Mapping)
759+
True
760+
>>> is_bare_dict(MutableMapping)
761+
True
707762
"""
708-
return typ is dict
763+
origin = get_origin(typ)
764+
if origin in (dict, Mapping, MutableMapping):
765+
return not type_args(typ)
766+
return typ in (dict, Mapping, MutableMapping)
709767

710768

711769
@cache

serde/core.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from dataclasses import dataclass
1313

1414
from beartype.door import is_bearable
15-
from collections.abc import Mapping, Sequence, Callable, Hashable
15+
from collections.abc import Mapping, Sequence, MutableSequence, Set, Callable, Hashable
1616
from typing import (
1717
overload,
1818
TypeVar,
@@ -403,8 +403,20 @@ def is_union_instance(obj: Any, typ: type[Any]) -> bool:
403403

404404

405405
def is_list_instance(obj: Any, typ: type[Any]) -> bool:
406-
if not isinstance(obj, list):
407-
return False
406+
origin = get_origin(typ) or typ
407+
if origin is list:
408+
if not isinstance(obj, list):
409+
return False
410+
elif origin is MutableSequence:
411+
if isinstance(obj, (str, bytes, bytearray)):
412+
return False
413+
if not isinstance(obj, MutableSequence):
414+
return False
415+
else:
416+
if isinstance(obj, (str, bytes, bytearray)):
417+
return False
418+
if not isinstance(obj, Sequence):
419+
return False
408420
if len(obj) == 0 or is_bare_list(typ):
409421
return True
410422
list_arg = type_args(typ)[0]
@@ -413,7 +425,7 @@ def is_list_instance(obj: Any, typ: type[Any]) -> bool:
413425

414426

415427
def is_set_instance(obj: Any, typ: type[Any]) -> bool:
416-
if not isinstance(obj, (set, frozenset)):
428+
if not isinstance(obj, Set):
417429
return False
418430
if len(obj) == 0 or is_bare_set(typ):
419431
return True
@@ -458,7 +470,7 @@ def is_tuple_instance(obj: Any, typ: type[Any]) -> bool:
458470

459471

460472
def is_dict_instance(obj: Any, typ: type[Any]) -> bool:
461-
if not isinstance(obj, dict):
473+
if not isinstance(obj, Mapping):
462474
return False
463475
if len(obj) == 0 or is_bare_dict(typ):
464476
return True

serde/de.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ def __getitem__(self, n: int) -> DeField[Any] | InnerField[Any]:
690690
"flatten": self.flatten,
691691
"parent": self.parent,
692692
}
693-
if is_list(self.type) or is_dict(self.type) or is_set(self.type):
693+
if is_list(self.type) or is_set(self.type) or is_dict(self.type):
694694
return InnerField(typ, "v", datavar="v", **opts)
695695
elif is_tuple(self.type):
696696
return InnerField(typ, f"{self.data}[{n}]", datavar=f"{self.data}[{n}]", **opts)

0 commit comments

Comments
 (0)