Repository: yukinarit/pyserde
Pull request #29 (2020-08-26 13:09:50)

fix: Fix "has no attribute 'mangle'" error


Patch:
diff --git a/examples/skip.py b/examples/skip.py
index b9b880b..60ec817 100644
--- a/examples/skip.py
+++ b/examples/skip.py
@@ -10,7 +10,7 @@ Usage:
 
 from typing import List, Dict
 from dataclasses import dataclass, field
-from serde import serialize
+from serde import serialize, deserialize
 from serde.json import to_json
 
 
@@ -22,6 +22,7 @@ class Resource:
     metadata: Dict[str, str] = field(default_factory=dict, metadata={'serde_skip': True})
 
 
+@deserialize
 @serialize
 @dataclass
 class World:
diff --git a/serde/core.py b/serde/core.py
index 20d7991..cdbb4c3 100644
--- a/serde/core.py
+++ b/serde/core.py
@@ -129,6 +129,32 @@ def typecheck(cls: Type[T], obj: T) -> None:
             raise ValueError(f'{obj} is not instance of {cls}')
 
 
+@dataclass
+class Func:
+    """
+    Function wrapper that has `mangled` optional field.
+
+    pyserde copies every function reference into global scope
+    for code generation. Mangling function names is needed in
+    order to avoid name conflict in the global scope when
+    multiple fields receives `skip_if` attribute.
+    """
+
+    inner: Callable[[Any], bool]
+    mangeld: str = ""
+
+    def __call__(self, v) -> bool:
+        return self.inner(v)
+
+    @property
+    def name(self) -> str:
+        return self.mangeld
+
+
+def skip_if_false(v):
+    return not bool(v)
+
+
 @dataclass
 class Field:
     """
@@ -141,23 +167,18 @@ class Field:
     case: Optional[str] = None
     rename: Optional[str] = None
     skip: Optional[bool] = None
-    skip_if: Optional[Callable[[Any], bool]] = None
+    skip_if: Optional[Func] = None
     skip_if_false: Optional[bool] = None
 
     @classmethod
     def from_dataclass(cls, f: DataclassField) -> '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'))
 
-            def skip_if_false(v):
-                return not bool(v)
-
-            skip_if_false.mangled = cls.mangle(f, 'skip_if')
-        else:
-            skip_if_false = None
-
-        skip_if = f.metadata.get('serde_skip_if')
-        if skip_if:
-            skip_if.mangled = cls.mangle(f, 'skip_if')
+        skip_if: Optional[Func] = None
+        if f.metadata.get('serde_skip_if'):
+            skip_if = Func(f.metadata.get('serde_skip_if'), cls.mangle(f, 'skip_if'))
 
         return cls(
             f.type,
@@ -165,9 +186,16 @@ class Field:
             default=f.default,
             rename=f.metadata.get('serde_rename'),
             skip=f.metadata.get('serde_skip'),
-            skip_if=skip_if or skip_if_false,
+            skip_if=skip_if or skip_if_false_func,
         )
 
+    @staticmethod
+    def mangle(field: DataclassField, name: str) -> str:
+        """
+        Get mangled name based on field name.
+        """
+        return f'{field.name}_{name}'
+
 
 def fields(FieldCls: Type, cls: Type) -> Iterator[Field]:
     return iter(FieldCls.from_dataclass(f) for f in dataclass_fields(cls))
diff --git a/serde/de.py b/serde/de.py
index 8dbfb6b..f93d19d 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -25,13 +25,7 @@ from .compat import is_dict, is_list, is_opt, is_primitive, is_tuple, is_union,
 from .core import FROM_DICT, FROM_ITER, HIDDEN_NAME, SETTINGS, Field, Hidden, SerdeError, T, conv, fields, gen
 from .more_types import deserialize as custom
 
-__all__: List = [
-    'deserialize',
-    'is_deserializable',
-    'Deserializer',
-    'from_dict',
-    'from_tuple',
-]
+__all__: List = ['deserialize', 'is_deserializable', 'Deserializer', 'from_dict', 'from_tuple']
 
 Custom = Optional[Callable[['DeField', Any], Any]]
 
diff --git a/serde/se.py b/serde/se.py
index 25fe43f..0f76900 100644
--- a/serde/se.py
+++ b/serde/se.py
@@ -19,13 +19,7 @@ from .compat import is_dict, is_list, is_opt, is_primitive, is_tuple, is_union,
 from .core import HIDDEN_NAME, SE_NAME, SETTINGS, TO_DICT, TO_ITER, Field, Hidden, T, conv, fields, gen
 from .more_types import serialize as custom
 
-__all__: List = [
-    'serialize',
-    'is_serializable',
-    'Serializer',
-    'astuple',
-    'asdict',
-]
+__all__: List = ['serialize', 'is_serializable', 'Serializer', 'astuple', 'asdict']
 
 Custom = Optional[Callable[[Any], Any]]
 
@@ -88,7 +82,7 @@ def serialize(_cls=None, rename_all: Optional[str] = None) -> Type:
         g: Dict[str, Any] = globals().copy()
         for f in sefields(cls):
             if f.skip_if:
-                g[f.skip_if.mangled] = f.skip_if
+                g[f.skip_if.name] = f.skip_if
         g['__custom_serializer__'] = custom
         cls = se_func(cls, TO_ITER, render_astuple(cls, custom), g)
         cls = se_func(cls, TO_DICT, render_asdict(cls, rename_all, custom), g)
@@ -203,10 +197,6 @@ class SeField(Field):
         typ = type_args(self.type)[n]
         return SeField(typ, None)
 
-    @staticmethod
-    def mangle(field: DataclassField, name: str) -> str:
-        return f'{field.name}_{name}'
-
 
 sefields = functools.partial(fields, SeField)
 
@@ -254,7 +244,7 @@ def {{func}}(obj):
 
   {% if not f.skip %}
     {% if f.skip_if %}
-  if not {{f.skip_if.mangled}}({{f|arg|rvalue()}}):
+  if not {{f.skip_if.name}}({{f|arg|rvalue()}}):
     res["{{f|case}}"] = {{f|arg|rvalue()}}
     {% else %}
   res["{{f|case}}"] = {{f|arg|rvalue()}}


Test patch:
diff --git a/tests/test_basics.py b/tests/test_basics.py
index c568061..09f9e1f 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -2,7 +2,7 @@ import enum
 import json
 import logging
 from dataclasses import dataclass, field, fields
-from typing import Dict, List, Tuple
+from typing import Dict, List, Optional, Tuple
 
 import pytest
 
@@ -15,7 +15,8 @@ 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, NestedPri, NestedPriOpt, NestedPriTuple, Pri, PriDefault, PriOpt, PriTuple, Str, ListPri
+from .data import (Bool, Float, Int, ListPri, NestedPri, NestedPriOpt, NestedPriTuple, Pri, PriDefault, PriOpt,
+                   PriTuple, Str)
 
 logging.basicConfig(level=logging.WARNING)
 
@@ -342,3 +343,50 @@ def test_rename_all(se, de):
 
     f = Foo(class_name='foo')
     assert f == de(Foo, se(f, named=True), named=True)
+
+
+@pytest.mark.parametrize('se,de', (format_dict + format_json + format_msgpack + format_yaml + format_toml))
+def test_skip_if(se, de):
+    @deserialize
+    @serialize
+    @dataclass
+    class Foo:
+        comments: Optional[List[str]] = field(default_factory=list, metadata={'serde_skip_if': lambda v: len(v) == 0})
+        attrs: Optional[Dict[str, str]] = field(
+            default_factory=dict, metadata={'serde_skip_if': lambda v: v is None or len(v) == 0}
+        )
+
+    f = Foo(['foo'], {"bar": "baz"})
+    assert f == de(Foo, se(f))
+
+    f = Foo([])
+    ff = de(Foo, se(f))
+    assert ff.comments is None  # TODO format_tuple gets [] instaed of None
+    assert ff.attrs is None  # TODO format_tuple gets {} instaed of None
+
+
+@pytest.mark.parametrize('se,de', all_formats)
+def test_skip_if_false(se, de):
+    @deserialize
+    @serialize
+    @dataclass
+    class Foo:
+        comments: Optional[List[str]] = field(default_factory=list, metadata={'serde_skip_if_false': True})
+
+    f = Foo(['foo'])
+    assert f == de(Foo, se(f))
+
+
+@pytest.mark.parametrize('se,de', (format_dict + format_json + format_msgpack + format_yaml + format_toml))
+def test_skip_if_overrides_skip_if_false(se, de):
+    @deserialize
+    @serialize
+    @dataclass
+    class Foo:
+        comments: Optional[List[str]] = field(
+            default_factory=list, metadata={'serde_skip_if_false': True, 'serde_skip_if': lambda v: len(v) == 1}
+        )
+
+    f = Foo(['foo'])
+    ff = de(Foo, se(f))
+    assert ff.comments is None
diff --git a/tests/test_more_types.py b/tests/test_more_types.py
index fa58503..8da673c 100644
--- a/tests/test_more_types.py
+++ b/tests/test_more_types.py
@@ -30,6 +30,7 @@ def test_decimal(se, de):
     f = Foo(d=Decimal(0.1))
     assert f == de(Foo, se(f))
 
+
 @pytest.mark.parametrize('se,de', format_json + format_yaml + format_toml)  # type: ignore
 def test_path(se, de):
     f = Bar(p=Path('/tmp'))