Repository: yukinarit/pyserde
Pull request #5 (2019-11-06 08:55:54)

Support nested optional types


Patch:
diff --git a/README.md b/README.md
index 9507152..7e977f3 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,15 @@ You can check [the benchmark code](bench/bench.py) for more information.
 
 ## Features
 
+<details open><summary><b><code>Supported types</code></b></summary><br />
+
+* primitives (int, float, str, bool)
+* containers (List, Tuple, Dict)
+* [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)
+* [`Dataclass`](https://docs.python.org/3/library/dataclasses.html)
+
+</details>
+
 <details open><summary><b><code>Supported data formats</code></b></summary><br />
 
 ```python
diff --git a/serde/de.py b/serde/de.py
index d61cd7d..e975a31 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -406,6 +406,8 @@ def to_arg(f: Field, rename_all: Optional[str] = None) -> DeField:
 def render_from_iter(cls: Type) -> str:
     template = """
 def {{func}}(data):
+  if data is None:
+    return None
   return cls(
   {% for f in cls|fields %}
   {{f|arg|rvalue}},
@@ -424,6 +426,8 @@ def {{func}}(data):
 def render_from_dict(cls: Type, rename_all: Optional[str] = None) -> str:
     template = """
 def {{func}}(data):
+  if data is None:
+    return None
   return cls(
   {% for f in cls|fields %}
   {{f|arg|rvalue}},
diff --git a/serde/se.py b/serde/se.py
index 59967e2..f481299 100644
--- a/serde/se.py
+++ b/serde/se.py
@@ -16,7 +16,7 @@ from typing import Any, Dict, List, Optional, Tuple, Type
 import jinja2
 import stringcase
 
-from .compat import is_dict, is_list, is_tuple, type_args
+from .compat import is_dict, is_list, is_tuple, is_opt, type_args
 from .core import HIDDEN_NAME, SE_NAME, SETTINGS, TO_DICT, TO_ITER, Field, Hidden, T, fields, gen
 
 
@@ -320,6 +320,8 @@ class Renderer:
         """
         if is_dataclass(arg.type):
             return self.dataclass(arg)
+        elif is_opt(arg.type):
+            return self.opt(arg)
         elif is_list(arg.type):
             return self.list(arg)
         elif is_dict(arg.type):
@@ -330,14 +332,31 @@ class Renderer:
             return self.primitive(arg)
 
     def dataclass(self, arg: SeField) -> str:
+        """
+        Render rvalue for dataclass.
+        """
         return f'{arg.varname}.{self.func}()'
 
+    def opt(self, arg: SeField) -> str:
+        """
+        Render rvalue for optional.
+        """
+        inner = arg[0]
+        inner.name = arg.varname
+        return f'{self.render(inner)} if {arg.varname} is not None else None'
+
     def list(self, arg: SeField) -> str:
+        """
+        Render rvalue for list.
+        """
         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]
@@ -346,6 +365,9 @@ class Renderer:
         return f"({', '.join(rvalues)})"
 
     def dict(self, arg: SeField) -> str:
+        """
+        Render rvalue for dict.
+        """
         karg = arg[0]
         karg.name = 'k'
         varg = arg[1]
@@ -353,6 +375,9 @@ class Renderer:
         return f'{{{self.render(karg)}: {self.render(varg)} for k, v in {arg.varname}.items()}}'
 
     def primitive(self, arg: SeField) -> str:
+        """
+        Render rvalue for primitives.
+        """
         return f'{arg.varname}'
 
 


Test patch:
diff --git a/tests/test_basics.py b/tests/test_basics.py
index 143da09..98577f1 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -6,7 +6,7 @@ from typing import Dict, List
 
 import pytest
 
-from serde import asdict, astuple, deserialize, from_dict
+from serde import asdict, deserialize, from_dict
 from serde import init as serde_init
 from serde import logger, serialize
 from serde.json import from_json, to_json
@@ -144,13 +144,16 @@ def test_optional(se, de):
     assert p == de(PriOpt, se(p))
 
 
-def test_optional_dataclass():
-    pass
-    # p = NestedPriOpt(Int(20), Str(None), Float(100.0), Bool(True))
-    # s = '{"i": 20, "s": null, "f": 100.0, "b": true}'
-    # TODO
-    # assert s == asdict(p)
-    # assert p == from_json(PriOpt, s)
+@pytest.mark.parametrize('se,de', all_formats)
+def test_optional_nested(se, de):
+    p = NestedPriOpt(Int(20), Str('foo'), Float(100.0), Bool(True))
+    assert p == de(NestedPriOpt, se(p))
+
+    p = NestedPriOpt(Int(20), None, Float(100.0), None)
+    assert p == de(NestedPriOpt, se(p))
+
+    p = NestedPriOpt(None, None, None, None)
+    assert p == de(NestedPriOpt, se(p))
 
 
 def test_complex():