Repository: yukinarit/pyserde
Pull request #285 (2022-11-27 23:48:24)

feat: Support FrozenSet
#83 


Patch:
diff --git a/README.md b/README.md
index e703e7f..45b4eba 100644
--- a/README.md
+++ b/README.md
@@ -47,12 +47,15 @@ Foo(i=10, s='foo', f=100.0, b=True)
 	- MsgPack
 - Supported types
     - Primitives (`int`, `float`, `str`, `bool`)
-    - Containers (`List`, `Set`, `Tuple`, `Dict`)
+    - Containers
+        - `List`, `Set`, `Tuple`, `Dict`
+        - `FrozenSet`
     - [`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)
     - [`typing.NewType`](https://docs.python.org/3/library/typing.html#newtype) for primitive types
     - [`typing.Any`](https://docs.python.org/3/library/typing.html#the-any-type)
+    - [`typing.Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)
     - [`typing.Generic`](https://docs.python.org/3/library/typing.html#user-defined-generic-types)
     - [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum) and [`IntEnum`](https://docs.python.org/3/library/enum.html#enum.IntEnum)
     - Standard library
diff --git a/docs/supported-types.md b/docs/supported-types.md
index 93546ed..a60d5e5 100644
--- a/docs/supported-types.md
+++ b/docs/supported-types.md
@@ -1,13 +1,16 @@
 # Supported types
 
 * Primitives (int, float, str, bool)
-* Containers (List, Set, Tuple, Dict)
+* Containers
+    * List, Set, Tuple, Dict
+    * FrozenSet
 * [`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)
 * [`typing.NewType`](https://docs.python.org/3/library/typing.html#newtype) for primitive types
 * [`typing.Any`](https://docs.python.org/3/library/typing.html#the-any-type)
 * [`typing.Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)
+* [`typing.Generic`](https://docs.python.org/3/library/typing.html#user-defined-generic-types)
 * [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum) and [`IntEnum`](https://docs.python.org/3/library/enum.html#enum.IntEnum)
 * Standard library
     * [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html)
diff --git a/serde/compat.py b/serde/compat.py
index d50d0d6..67daf8c 100644
--- a/serde/compat.py
+++ b/serde/compat.py
@@ -14,7 +14,7 @@ import types
 import typing
 import uuid
 from dataclasses import is_dataclass
-from typing import Any, Dict, Generic, Iterator, List, Optional, Set, Tuple, TypeVar, Union
+from typing import Any, Dict, FrozenSet, Generic, Iterator, List, Optional, Set, Tuple, TypeVar, Union
 
 import typing_inspect
 from typing_extensions import Type
@@ -526,18 +526,20 @@ def is_bare_tuple(typ: Type[Any]) -> bool:
 
 def is_set(typ: Type[Any]) -> bool:
     """
-    Test if the type is `typing.Set`.
+    Test if the type is `typing.Set` or `typing.FrozenSet`.
 
     >>> from typing import Set
     >>> is_set(Set[int])
     True
     >>> is_set(Set)
     True
+    >>> is_set(FrozenSet[int])
+    True
     """
     try:
-        return issubclass(get_origin(typ), set)  # type: ignore
+        return issubclass(get_origin(typ), (set, frozenset))  # type: ignore
     except TypeError:
-        return typ in (Set, set)
+        return typ in (Set, set, FrozenSet, frozenset)
 
 
 def is_bare_set(typ: Type[Any]) -> bool:
@@ -553,6 +555,22 @@ def is_bare_set(typ: Type[Any]) -> bool:
     return typ in (Set, set)
 
 
+def is_frozen_set(typ: Type[Any]) -> bool:
+    """
+    Test if the type is `typing.FrozenSet`.
+
+    >>> from typing import Set
+    >>> is_frozen_set(FrozenSet[int])
+    True
+    >>> is_frozen_set(Set)
+    False
+    """
+    try:
+        return issubclass(get_origin(typ), frozenset)  # type: ignore
+    except TypeError:
+        return typ in (FrozenSet, frozenset)
+
+
 def is_dict(typ: Type[Any]) -> bool:
     """
     Test if the type is `typing.Dict`.
diff --git a/serde/de.py b/serde/de.py
index 6e6d583..26f4846 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -31,6 +31,7 @@ from .compat import (
     is_datetime,
     is_dict,
     is_enum,
+    is_frozen_set,
     is_generic,
     is_list,
     is_literal,
@@ -372,6 +373,9 @@ def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool):
         elif is_set(c):
             if is_bare_set(c):
                 return set(e for e in o)
+            elif is_frozen_set(c):
+                res = frozenset(thisfunc(type_args(c)[0], e) for e in o)
+                return res
             else:
                 res = set(thisfunc(type_args(c)[0], e) for e in o)
                 return res
@@ -695,6 +699,8 @@ reuse_instances=reuse_instances)) if data.get("f") is not None else None'
         """
         if is_bare_set(arg.type):
             return f'set({arg.data})'
+        elif is_frozen_set(arg.type):
+            return f'frozenset({self.render(arg[0])} for v in {arg.data})'
         else:
             return f'set({self.render(arg[0])} for v in {arg.data})'
 


Test patch:
diff --git a/tests/common.py b/tests/common.py
index d65592d..158b908 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -6,7 +6,7 @@ import os
 import pathlib
 import sys
 import uuid
-from typing import Any, Callable, Dict, Generic, List, NewType, Optional, Set, Tuple, TypeVar
+from typing import Any, Callable, Dict, FrozenSet, Generic, List, NewType, Optional, Set, Tuple, TypeVar
 
 import more_itertools
 
@@ -77,6 +77,7 @@ types: List = [
     param({1, 2}, Set, toml_not_supported),
     param({1, 2}, set, toml_not_supported),
     param(set(), Set[int], toml_not_supported),
+    param({1, 2}, FrozenSet[int], toml_not_supported),
     param((1, 1), Tuple[int, int]),
     param((1, 1), Tuple),
     param((1, 1), Tuple[int, ...]),
diff --git a/tests/test_basics.py b/tests/test_basics.py
index 7e47518..dfde95b 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -4,7 +4,7 @@ import enum
 import logging
 import pathlib
 import uuid
-from typing import Dict, List, Optional, Set, Tuple, Union
+from typing import Dict, FrozenSet, List, Optional, Set, Tuple, Union
 
 import pytest
 
@@ -914,3 +914,21 @@ def test_coerce():
 
     d = {"i": {"i": "10"}, "s": {"s": 100}, "f": {"f": 1000}, "b": {"b": "True"}}
     p = serde.from_dict(Nested, d)
+
+
+def test_frozenset() -> None:
+    @serde.serde
+    @dataclasses.dataclass
+    class Foo:
+        d: FrozenSet[int]
+
+    f = Foo(frozenset({1, 1, 2}))
+    assert '{"d":[1,2]}' == serde.json.to_json(f)
+
+    ff = serde.json.from_json(Foo, '{"d":[1,2]}')
+    assert f == ff
+    assert isinstance(f.d, frozenset)
+    assert isinstance(ff.d, frozenset)
+
+    fs = serde.json.from_json(FrozenSet[int], '[1,2]')
+    assert fs == frozenset([1, 2])