Repository: yukinarit/pyserde

Issue:
render tuple does not properly escape quotes when using coerce
The following code throws a `SyntaxError`:

```python
import serde

@serde.serde(type_check=serde.coerce)
class Foo:
    loc: tuple[float, float]

foo = Foo(loc=(1, 2))
print(serde.to_dict(foo))
```

The error is
```
Traceback (most recent call last):
  File "/Users/kevin.squire/tmp/pyserde/tmp/test2.py", line 5, in <module>
    class Foo:
  File "/Users/kevin.squire/tmp/pyserde/serde/__init__.py", line 182, in wrap
    deserialize(
  File "/Users/kevin.squire/tmp/pyserde/serde/de.py", line 356, in deserialize
    return wrap(_cls)
  File "/Users/kevin.squire/tmp/pyserde/serde/de.py", line 329, in wrap
    add_func(
  File "/Users/kevin.squire/tmp/pyserde/serde/core.py", line 339, in add_func
    code = gen(func_code, globals)
  File "/Users/kevin.squire/tmp/pyserde/serde/core.py", line 325, in gen
    exec(code, globals, locals)
  File "<string>", line 12
    __loc = (coerce_object("Foo", "data["loc"][0]", float, data["loc"][0]), coerce_object("Foo", "data["loc"][1]", float, data["loc"][1]),)
                                  ^^^^^^^^^^
SyntaxError: invalid syntax. Perhaps you forgot a comma?
```

The quotes around `"loc"` need to be escaped in the string in the second argument to `coerce_object`.

Fix forthcoming.

Discussion:

Patch:
diff --git a/serde/de.py b/serde/de.py
index bd0d78e..5e3e478 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -956,7 +956,9 @@ class Renderer:
         if self.suppress_coerce and suppress_coerce:
             return dat
         else:
-            return f'coerce_object("{self.class_name}", "{arg.name}", {typ}, {dat})'
+            assert arg.name
+            escaped_arg_name = arg.name.replace('"', '\\"')
+            return f'coerce_object("{self.class_name}", "{escaped_arg_name}", {typ}, {dat})'
 
     def c_tor(self, arg: DeField[Any]) -> str:
         return f"{typename(arg.type)}({arg.data})"
diff --git a/serde/se.py b/serde/se.py
index b74abe4..e258fb9 100644
--- a/serde/se.py
+++ b/serde/se.py
@@ -922,7 +922,9 @@ class Renderer:
         if self.suppress_coerce:
             return var
         else:
-            return f'coerce_object("{self.class_name}", "{arg.name}", {typ}, {var})'
+            assert arg.name
+            escaped_arg_name = arg.name.replace('"', '\\"')
+            return f'coerce_object("{self.class_name}", "{escaped_arg_name}", {typ}, {var})'
 
     def string(self, arg: SeField[Any]) -> str:
         return f"str({arg.varname})"


Test patch:
diff --git a/tests/test_de.py b/tests/test_de.py
index 6bb64dd..6348608 100644
--- a/tests/test_de.py
+++ b/tests/test_de.py
@@ -60,8 +60,8 @@ def test_render_tuple() -> None:
         pass
 
     rendered = Renderer("foo").render(DeField(tuple[str, int, list[int], Foo], "d", datavar="data"))
-    rendered_str = 'coerce_object("None", "data["d"][0]", str, data["d"][0])'
-    rendered_int = 'coerce_object("None", "data["d"][1]", int, data["d"][1])'
+    rendered_str = 'coerce_object("None", "data[\\"d\\"][0]", str, data["d"][0])'
+    rendered_int = 'coerce_object("None", "data[\\"d\\"][1]", int, data["d"][1])'
     rendered_lst = '[coerce_object("None", "v", int, v) for v in data["d"][2]]'
     rendered_foo = f"Foo.__serde__.funcs['foo'](data=data[\"d\"][3], {kwargs})"
     assert rendered == f"({rendered_str}, {rendered_int}, {rendered_lst}, {rendered_foo},)"
diff --git a/tests/test_type_check.py b/tests/test_type_check.py
index e45ef4b..d367882 100644
--- a/tests/test_type_check.py
+++ b/tests/test_type_check.py
@@ -184,3 +184,12 @@ def test_coerce() -> None:
     assert p3.s.s == "100"
     assert p3.f.f == 1000.0
     assert p3.b.b
+
+    @serde.serde(type_check=serde.coerce)
+    class InnerTuple:
+        # Note: `foo` needs to be longer than 1 char, to properly test
+        # quote escaping
+        foo: tuple[float, float]
+
+    f = InnerTuple(foo=(1, 2))
+    assert f.foo == (1.0, 2.0)