Repository: yukinarit/pyserde (python)

Issue:
Switch toml library to correctly handle Toml v1 standard
Please consider changing the current "toml" library dependency. It is not fully toml  v1 compliant. See: https://github.com/toml-lang/toml/wiki. 
I would recommend:
https://github.com/samuelcolvin/rtoml

Pull request:
Tomli
Closes #154 

Patch:
diff --git a/examples/pyproject.toml b/examples/pyproject.toml
index 3c7c673..72cba13 100644
--- a/examples/pyproject.toml
+++ b/examples/pyproject.toml
@@ -6,7 +6,6 @@ authors = ["yukinarit <yukinarit84@gmail.com>"]
 
 [tool.poetry.dependencies]
 python = "^3.7.0"
-pyserde = {path = "../"}
-toml = "*"
+pyserde = {path = "../", extras = ["all"]}
 requests = "*"
 envclasses = "^0.2.1"
diff --git a/pyproject.toml b/pyproject.toml
index 0c2dfb8..5fa7d6d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,7 +29,8 @@ typing_extensions = { version = "*", markers = "python_version ~= '3.7.0'" }
 casefy = "*"
 jinja2 = "*"
 msgpack = { version = "*", markers = "extra == 'msgpack' or extra == 'all'", optional = true }
-toml = { version = "*", markers = "extra == 'toml' or extra == 'all'", optional = true }
+tomli = { version = "*", markers = "extra == 'toml' or extra == 'all'", optional = true }
+tomli-w = { version = "*", markers = "extra == 'toml' or extra == 'all'", optional = true }
 pyyaml = { version = "*", markers = "extra == 'yaml' or extra == 'all'", optional = true }
 numpy = [
     { version = "~1.21.0,<1.23.0", markers = "python_version ~= '3.7.0' and (extra == 'numpy' or extra == 'all')", optional = true },
@@ -41,7 +42,8 @@ orjson = { version = "*", markers = "extra == 'orjson' or extra == 'all'", optio
 
 [tool.poetry.dev-dependencies]
 pyyaml = "*"
-toml = "*"
+tomli = "*"
+tomli-w = "*"
 msgpack = "*"
 numpy = [
     { version = "~1.21.0", markers = "python_version ~= '3.7.0'" },
@@ -60,14 +62,15 @@ mypy = { version = "==0.931", markers = "platform_python_implementation!='PyPy'"
 more-itertools = "~=8.6.0"
 pre-commit = "==v2.10.1"
 pytest-xdist = "^2.3.0"
+types-PyYAML = "^6.0.9"
 
 [tool.poetry.extras]
 msgpack = ["msgpack"]
 numpy = ["numpy"]
-toml = ["toml"]
+toml = ["tomli", "tomli-w"]
 yaml = ["pyyaml"]
 orjson = ["orjson"]
-all = ["msgpack", "toml", "pyyaml", "numpy", "orjson"]
+all = ["msgpack", "tomli", "tomli-w", "pyyaml", "numpy", "orjson"]
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]
diff --git a/serde/toml.py b/serde/toml.py
index 5cd97a6..efbd0f4 100644
--- a/serde/toml.py
+++ b/serde/toml.py
@@ -1,9 +1,10 @@
 """
-Serialize and Deserialize in TOML format. This module depends on [toml](https://pypi.org/project/toml/) package.
+Serialize and Deserialize in TOML format. This module depends on [tomli](https://github.com/hukkin/tomli) and [tomli-w](https://github.com/hukkin/tomli-w) packages.
 """
 from typing import Type
 
-import toml
+import tomli
+import tomli_w
 
 from .compat import T
 from .core import Coerce, TypeCheck
@@ -16,13 +17,13 @@ __all__ = ["from_toml", "to_toml"]
 class TomlSerializer(Serializer):
     @classmethod
     def serialize(cls, obj, **opts) -> str:
-        return toml.dumps(obj, **opts)
+        return tomli_w.dumps(obj, **opts)
 
 
 class TomlDeserializer(Deserializer):
     @classmethod
     def deserialize(cls, s, **opts):
-        return toml.loads(s, **opts)
+        return tomli.loads(s, **opts)
 
 
 def to_toml(obj, se: Type[Serializer] = TomlSerializer, type_check: TypeCheck = Coerce, **opts) -> str:
@@ -30,7 +31,7 @@ def to_toml(obj, se: Type[Serializer] = TomlSerializer, type_check: TypeCheck =
     Serialize the object into TOML.
 
     You can pass any serializable `obj`. If you supply keyword arguments other than `se`,
-    they will be passed in `toml.dumps` function.
+    they will be passed in `toml_w.dumps` function.
 
     If you want to use the other toml package, you can subclass `TomlSerializer` and implement your own logic.
     """


Test patch:
diff --git a/tests/common.py b/tests/common.py
index ebb521c..5f46b0f 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, Dict, Generic, List, NewType, Optional, Set, Tuple, TypeVar
+from typing import Any, Callable, Dict, Generic, List, NewType, Optional, Set, Tuple, TypeVar
 
 import more_itertools
 
@@ -42,60 +42,75 @@ class GenericClass(Generic[T, U]):
     b: U
 
 
+def param(val, typ, filter: Optional[Callable] = None):
+    """
+    Create a test parameter
+
+    * `val` is the expected value
+    * `typ` is the expected type
+    * If `filter` evaluates to True, it will filter this test case.
+    """
+    return (val, typ, filter or (lambda se, de, opt: False))
+
+
+def toml_not_supported(se, de, opt) -> bool:
+    return se is to_toml
+
+
 types: List = [
-    (10, int),  # Primitive
-    ('foo', str),
-    (100.0, float),
-    (True, bool),
-    (10, Optional[int]),  # Optional
-    (None, Optional[int]),
-    ([1, 2], List[int]),  # Container
-    ([1, 2], List),
-    ([1, 2], list),
-    ([], List[int]),
-    ({1, 2}, Set[int]),
-    ({1, 2}, Set),
-    ({1, 2}, set),
-    (set(), Set[int]),
-    ((1, 1), Tuple[int, int]),
-    ((1, 1), Tuple),
-    ({'a': 1}, Dict[str, int]),
-    ({'a': 1}, Dict),
-    ({'a': 1}, dict),
-    ({}, Dict[str, int]),
-    (data.Pri(10, 'foo', 100.0, True), data.Pri),  # dataclass
-    (data.Pri(10, 'foo', 100.0, True), Optional[data.Pri]),
-    (None, Optional[data.Pri]),
-    (10, NewType('Int', int)),  # NewType
-    ({'a': 1}, Any),  # Any
-    (GenericClass[str, int]('foo', 10), GenericClass[str, int]),  # Generic
-    (pathlib.Path('/tmp/foo'), pathlib.Path),  # Extended types
-    (pathlib.Path('/tmp/foo'), Optional[pathlib.Path]),
-    (None, Optional[pathlib.Path]),
-    (pathlib.PurePath('/tmp/foo'), pathlib.PurePath),
-    (pathlib.PurePosixPath('/tmp/foo'), pathlib.PurePosixPath),
-    (pathlib.PureWindowsPath('C:\\tmp'), pathlib.PureWindowsPath),
-    (uuid.UUID("8f85b32c-a0be-466c-87eb-b7bbf7a01683"), uuid.UUID),
-    (ipaddress.IPv4Address("127.0.0.1"), ipaddress.IPv4Address),
-    (ipaddress.IPv6Address("::1"), ipaddress.IPv6Address),
-    (ipaddress.IPv4Network("127.0.0.0/8"), ipaddress.IPv4Network),
-    (ipaddress.IPv6Network("::/128"), ipaddress.IPv6Network),
-    (ipaddress.IPv4Interface("192.168.1.1/24"), ipaddress.IPv4Interface),
-    (ipaddress.IPv6Interface("::1/128"), ipaddress.IPv6Interface),
-    (decimal.Decimal(10), decimal.Decimal),
-    (datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p'), datetime.datetime),
-    (datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p').date(), datetime.date),
-    (datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p').time(), datetime.time),
+    param(10, int),  # Primitive
+    param('foo', str),
+    param(100.0, float),
+    param(True, bool),
+    param(10, Optional[int]),  # Optional
+    param(None, Optional[int], toml_not_supported),
+    param([1, 2], List[int]),  # Container
+    param([1, 2], List),
+    param([1, 2], list),
+    param([], List[int]),
+    param({1, 2}, Set[int], toml_not_supported),
+    param({1, 2}, Set, toml_not_supported),
+    param({1, 2}, set, toml_not_supported),
+    param(set(), Set[int], toml_not_supported),
+    param((1, 1), Tuple[int, int]),
+    param((1, 1), Tuple),
+    param({'a': 1}, Dict[str, int]),
+    param({'a': 1}, Dict),
+    param({'a': 1}, dict),
+    param({}, Dict[str, 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),
+    param(10, NewType('Int', int)),  # NewType
+    param({'a': 1}, Any),  # Any
+    param(GenericClass[str, int]('foo', 10), GenericClass[str, int]),  # Generic
+    param(pathlib.Path('/tmp/foo'), pathlib.Path),  # Extended types
+    param(pathlib.Path('/tmp/foo'), Optional[pathlib.Path]),
+    param(None, Optional[pathlib.Path], toml_not_supported),
+    param(pathlib.PurePath('/tmp/foo'), pathlib.PurePath),
+    param(pathlib.PurePosixPath('/tmp/foo'), pathlib.PurePosixPath),
+    param(pathlib.PureWindowsPath('C:\\tmp'), pathlib.PureWindowsPath),
+    param(uuid.UUID("8f85b32c-a0be-466c-87eb-b7bbf7a01683"), uuid.UUID),
+    param(ipaddress.IPv4Address("127.0.0.1"), ipaddress.IPv4Address),
+    param(ipaddress.IPv6Address("::1"), ipaddress.IPv6Address),
+    param(ipaddress.IPv4Network("127.0.0.0/8"), ipaddress.IPv4Network),
+    param(ipaddress.IPv6Network("::/128"), ipaddress.IPv6Network),
+    param(ipaddress.IPv4Interface("192.168.1.1/24"), ipaddress.IPv4Interface),
+    param(ipaddress.IPv6Interface("::1/128"), ipaddress.IPv6Interface),
+    param(decimal.Decimal(10), decimal.Decimal),
+    param(datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p'), datetime.datetime),
+    param(datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p').date(), datetime.date),
+    param(datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p').time(), datetime.time),
 ]
 
 # these types can only be instantiated on their corresponding system
 if os.name == "posix":
-    types.append((pathlib.PosixPath('/tmp/foo'), pathlib.PosixPath))
+    types.append(param(pathlib.PosixPath('/tmp/foo'), pathlib.PosixPath))
 if os.name == "nt":
-    types.append((pathlib.WindowsPath('C:\\tmp'), pathlib.WindowsPath))
+    types.append(param(pathlib.WindowsPath('C:\\tmp'), pathlib.WindowsPath))
 
 if sys.version_info[:3] >= (3, 9, 0):
-    types.extend([([1, 2], list[int]), ({'a': 1}, dict[str, int]), ((1, 1), tuple[int, int])])
+    types.extend([param([1, 2], list[int]), param({'a': 1}, dict[str, int]), param((1, 1), tuple[int, int])])
 
 types_combinations: List = list(map(lambda c: list(more_itertools.flatten(c)), itertools.combinations(types, 2)))
 
@@ -122,7 +137,7 @@ def type_ids():
     from serde.compat import typename
 
     def make_id(pair: Tuple):
-        t, T = pair
+        t, T, _ = pair
         return f'{typename(T)}({t})'
 
     return list(map(make_id, types))
diff --git a/tests/test_basics.py b/tests/test_basics.py
index 1d48563..155ae5c 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -31,12 +31,15 @@ log = logging.getLogger('test')
 serde.init(True)
 
 
-@pytest.mark.parametrize('t,T', types, ids=type_ids())
+@pytest.mark.parametrize('t,T,f', types, ids=type_ids())
 @pytest.mark.parametrize('opt', opt_case, ids=opt_case_ids())
 @pytest.mark.parametrize('se,de', all_formats)
-def test_simple(se, de, opt, t, T):
+def test_simple(se, de, opt, t, T, f):
     log.info(f'Running test with se={se.__name__} de={de.__name__} opts={opt}')
 
+    if f(se, de, opt):
+        return
+
     @serde.serde(**opt)
     class C:
         i: int
@@ -63,10 +66,10 @@ def test_simple(se, de, opt, t, T):
         assert t == de(T, se(t, type_check=Strict), type_check=Strict)
 
 
-@pytest.mark.parametrize('t,T', types, ids=type_ids())
+@pytest.mark.parametrize('t,T,filter', types, ids=type_ids())
 @pytest.mark.parametrize('opt', opt_case, ids=opt_case_ids())
 @pytest.mark.parametrize('se,de', (format_dict + format_tuple))
-def test_simple_with_reuse_instances(se, de, opt, t, T):
+def test_simple_with_reuse_instances(se, de, opt, t, T, filter):
     log.info(f'Running test with se={se.__name__} de={de.__name__} opts={opt} while reusing instances')
 
     @serde.serde(**opt)
@@ -334,18 +337,19 @@ def test_default(se, de):
     assert p == from_dict(PriDefault, {'i': 10, 's': 'foo', 'f': 100.0, 'b': True})
     assert p == from_tuple(PriDefault, (10, 'foo', 100.0, True))
 
-    o = OptDefault()
-    assert o == de(OptDefault, se(o))
-
-    o = OptDefault()
-    assert o == from_dict(OptDefault, {})
-    assert o == from_dict(OptDefault, {"n": None})
-    assert o == from_dict(OptDefault, {"n": None, "i": 10})
-    assert o == from_tuple(OptDefault, (None, 10))
+    if se is not serde.toml.to_toml:
+        o = OptDefault()
+        assert o == de(OptDefault, se(o))
+        o = OptDefault()
+        assert o == from_dict(OptDefault, {})
+        assert o == from_dict(OptDefault, {"n": None})
+        assert o == from_dict(OptDefault, {"n": None, "i": 10})
+        assert o == from_tuple(OptDefault, (None, 10))
 
-    o = OptDefault(n=None, i=None)
-    assert o == from_dict(OptDefault, {"n": None, "i": None})
-    assert o == from_tuple(OptDefault, (None, None))
+    if se is not serde.toml.to_toml:
+        o = OptDefault(n=None, i=None)
+        assert o == from_dict(OptDefault, {"n": None, "i": None})
+        assert o == from_tuple(OptDefault, (None, None))
 
     assert 10 == dataclasses.fields(PriDefault)[0].default
     assert 'foo' == dataclasses.fields(PriDefault)[1].default
@@ -396,6 +400,32 @@ def test_msgpack_unnamed():
     assert p == serde.msgpack.from_msgpack(data.Pri, d, named=False)
 
 
+def test_toml():
+    @serde.serde
+    @dataclasses.dataclass
+    class Foo:
+        v: Optional[int]
+
+    f = Foo(10)
+    assert "v = 10\n" == serde.toml.to_toml(f)
+    assert f == serde.toml.from_toml(Foo, "v = 10\n")
+
+    # TODO: Should raise SerdeError
+    with pytest.raises(TypeError):
+        f = Foo(None)
+        serde.toml.to_toml(f)
+
+    @serde.serde
+    @dataclasses.dataclass
+    class Foo:
+        v: Set[int]
+
+    # TODO: Should raise SerdeError
+    with pytest.raises(TypeError):
+        f = Foo({1, 2, 3})
+        serde.toml.to_toml(f)
+
+
 @pytest.mark.parametrize('se,de', all_formats)
 def test_rename(se, de):
     @serde.serde