Repository: yukinarit/pyserde

Issue:
Add tests for base container type e.g. Dict, List
```python
@dataclass
class Foo:
    items: Dict
    list: List
```


Discussion:

Patch:
diff --git a/Pipfile b/Pipfile
index af2200e..8037a9e 100644
--- a/Pipfile
+++ b/Pipfile
@@ -17,7 +17,6 @@ flake8 = "*"
 pytest = "*"
 pytest-cov = "*"
 pytest-watch = "*"
-python-coveralls = "*"
 coverage = "==4.5.4"
 pdoc3 = "~=0.8"
 mypy = {version = "==0.782",markers = "platform_python_implementation!='PyPy'"}
diff --git a/serde/compat.py b/serde/compat.py
index 0e32883..d8892ab 100644
--- a/serde/compat.py
+++ b/serde/compat.py
@@ -3,17 +3,28 @@ Module for compatibility.
 """
 import dataclasses
 import enum
+import typing
 from dataclasses import fields, is_dataclass
 from itertools import zip_longest
-from typing import Iterator, List, Optional, Tuple, Type, TypeVar, Union
+from typing import Dict, Iterator, List, Optional, Tuple, Type, TypeVar, Union
 
-from typing_inspect import get_args, get_origin, is_optional_type, is_union_type
+import typing_inspect
 
 __all__: List = []
 
 T = TypeVar('T')
 
 
+def get_origin(typ):
+    """
+    Provide `get_origin` that works in python >= 3.6.
+    """
+    try:
+        return typing.get_origin(typ)  # python>=3.8 typing module has get_args.
+    except AttributeError:
+        return typing_inspect.get_origin(typ)
+
+
 def typename(typ) -> str:
     """
     >>> from typing import List, Dict
@@ -50,11 +61,18 @@ def typename(typ) -> str:
         return typ.__name__
 
 
-def type_args(cls: Type):
+def type_args(typ):
     """
     Wrapper to suppress type error for accessing private members.
     """
-    return cls.__args__  # type: ignore
+    try:
+        args = typ.__args__  # type: ignore
+        if args is None:
+            return []
+        else:
+            return args
+    except AttributeError:
+        return typing.get_args(typ)  # python>=3.8 typing module has get_args.
 
 
 def union_args(typ: Union) -> Tuple:
@@ -86,18 +104,24 @@ def iter_types(cls: Type) -> Iterator[Type]:
     elif isinstance(cls, str):
         yield cls
     elif is_opt(cls):
-        yield from iter_types(type_args(cls)[0])
+        arg = type_args(cls)
+        if arg:
+            yield from iter_types(arg[0])
     elif is_union(cls):
         for arg in type_args(cls):
             yield from iter_types(arg)
     elif is_list(cls):
-        yield from iter_types(type_args(cls)[0])
+        arg = type_args(cls)
+        if arg:
+            yield from iter_types(arg[0])
     elif is_tuple(cls):
         for arg in type_args(cls):
             yield from iter_types(arg)
     elif is_dict(cls):
-        yield from iter_types(type_args(cls)[0])
-        yield from iter_types(type_args(cls)[1])
+        arg = type_args(cls)
+        if arg and len(arg) >= 2:
+            yield from iter_types(arg[0])
+            yield from iter_types(arg[1])
     else:
         yield cls
 
@@ -106,20 +130,26 @@ def is_union(typ) -> bool:
     """
     Test if the type is `typing.Union`.
     """
-    return is_union_type(typ) and not is_opt(typ)
+    return typing_inspect.is_union_type(typ) and not is_opt(typ)
 
 
 def is_opt(typ) -> bool:
     """
     Test if the type is `typing.Optional`.
     """
-    args = get_args(typ)
-    return is_optional_type(typ) and len(args) == 2 and not is_none(args[0]) and is_none(args[1])
+    args = typing_inspect.get_args(typ)
+    return typing_inspect.is_optional_type(typ) and len(args) == 2 and not is_none(args[0]) and is_none(args[1])
 
 
 def is_list(typ) -> bool:
     """
     Test if the type is `typing.List`.
+
+    >>> from typing import List
+    >>> is_list(List[int])
+    True
+    >>> is_list(List)
+    True
     """
     try:
         return issubclass(get_origin(typ), list)
@@ -127,6 +157,19 @@ def is_list(typ) -> bool:
         return isinstance(typ, list)
 
 
+def is_bare_list(typ) -> bool:
+    """
+    Test if the type is `typing.List` without type args.
+
+    >>> from typing import List
+    >>> is_bare_list(List[int])
+    False
+    >>> is_bare_list(List)
+    True
+    """
+    return is_list(typ) and typ is List
+
+
 def is_tuple(typ) -> bool:
     """
     Test if the type is `typing.Tuple`.
@@ -137,9 +180,28 @@ def is_tuple(typ) -> bool:
         return isinstance(typ, tuple)
 
 
+def is_bare_tuple(typ) -> bool:
+    """
+    Test if the type is `typing.Tuple` without type args.
+
+    >>> from typing import Tuple
+    >>> is_bare_tuple(Tuple[int, str])
+    False
+    >>> is_bare_tuple(Tuple)
+    True
+    """
+    return is_tuple(typ) and typ is Tuple
+
+
 def is_dict(typ) -> bool:
     """
     Test if the type is `typing.Dict`.
+
+    >>> from typing import Dict
+    >>> is_dict(Dict[int, int])
+    True
+    >>> is_dict(Dict)
+    True
     """
     try:
         return issubclass(get_origin(typ), dict)
@@ -147,6 +209,19 @@ def is_dict(typ) -> bool:
         return isinstance(typ, dict)
 
 
+def is_bare_dict(typ) -> bool:
+    """
+    Test if the type is `typing.Dict` without type args.
+
+    >>> from typing import Dict
+    >>> is_bare_dict(Dict[int, str])
+    False
+    >>> is_bare_dict(Dict)
+    True
+    """
+    return is_dict(typ) and typ is Dict
+
+
 def is_none(typ) -> bool:
     """
     >>> is_none(int)
diff --git a/serde/de.py b/serde/de.py
index 951c966..7fa9015 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -19,8 +19,8 @@ from typing import Any, Callable, Dict, List, Optional, Type
 
 import jinja2
 
-from .compat import (has_default, has_default_factory, is_dict, is_enum, is_list, is_opt, is_primitive, is_tuple,
-                     is_union, iter_types, type_args)
+from .compat import (has_default, has_default_factory, is_bare_dict, is_bare_list, is_bare_tuple, is_dict, is_enum,
+                     is_list, is_opt, is_primitive, is_tuple, is_union, iter_types, type_args)
 from .core import FROM_DICT, FROM_ITER, HIDDEN_NAME, SETTINGS, Field, Hidden, SerdeError, T, conv, fields, gen, logger
 from .more_types import deserialize as custom
 
@@ -383,7 +383,10 @@ class Renderer:
         >>> Renderer('foo').render(DeField(List[List[int]], 'l', datavar='data'))
         '[[v for v in v] for v in data["l"]]'
         """
-        return f'[{self.render(arg[0])} for v in {arg.data}]'
+        if is_bare_list(arg.type):
+            return f'list({arg.data})'
+        else:
+            return f'[{self.render(arg[0])} for v in {arg.data}]'
 
     def tuple(self, arg: DeField) -> str:
         """
@@ -400,11 +403,14 @@ class Renderer:
         >>> Renderer('foo').render(field)
         '(data[0][0], data[0][1], [v for v in data[0][2]], Foo.foo(data[0][3]))'
         """
-        values = []
-        for i, typ in enumerate(type_args(arg.type)):
-            inner = arg[i]
-            values.append(self.render(inner))
-        return f'({", ".join(values)})'
+        if is_bare_tuple(arg.type):
+            return f'tuple({arg.data})'
+        else:
+            values = []
+            for i, typ in enumerate(type_args(arg.type)):
+                inner = arg[i]
+                values.append(self.render(inner))
+            return f'({", ".join(values)})'
 
     def dict(self, arg: DeField) -> str:
         """
@@ -420,9 +426,12 @@ class Renderer:
         >>> Renderer('foo').render(DeField(Dict[Foo, List[Foo]], 'f', datavar='data'))
         '{Foo.foo(k): [Foo.foo(v) for v in v] for k, v in data["f"].items()}'
         """
-        k = arg.key_field()
-        v = arg.value_field()
-        return f'{{{self.render(k)}: {self.render(v)} for k, v in {arg.data}.items()}}'
+        if is_bare_dict(arg.type):
+            return arg.data
+        else:
+            k = arg.key_field()
+            v = arg.value_field()
+            return f'{{{self.render(k)}: {self.render(v)} for k, v in {arg.data}.items()}}'
 
     def enum(self, arg: DeField) -> str:
         return f'{arg.type.__name__}({self.primitive(arg)})'
diff --git a/serde/se.py b/serde/se.py
index 156a815..12dffaa 100644
--- a/serde/se.py
+++ b/serde/se.py
@@ -11,7 +11,8 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Type
 
 import jinja2
 
-from .compat import is_dict, is_enum, is_list, is_opt, is_primitive, is_tuple, is_union, iter_types, type_args
+from .compat import (is_bare_dict, is_bare_list, is_bare_tuple, is_dict, is_enum, is_list, is_opt, is_primitive,
+                     is_tuple, is_union, iter_types, type_args)
 from .core import (HIDDEN_NAME, SE_NAME, SETTINGS, TO_DICT, TO_ITER, Field, Hidden, SerdeError, T, conv, fields, gen,
                    logger)
 from .more_types import serialize as custom
@@ -363,30 +364,39 @@ class Renderer:
         """
         Render rvalue for list.
         """
-        earg = arg[0]
-        earg.name = 'v'
-        return f'[{self.render(earg)} for v in {arg.varname}]'
+        if is_bare_list(arg.type):
+            return arg.varname
+        else:
+            earg = arg[0]
+            earg.name = 'v'
+            return f'[{self.render(earg)} for v in {arg.varname}]'
 
     def tuple(self, arg: SeField) -> str:
         """
         Render rvalue for tuple.
         """
-        rvalues = []
-        for i, _ in enumerate(type_args(arg.type)):
-            r = arg[i]
-            r.name = f'{arg.varname}[{i}]'
-            rvalues.append(self.render(r))
-        return f"({', '.join(rvalues)})"
+        if is_bare_tuple(arg.type):
+            return arg.varname
+        else:
+            rvalues = []
+            for i, _ in enumerate(type_args(arg.type)):
+                r = arg[i]
+                r.name = f'{arg.varname}[{i}]'
+                rvalues.append(self.render(r))
+            return f"({', '.join(rvalues)})"
 
     def dict(self, arg: SeField) -> str:
         """
         Render rvalue for dict.
         """
-        karg = arg[0]
-        karg.name = 'k'
-        varg = arg[1]
-        varg.name = 'v'
-        return f'{{{self.render(karg)}: {self.render(varg)} for k, v in {arg.varname}.items()}}'
+        if is_bare_dict(arg.type):
+            return arg.varname
+        else:
+            karg = arg[0]
+            karg.name = 'k'
+            varg = arg[1]
+            varg.name = 'v'
+            return f'{{{self.render(karg)}: {self.render(varg)} for k, v in {arg.varname}.items()}}'
 
     def enum(self, arg: SeField) -> str:
         return f'__serde_enum_value__({arg.type.__name__}, {arg.varname})'


Test patch:
diff --git a/tests/test_basics.py b/tests/test_basics.py
index bd142ad..ca01422 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -1,6 +1,5 @@
 import dataclasses
 import enum
-import json
 import logging
 from dataclasses import dataclass, field
 from typing import Dict, List, Optional, Tuple
@@ -15,8 +14,7 @@ from serde.toml import from_toml, to_toml
 from serde.yaml import from_yaml, to_yaml
 
 from . import data
-from .data import (Bool, Float, Int, ListPri, NestedPri, NestedPriOpt, NestedPriTuple, Pri, PriDefault, PriOpt,
-                   PriTuple, Str)
+from .data import Bool, Float, Int, ListPri, NestedPri, NestedPriOpt, Pri, PriDefault, PriOpt, Str
 
 log = logging.getLogger('test')
 
@@ -107,6 +105,69 @@ def test_forward_declaration():
     assert 'Bar' == dataclasses.fields(Foo)[0].type
 
 
+@pytest.mark.parametrize('opt', opt_case, ids=opt_case_ids())
+@pytest.mark.parametrize('se,de', all_formats)
+def test_list(se, de, opt):
+    @deserialize(**opt)
+    @serialize(**opt)
+    @dataclass
+    class PriList:
+        i: List[int]
+        s: List[str]
+        f: List[float]
+        b: List[bool]
+
+    p = PriList([10, 10], ['foo', 'bar'], [10.0, 10.0], [True, False])
+    assert p == de(PriList, se(p))
+
+    @deserialize(**opt)
+    @serialize(**opt)
+    @dataclass
+    class BareList:
+        i: List
+
+    p = BareList([10])
+    assert p == de(BareList, se(p))
+
+    # List can contain different types (except Toml).
+    if se is not to_toml:
+        p = BareList([10, 'foo', 10.0, True])
+        assert p == de(BareList, se(p))
+
+
+@pytest.mark.parametrize('opt', opt_case, ids=opt_case_ids())
+@pytest.mark.parametrize('se,de', all_formats)
+def test_dict(se, de, opt):
+    @deserialize(**opt)
+    @serialize(**opt)
+    @dataclass
+    class PriDict:
+        i: Dict[int, int]
+        s: Dict[str, str]
+        f: Dict[float, float]
+        b: Dict[bool, bool]
+
+    if se in (to_json, to_msgpack, to_toml):
+        # JSON, Msgpack, Toml don't allow non string key.
+        p = PriDict({'10': 10}, {'foo': 'bar'}, {'100.0': 100.0}, {'True': False})
+        assert p == de(PriDict, se(p))
+    else:
+        p = PriDict({10: 10}, {'foo': 'bar'}, {100.0: 100.0}, {True: False})
+        assert p == de(PriDict, se(p))
+
+    @deserialize(**opt)
+    @serialize(**opt)
+    @dataclass
+    class BareDict:
+        d: Dict
+
+    p = BareDict({'10': 10})
+    assert p == de(BareDict, se(p))
+
+    p = BareDict({'10': 10, 'foo': 'bar', '100.0': 100.0, 'True': False})
+    assert p == de(BareDict, se(p))
+
+
 @pytest.mark.parametrize('opt', opt_case, ids=opt_case_ids())
 @pytest.mark.parametrize('se,de', all_formats)
 def test_enum(se, de, opt):
@@ -162,48 +223,58 @@ def test_enum_imported(se, de):
     assert c == cc
 
 
+@pytest.mark.parametrize('opt', opt_case, ids=opt_case_ids())
 @pytest.mark.parametrize('se,de', all_formats)
-def test_tuple(se, de):
-    p = PriTuple(
-        (10, 20, 30), ('a', 'b', 'c', 'd'), (10.0, 20.0, 30.0, 40.0, 50.0), (True, False, True, False, True, False)
-    )
-    tpl: PriTuple = de(PriTuple, se(p))
-    assert tpl.i == (10, 20, 30)
-    assert tpl.s == ('a', 'b', 'c', 'd')
-    assert tpl.f == (10.0, 20.0, 30.0, 40.0, 50.0)
-    assert tpl.b == (True, False, True, False, True, False)
+def test_tuple(se, de, opt):
+    @deserialize(**opt)
+    @serialize(**opt)
+    @dataclass
+    class Homogeneous:
+        i: Tuple[int, int]
+        s: Tuple[str, str]
+        f: Tuple[float, float]
+        b: Tuple[bool, bool]
+
+    p = Homogeneous((10, 20), ('a', 'b'), (10.0, 20.0), (True, False))
+    assert p == de(Homogeneous, se(p))
 
     # List can also be used.
-    p = PriTuple(
-        [10, 20, 30], ['a', 'b', 'c', 'd'], [10.0, 20.0, 30.0, 40.0, 50.0], [True, False, True, False, True, False]
-    )
-    tpl: PriTuple = de(PriTuple, se(p))
-    assert tpl.i == (10, 20, 30)
-    assert tpl.s == ('a', 'b', 'c', 'd')
-    assert tpl.f == (10.0, 20.0, 30.0, 40.0, 50.0)
-    assert tpl.b == (True, False, True, False, True, False)
+    p = Homogeneous([10, 20], ['a', 'b'], [10.0, 20.0], [True, False])
+    assert p != de(Homogeneous, se(p))
 
+    @deserialize(**opt)
+    @serialize(**opt)
+    @dataclass
+    class Variant:
+        t: Tuple[int, str, float, bool]
 
-@pytest.mark.parametrize('se,de', all_formats)
-def test_dataclass_in_tuple(se, de):
-    src = NestedPriTuple(
-        (Int(10), Int(10), Int(10)),
-        (Str("10"), Str("10"), Str("10"), Str("10")),
-        (Float(10.0), Float(10.0), Float(10.0), Float(10.0), Float(10.0)),
-        (Bool(False), Bool(False), Bool(False), Bool(False), Bool(False), Bool(False)),
-    )
-    assert src == from_json(NestedPriTuple, to_json(src))
-
-    with pytest.raises(IndexError):
-        j = json.dumps(
-            {
-                'i': (10, 20),
-                's': ('a', 'b', 'c', 'd'),
-                'f': (10.0, 20.0, 30.0, 40.0, 50.0),
-                'b': (True, False, True, False, True, False),
-            }
-        )
-        _: PriTuple = from_json(PriTuple, j)
+    # Toml doesn't support variant type of array.
+    if se is not to_toml:
+        p = Variant((10, 'a', 10.0, True))
+        assert p == de(Variant, se(p))
+
+    @deserialize(**opt)
+    @serialize(**opt)
+    @dataclass
+    class BareTuple:
+        t: Tuple
+
+    p = BareTuple((10, 20))
+    assert p == de(BareTuple, se(p))
+
+    @deserialize(**opt)
+    @serialize(**opt)
+    @dataclass
+    class Nested:
+        i: Tuple[Int, Int]
+        s: Tuple[Str, Str]
+        f: Tuple[Float, Float]
+        b: Tuple[Bool, Bool]
+
+    # wmmm.. Nested tuple doesn't work ..
+    if se is not to_toml:
+        p = Nested((Int(10), Int(20)), (Str("a"), Str("b")), (Float(10.0), Float(20.0)), (Bool(True), Bool(False)))
+        assert p == de(Nested, se(p))
 
 
 @pytest.mark.parametrize('se,de', all_formats)