Repository: yukinarit/pyserde
Pull request #413 (2023-07-22 14:34:44)

Add support for dataclasses with `kw_only`
Python 3.10 introduces the `kw_only` argument to the dataclass decorator. This is not compatible with current versions of pyserde (see #297 ).

The `kw_only` argument can be useful for dataclasses making use of inheritance because the "TypeError: non-default argument 'child_val' follows default argument" error.

The change in this pull request changes the `from_dict` method to always use named arguments. Because there are no positional only arguments in dataclasses (at least to my knowledge) there should not be any issue with this.


Patch:
diff --git a/serde/core.py b/serde/core.py
index bb39391..d067b74 100644
--- a/serde/core.py
+++ b/serde/core.py
@@ -6,6 +6,7 @@ import dataclasses
 import enum
 import functools
 import logging
+import sys
 import re
 from dataclasses import dataclass
 from typing import (
@@ -609,6 +610,8 @@ class Field(Generic[T]):
         if flatten is True:
             flatten = FlattenOpts()
 
+        kw_only = bool(f.kw_only) if sys.version_info >= (3, 10) else False
+
         return cls(
             f.type,
             f.name,
@@ -627,6 +630,7 @@ class Field(Generic[T]):
             deserializer=deserializer,
             flatten=flatten,
             parent=parent,
+            kw_only=kw_only,
         )
 
     def to_dataclass(self) -> DataclassField:
diff --git a/serde/de.py b/serde/de.py
index a3bb79d..d711def 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -1065,7 +1065,11 @@ def {{func}}(cls=cls, maybe_generic=None, maybe_generic_type_vars=None, data=Non
   try:
     rv = cls(
     {% for f in fields %}
+    {% if f.kw_only %}
+    {{f.name}}=__{{f.name}},
+    {% else %}
     __{{f.name}},
+    {% endif %}
     {% endfor %}
     )
   except Exception as e:


Test patch:
diff --git a/tests/test_kwonly.py b/tests/test_kwonly.py
new file mode 100644
index 0000000..dac0595
--- /dev/null
+++ b/tests/test_kwonly.py
@@ -0,0 +1,46 @@
+import sys
+from dataclasses import dataclass, field
+from typing import Optional
+
+import pytest
+
+from serde import deserialize, from_dict
+
+
+@pytest.mark.skipif(
+    sys.version_info < (3, 10), reason="dataclasses `kw_only` requires python3.10 or higher"
+)
+def test_simple():
+    @deserialize
+    @dataclass(kw_only=True)
+    class Hello:
+        a: str
+
+    assert Hello(a="ok") == from_dict(Hello, {"a": "ok"})
+
+
+@pytest.mark.skipif(
+    sys.version_info < (3, 10), reason="dataclasses `kw_only` requires python3.10 or higher"
+)
+def test_inheritance():
+    @dataclass(kw_only=True)
+    class Friend:
+        name: str = "MyFriend"
+
+    @dataclass(kw_only=True)
+    class Parent:
+        value: Optional[int]
+
+    @dataclass(kw_only=True)
+    class Child(Parent):
+        value: int = 42
+        friend: Friend = field(default_factory=Friend)
+        child_val: str
+
+    # check with defaults
+    assert Child(child_val="test") == from_dict(Child, {"child_val": "test"})
+
+    # check without defaults
+    assert Child(child_val="test", value=34, friend=Friend(name="my_friend")) == from_dict(
+        Child, {"child_val": "test", "value": 34, "friend": {"name": "my_friend"}}
+    )