Repository: yukinarit/pyserde
Pull request #38 (2020-10-05 14:28:37)

Support optional enums


Patch:
diff --git a/examples/enum34.py b/examples/enum34.py
index a4c5f3a..a19d864 100644
--- a/examples/enum34.py
+++ b/examples/enum34.py
@@ -3,6 +3,8 @@ from dataclasses import dataclass
 from serde import serialize, deserialize
 from serde.json import to_json, from_json
 
+import imported
+
 
 class Nested(enum.Enum):
     S = 'foo'
@@ -28,6 +30,7 @@ class Foo:
     v0: IE
     v1: IE = IE.V1  # Default enum value.
     v2: E = E.S
+    v3: imported.ImportedEnum = imported.ImportedEnum.V3  # Use enum imported from other module.
     # v3: E = E.N  # Sorry nested is not yet supported.
 
 
diff --git a/examples/imported.py b/examples/imported.py
new file mode 100644
index 0000000..cbdca94
--- /dev/null
+++ b/examples/imported.py
@@ -0,0 +1,8 @@
+import enum
+
+
+class ImportedEnum(enum.IntEnum):
+    V0 = enum.auto()
+    V1 = enum.auto()
+    V2 = 10
+    V3 = 100
diff --git a/serde/core.py b/serde/core.py
index a7486d2..1451a69 100644
--- a/serde/core.py
+++ b/serde/core.py
@@ -1,12 +1,9 @@
 """
 pyserde core module.
 """
+import dataclasses
 import logging
-from dataclasses import _MISSING_TYPE as DEFAULT_MISSING_TYPE
-from dataclasses import Field as DataclassField
-from dataclasses import dataclass, field
-from dataclasses import fields as dataclass_fields
-from dataclasses import is_dataclass
+from dataclasses import dataclass, field, is_dataclass
 from typing import Any, Callable, Dict, Iterator, List, Optional, Type, TypeVar
 
 import stringcase
@@ -88,7 +85,7 @@ def typecheck(cls: Type[T], obj: T) -> None:
     """
     if is_dataclass(obj):
         # If dataclass, type check recursively.
-        for f in dataclass_fields(obj):
+        for f in dataclasses.fields(obj):
             typecheck(f.type, getattr(obj, f.name, None))
     elif is_opt(cls):
         if obj is not None:
@@ -163,7 +160,7 @@ class Field:
 
     type: Type
     name: Optional[str]
-    default: Any = field(default_factory=DEFAULT_MISSING_TYPE)
+    default: Any = field(default_factory=dataclasses._MISSING_TYPE)
     case: Optional[str] = None
     rename: Optional[str] = None
     skip: Optional[bool] = None
@@ -171,7 +168,7 @@ class Field:
     skip_if_false: Optional[bool] = None
 
     @classmethod
-    def from_dataclass(cls, f: DataclassField) -> 'Field':
+    def from_dataclass(cls, f: dataclasses.Field) -> 'Field':
         skip_if_false_func: Optional[Func] = None
         if f.metadata.get('serde_skip_if_false'):
             skip_if_false_func = Func(skip_if_false, cls.mangle(f, 'skip_if'))
@@ -192,7 +189,7 @@ class Field:
         )
 
     @staticmethod
-    def mangle(field: DataclassField, name: str) -> str:
+    def mangle(field: dataclasses.Field, name: str) -> str:
         """
         Get mangled name based on field name.
         """
@@ -200,7 +197,7 @@ class Field:
 
 
 def fields(FieldCls: Type, cls: Type) -> Iterator[Field]:
-    return iter(FieldCls.from_dataclass(f) for f in dataclass_fields(cls))
+    return iter(FieldCls.from_dataclass(f) for f in dataclasses.fields(cls))
 
 
 def conv(f: Field, case: Optional[str] = None) -> str:
diff --git a/serde/de.py b/serde/de.py
index 447ef13..2b0cf22 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -12,17 +12,15 @@ Defines classess and functions for `deserialize` decorator.
 parts of pyserde.
 """
 import abc
+import dataclasses
 import functools
-from dataclasses import _MISSING_TYPE as DEFAULT_MISSING_TYPE
-from dataclasses import dataclass
-from dataclasses import fields as dataclass_fields
-from dataclasses import is_dataclass
-from typing import Any, Callable, Dict, List, Optional, Tuple, Type
+from dataclasses import dataclass, is_dataclass
+from typing import Any, Callable, Dict, List, Optional, 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 .core import FROM_DICT, FROM_ITER, HIDDEN_NAME, SETTINGS, Field, Hidden, SerdeError, T, conv, fields, gen
+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
 
 __all__: List = ['deserialize', 'is_deserializable', 'Deserializer', 'from_dict', 'from_tuple']
@@ -65,14 +63,23 @@ def deserialize(_cls=None, rename_all: Optional[str] = None):
 
     def wrap(cls):
         g: Dict[str, Any] = globals().copy()
+
+        # Create hidden object used by serde.
         if not hasattr(cls, HIDDEN_NAME):
             setattr(cls, HIDDEN_NAME, Hidden())
+
+        # Create a scope storage used by serde.
+        scope = {}
+        setattr(cls, '__serde_scope__', scope)
+
+        # Set custom deserializer.
         g['__custom_deserializer__'] = custom
 
-        # If there is a field of Enum, imports the class of Enum into the generation scope.
-        for f in dataclass_fields(cls):
-            if is_enum(f.type):
-                g[f.type.__name__] = f.type
+        # Collect types used in the gernerated code.
+        for typ in iter_types(cls):
+            if is_dataclass(typ) or is_enum(typ):
+                getattr(cls, '__serde_scope__')[typ.__name__] = typ
+        logger.debug(f'{cls.__name__}: __serde_scope__ {scope}')
 
         cls = de_func(cls, FROM_ITER, render_from_iter(cls, custom), g)
         cls = de_func(cls, FROM_DICT, render_from_dict(cls, rename_all, custom), g)
@@ -213,26 +220,40 @@ def from_tuple(cls, o):
 
 @dataclass
 class DeField(Field):
+    """
+    Feild class for deserialization.
+    """
+
     datavar: Optional[str] = None  # name of variable to deserialize from.
-    index: int = 0  # Field number inside dataclass.
-    parent: Optional['DeField'] = None  # Parent of this field.
+    index: int = 0  # Field index.
     iterbased: bool = False  # Iterater based deserializer or not.
 
     def __getitem__(self, n) -> 'DeField':
+        """
+        Access inner `Field` e.g. T of List[T].
+        """
         typ = type_args(self.type)[n]
         if is_list(self.type) or is_dict(self.type):
-            return ElementField(typ, 'v', datavar='v')
+            return InnerField(typ, 'v', datavar='v')
         elif is_tuple(self.type):
-            return ElementField(typ, f'{self.data}[{n}]', datavar=f'{self.data}[{n}]')
+            return InnerField(typ, f'{self.data}[{n}]', datavar=f'{self.data}[{n}]')
         else:
             return DeField(typ, self.name, datavar=self.datavar, index=self.index, iterbased=self.iterbased)
 
-    def get_kv(self) -> Tuple['DeField', 'DeField']:
+    def key_field(self) -> 'DeField':
+        """
+        Get inner key field for Dict like class.
+        """
         k = self[0]
         k.name = 'k'
         k.datavar = 'k'
-        v = self[1]
-        return (k, v)
+        return k
+
+    def value_field(self) -> 'DeField':
+        """
+        Get inner value field for Dict like class.
+        """
+        return self[1]
 
     @property
     def data(self) -> str:
@@ -247,9 +268,9 @@ class DeField(Field):
 
 
 @dataclass
-class ElementField(DeField):
+class InnerField(DeField):
     """
-    Field for element type such as List[T].
+    Field for inner type e.g. T of List[T].
     """
 
     @property
@@ -343,7 +364,7 @@ class Renderer:
         """
         Render rvalue for tuple.
 
-        >>> from typing import List
+        >>> from typing import List, Tuple
         >>> @deserialize
         ... @dataclass
         ... class Foo: pass
@@ -374,7 +395,8 @@ 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, v = arg.get_kv()
+        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:
@@ -393,7 +415,7 @@ class Renderer:
         >>> Renderer('foo').render(DeField(int, 'i', datavar='data', index=1, iterbased=True))
         'data[1]'
         """
-        if not arg.iterbased and not isinstance(arg.default, DEFAULT_MISSING_TYPE):
+        if not arg.iterbased and not isinstance(arg.default, dataclasses._MISSING_TYPE):
             default = arg.default
             if isinstance(default, str):
                 default = f'"{default}"'
@@ -405,7 +427,6 @@ class Renderer:
 def to_arg(f: DeField, index, rename_all: Optional[str] = None) -> DeField:
     f.index = index
     f.data = 'data'
-    f.parent = DeField(type(None), 'data', datavar='data', case=f.case or rename_all)
     f.case = f.case or rename_all
     return f
 
@@ -419,12 +440,17 @@ def to_iter_arg(f: DeField, *args, **kwargs):
 def render_from_iter(cls: Type, custom: Custom = None) -> str:
     template = """
 def {{func}}(data):
+  {# List up all classes used by this class. -#}
+  {% for name in cls.__serde_scope__ -%}
+  {{name}} = getattr(cls, '__serde_scope__')['{{name}}']
+  {% endfor -%}
+
   if data is None:
     return None
   return cls(
-  {% for f in cls|fields %}
+  {% for f in cls|fields -%}
   {{f|arg(loop.index-1)|rvalue}},
-  {% endfor %}
+  {% endfor -%}
   )
     """
 
@@ -439,13 +465,18 @@ def {{func}}(data):
 def render_from_dict(cls: Type, rename_all: Optional[str] = None, custom: Custom = None) -> str:
     template = """
 def {{func}}(data):
+  {# List up all classes used by this class. -#}
+  {% for name in cls.__serde_scope__ -%}
+  {{name}} = getattr(cls, '__serde_scope__')['{{name}}']
+  {% endfor -%}
+
   if data is None:
     return None
   fs = fields(cls)
   return cls(
-  {% for f in cls|fields %}
+  {% for f in cls|fields -%}
   {{f|arg(loop.index-1)|rvalue}},
-  {% endfor %}
+  {% endfor -%}
   )
     """
 
@@ -457,25 +488,16 @@ def {{func}}(data):
     return env.get_template('dict').render(func=FROM_DICT, cls=cls)
 
 
-def de_func(cls: Type[T], func: str, code: str, g: Dict = None, local: Dict = None) -> Type[T]:
+def de_func(cls: Type[T], func: str, code: str, g: Dict) -> Type[T]:
     """
     Generate function to deserialize into an instance of `deserialize` class.
     """
-    if not g:
-        g = globals().copy()
-    if not local:
-        local = locals().copy()
-
-    # Collect types to be used in the `exec` scope.
-    for typ in iter_types(cls):
-        if is_dataclass(typ):
-            g[typ.__name__] = typ
     g['cls'] = cls
     import typing
 
     g['typing'] = typing
     g['NoneType'] = type(None)
-    g['fields'] = dataclass_fields
+    g['fields'] = dataclasses.fields
 
     # Generate deserialize function.
     code = gen(code, g, cls=cls)
diff --git a/serde/inspect.py b/serde/inspect.py
index e878124..4970a36 100644
--- a/serde/inspect.py
+++ b/serde/inspect.py
@@ -11,11 +11,12 @@ $ python -m serde.inspect <PATH> <NAME>
 """
 import argparse
 import importlib
+import logging
 import os
 import sys
 from typing import Type
 
-from .core import HIDDEN_NAME, init
+from .core import HIDDEN_NAME, init, logger
 
 init(True)
 
@@ -29,6 +30,8 @@ def inspect(cls: Type) -> str:
 
 
 def main(arg):
+    if arg.verbose:
+        logging.basicConfig(level=logging.DEBUG)
     dir = os.path.dirname(arg.path)
     mod = os.path.basename(arg.path)[:-3]
     print(f'Loading {mod}.{arg.name} from {dir}.')
@@ -36,11 +39,14 @@ def main(arg):
     pkg = importlib.import_module(mod)
     cls = getattr(pkg, arg.name)
     print(inspect(cls))
+    print('----------------------------------')
+    print(f"serde_scope: {getattr(cls, '__serde_scope__')}")
 
 
 parser = argparse.ArgumentParser(description='pyserde-inspect')
 parser.add_argument('path', type=str, help='Python script path.')
 parser.add_argument('name', type=str, help='Pyserde class name.')
+parser.add_argument('-v', dest='verbose', action='store_true', help='Enable debug logging.')
 
 
 if __name__ == '__main__':
diff --git a/serde/se.py b/serde/se.py
index 9dd4f4d..5c839f9 100644
--- a/serde/se.py
+++ b/serde/se.py
@@ -4,12 +4,9 @@ Defines classess and functions for `serialize` decorator.
 """
 import abc
 import copy  # noqa
+import dataclasses
 import functools
-from dataclasses import asdict as _asdict
-from dataclasses import astuple as _astuple
-from dataclasses import dataclass
-from dataclasses import fields as dataclass_fields  # noqa
-from dataclasses import is_dataclass
+from dataclasses import dataclass, is_dataclass
 from typing import Any, Callable, Dict, List, Optional, Tuple, Type
 
 import jinja2
@@ -118,7 +115,7 @@ def astuple(v):
     if is_serializable(v):
         return getattr(v, TO_ITER)()
     elif is_dataclass(v):
-        return _astuple(v)
+        return dataclasses.astuple(v)
     elif isinstance(v, Dict):
         return {astuple(k): astuple(v) for k, v in v.items()}
     elif isinstance(v, (Tuple, List)):
@@ -134,7 +131,7 @@ def asdict(v):
     if is_serializable(v):
         return getattr(v, TO_DICT)()
     elif is_dataclass(v):
-        return _asdict(v)
+        return dataclasses.asdict(v)
     elif isinstance(v, Dict):
         return {asdict(k): asdict(v) for k, v in v.items()}
     elif isinstance(v, (Tuple, List)):
@@ -216,9 +213,9 @@ def {{func}}(obj):
   {% if cls|is_dataclass %}
   res = []
   {% for f in cls|fields -%}
-  {% if not f.skip|default(False) %}
+  {% if not f.skip|default(False) -%}
   res.append({{f|arg|rvalue()}})
-  {% endif %}
+  {% endif -%}
   {% endfor -%}
   return tuple(res)
   {% endif %}
@@ -239,22 +236,22 @@ def {{func}}(obj):
   if not is_dataclass(obj):
     return copy.deepcopy(obj)
 
-  {% if cls|is_dataclass %}
+  {% if cls|is_dataclass -%}
   res = {}
   {% for f in cls|fields -%}
 
-  {% if not f.skip %}
-    {% if f.skip_if %}
+  {% if not f.skip -%}
+    {% if f.skip_if -%}
   if not {{f.skip_if.name}}({{f|arg|rvalue()}}):
     res["{{f|case}}"] = {{f|arg|rvalue()}}
-    {% else %}
+    {% else -%}
   res["{{f|case}}"] = {{f|arg|rvalue()}}
-    {% endif %}
+    {% endif -%}
   {% endif %}
 
   {% endfor -%}
   return res
-  {% endif %}
+  {% endif -%}
     """
     renderer = Renderer(TO_DICT, custom)
     env = jinja2.Environment(loader=jinja2.DictLoader({'dict': template}))
@@ -374,18 +371,16 @@ class Renderer:
         return f'{arg.varname}'
 
 
-def se_func(cls: Type[T], func: str, code: str, g: Dict = None, local: Dict = None) -> Type[T]:
+def se_func(cls: Type[T], func: str, code: str, g: Dict = None) -> Type[T]:
     """
     Generate function to serialize into an object.
     """
     # Generate serialize function.
     if not g:
         g = globals().copy()
-    if not local:
-        local = locals().copy()
-    code = gen(code, g, local, cls=cls)
+    code = gen(code, g, cls=cls)
 
-    setattr(cls, func, local[func])
+    setattr(cls, func, g[func])
     if SETTINGS['debug']:
         hidden = getattr(cls, HIDDEN_NAME)
         hidden.code[func] = code


Test patch:
diff --git a/tests/data.py b/tests/data.py
index 7775c70..c1f0983 100644
--- a/tests/data.py
+++ b/tests/data.py
@@ -1,8 +1,11 @@
+import enum
 from dataclasses import dataclass
 from typing import Dict, List, Optional, Tuple
 
 from serde import deserialize, serialize
 
+from . import imported
+
 
 @deserialize
 @serialize
@@ -213,6 +216,33 @@ class PriDefault:
     b: bool = True
 
 
+class E(enum.Enum):
+    V0 = 1
+    V1 = 'foo'
+    V2 = 10.0
+    V3 = True
+
+
+class IE(enum.IntEnum):
+    V0 = enum.auto()
+    V1 = enum.auto()
+    V2 = 10
+    V3 = 100
+
+
+@deserialize
+@serialize
+@dataclass
+class EnumInClass:
+    """
+    Class having enum fields.
+    """
+
+    e: IE = IE.V2
+    o: Optional[E] = E.V1
+    i: imported.IE = imported.IE.V1
+
+
 ListPri = List[Pri]
 
 DictPri = Dict[str, Pri]
diff --git a/tests/imported.py b/tests/imported.py
new file mode 100644
index 0000000..2f62979
--- /dev/null
+++ b/tests/imported.py
@@ -0,0 +1,7 @@
+import enum
+
+
+class IE(enum.IntEnum):
+    V0 = enum.auto()
+    V1 = enum.auto()
+    V2 = 10
diff --git a/tests/test_basics.py b/tests/test_basics.py
index c58fffd..2b80296 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -1,14 +1,14 @@
+import dataclasses
 import enum
 import json
 import logging
-from dataclasses import dataclass, field, fields
+from dataclasses import dataclass, field
 from typing import Dict, List, Optional, Tuple
 
 import pytest
 
-from serde import asdict, astuple, deserialize, from_dict, from_tuple
-from serde import init as serde_init
-from serde import logger, serialize
+import serde
+from serde import asdict, astuple, deserialize, from_dict, from_tuple, logger, serialize
 from serde.json import from_json, to_json
 from serde.msgpack import from_msgpack, to_msgpack
 from serde.toml import from_toml, to_toml
@@ -22,7 +22,7 @@ logging.basicConfig(level=logging.WARNING)
 
 logger.setLevel(logging.DEBUG)
 
-serde_init(True)
+serde.init(True)
 
 format_dict: List = [(asdict, from_dict)]
 
@@ -79,10 +79,7 @@ def test_forward_declaration():
 
 @pytest.mark.parametrize('se,de', all_formats)
 def test_intenum(se, de):
-    class IE(enum.IntEnum):
-        V0 = enum.auto()
-        V1 = enum.auto()
-        V2 = 10
+    from .data import IE
 
     @deserialize
     @serialize
@@ -106,11 +103,7 @@ def test_intenum(se, de):
 
 @pytest.mark.parametrize('se,de', all_formats)
 def test_enum(se, de):
-    class E(enum.Enum):
-        V0 = 1
-        V1 = 'foo'
-        V2 = 10.0
-        V3 = True
+    from .data import E
 
     @deserialize
     @serialize
@@ -158,6 +151,15 @@ def test_enum_nested(se, de):
     # ff = de(Foo, se(f))
 
 
+@pytest.mark.parametrize('se,de', all_formats)
+def test_enum_imported(se, de):
+    from .data import EnumInClass
+
+    c = EnumInClass()
+    cc = de(EnumInClass, se(c))
+    assert c == cc
+
+
 @pytest.mark.parametrize('se,de', all_formats)
 def test_tuple(se, de):
     p = PriTuple(
@@ -229,8 +231,8 @@ def test_field_default():
     class Bar:
         i: int = field(default=10)
 
-    assert 10 == fields(Foo)[0].default
-    assert 10 == fields(Bar())[0].default
+    assert 10 == dataclasses.fields(Foo)[0].default
+    assert 10 == dataclasses.fields(Bar())[0].default
 
 
 @pytest.mark.parametrize('se,de', all_formats)