Repository: yukinarit/pyserde (python)

Issue:
Provide a way to disable `datetime` being converted to `str`
Currently, https://github.com/yukinarit/pyserde/blob/35901de31a8b135c777710d5388c8f5a0f11dc66/serde/se.py#L383-L384 converts `datetime` to an ISO string unconditionally. This makes the `msgpack` serializer unable to convert `datetime` to its [extension type](https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type) in the spec. It would be nice to have a flag to disable this convertion and let the serializer handle it.

Pull request description:
Fix reuse_instances for non dataclass objects
Closes #483 

Patch:
diff --git a/serde/core.py b/serde/core.py
index 58d9e03..8418025 100644
--- a/serde/core.py
+++ b/serde/core.py
@@ -150,13 +150,13 @@ class Cache:
         logger.debug(f"(de)serializing code for {class_name} was generated")
         return wrapper
 
-    def serialize(self, cls: Type[Any], obj: Any) -> Any:
+    def serialize(self, cls: Type[Any], obj: Any, **kwargs: Any) -> Any:
         """
         Serialize the specified type of object into dict or tuple.
         """
         wrapper = self._get_class(cls)
         scope: Scope = getattr(wrapper, SERDE_SCOPE)
-        data = scope.funcs[TO_DICT](wrapper(obj), reuse_instances=False, convert_sets=True)
+        data = scope.funcs[TO_DICT](wrapper(obj), **kwargs)
 
         logging.debug(f"Intermediate value: {data}")
 
diff --git a/serde/json.py b/serde/json.py
index 3b35635..4b34b61 100644
--- a/serde/json.py
+++ b/serde/json.py
@@ -52,7 +52,12 @@ class JsonDeserializer(Deserializer[AnyStr]):
 
 
 def to_json(
-    obj: Any, cls: Optional[Any] = None, se: Type[Serializer[str]] = JsonSerializer, **opts: Any
+    obj: Any,
+    cls: Optional[Any] = None,
+    se: Type[Serializer[str]] = JsonSerializer,
+    reuse_instances: bool = False,
+    convert_sets: bool = True,
+    **opts: Any,
 ) -> str:
     """
     Serialize the object into JSON str. [orjson](https://github.com/ijl/orjson)
@@ -67,7 +72,9 @@ def to_json(
     If you want to use another json package, you can subclass `JsonSerializer` and implement
     your own logic.
     """
-    return se.serialize(to_dict(obj, c=cls, reuse_instances=False, convert_sets=True), **opts)
+    return se.serialize(
+        to_dict(obj, c=cls, reuse_instances=reuse_instances, convert_sets=convert_sets), **opts
+    )
 
 
 @overload
diff --git a/serde/msgpack.py b/serde/msgpack.py
index c2c88fe..bed4c4c 100644
--- a/serde/msgpack.py
+++ b/serde/msgpack.py
@@ -45,6 +45,8 @@ def to_msgpack(
     se: Type[Serializer[bytes]] = MsgPackSerializer,
     named: bool = True,
     ext_dict: Optional[Dict[Type[Any], int]] = None,
+    reuse_instances: bool = False,
+    convert_sets: bool = True,
     **opts: Any,
 ) -> bytes:
     """
@@ -68,7 +70,7 @@ def to_msgpack(
         if ext_type_code is None:
             raise SerdeError(f"Could not find type code for {obj_type.__name__} in ext_dict")
 
-    kwargs: Any = {"c": cls, "reuse_instances": False, "convert_sets": True}
+    kwargs: Any = {"c": cls, "reuse_instances": reuse_instances, "convert_sets": convert_sets}
     dict_or_tuple = to_dict(obj, **kwargs) if named else to_tuple(obj, **kwargs)
     return se.serialize(
         dict_or_tuple,
diff --git a/serde/pickle.py b/serde/pickle.py
index 433aa74..9c285d4 100644
--- a/serde/pickle.py
+++ b/serde/pickle.py
@@ -24,9 +24,16 @@ class PickleDeserializer(Deserializer[bytes]):
 
 
 def to_pickle(
-    obj: Any, cls: Optional[Any] = None, se: Type[Serializer[bytes]] = PickleSerializer, **opts: Any
+    obj: Any,
+    cls: Optional[Any] = None,
+    se: Type[Serializer[bytes]] = PickleSerializer,
+    reuse_instances: bool = False,
+    convert_sets: bool = True,
+    **opts: Any,
 ) -> bytes:
-    return se.serialize(to_dict(obj, c=cls, reuse_instances=False), **opts)
+    return se.serialize(
+        to_dict(obj, c=cls, reuse_instances=reuse_instances, convert_sets=convert_sets), **opts
+    )
 
 
 @overload
diff --git a/serde/se.py b/serde/se.py
index 8a43e1e..eca1dcc 100644
--- a/serde/se.py
+++ b/serde/se.py
@@ -368,7 +368,7 @@ def to_obj(
         if is_dataclass_without_se(o):
             serialize(type(o))
             return serializable_to_obj(o)
-        if is_serializable(o):
+        elif is_serializable(o):
             return serializable_to_obj(o)
         elif isinstance(o, list):
             return [thisfunc(e) for e in o]
@@ -378,10 +378,10 @@ def to_obj(
             return [thisfunc(e) for e in o]
         elif isinstance(o, dict):
             return {k: thisfunc(v) for k, v in o.items()}
-        elif is_str_serializable_instance(o):
-            return str(o)
-        elif is_datetime_instance(o):
-            return o.isoformat()
+        elif is_str_serializable_instance(o) or is_datetime_instance(o):
+            return CACHE.serialize(
+                c or o.__class__, o, reuse_instances=reuse_instances, convert_sets=convert_sets
+            )
 
         return o
 
diff --git a/serde/toml.py b/serde/toml.py
index 3a992ff..5d88297 100644
--- a/serde/toml.py
+++ b/serde/toml.py
@@ -34,7 +34,12 @@ class TomlDeserializer(Deserializer[str]):
 
 
 def to_toml(
-    obj: Any, cls: Optional[Any] = None, se: Type[Serializer[str]] = TomlSerializer, **opts: Any
+    obj: Any,
+    cls: Optional[Any] = None,
+    se: Type[Serializer[str]] = TomlSerializer,
+    reuse_instances: bool = False,
+    convert_sets: bool = True,
+    **opts: Any,
 ) -> str:
     """
     Serialize the object into TOML.
@@ -45,7 +50,9 @@ def to_toml(
     If you want to use the other toml package, you can subclass `TomlSerializer` and implement
     your own logic.
     """
-    return se.serialize(to_dict(obj, c=cls, reuse_instances=False), **opts)
+    return se.serialize(
+        to_dict(obj, c=cls, reuse_instances=reuse_instances, convert_sets=convert_sets), **opts
+    )
 
 
 @overload
diff --git a/serde/yaml.py b/serde/yaml.py
index fd723e7..d916daf 100644
--- a/serde/yaml.py
+++ b/serde/yaml.py
@@ -26,7 +26,12 @@ class YamlDeserializer(Deserializer[str]):
 
 
 def to_yaml(
-    obj: Any, cls: Optional[Any] = None, se: Type[Serializer[str]] = YamlSerializer, **opts: Any
+    obj: Any,
+    cls: Optional[Any] = None,
+    se: Type[Serializer[str]] = YamlSerializer,
+    reuse_instances: bool = False,
+    convert_sets: bool = True,
+    **opts: Any,
 ) -> str:
     """
     Serialize the object into YAML.
@@ -37,7 +42,9 @@ def to_yaml(
     If you want to use the other yaml package, you can subclass `YamlSerializer` and implement
     your own logic.
     """
-    return se.serialize(to_dict(obj, c=cls, reuse_instances=False), **opts)
+    return se.serialize(
+        to_dict(obj, c=cls, reuse_instances=reuse_instances, convert_sets=convert_sets), **opts
+    )
 
 
 @overload


Test patch:
diff --git a/tests/test_basics.py b/tests/test_basics.py
index 8bb7b5a..f81b342 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -2,6 +2,7 @@ import dataclasses
 import enum
 import logging
 import uuid
+import datetime
 from beartype.roar import BeartypeCallHintViolation
 from typing import (
     ClassVar,
@@ -111,6 +112,16 @@ def test_simple_with_reuse_instances(se, de, opt, t, T, filter):
     assert c == de(C, se(c, reuse_instances=True), reuse_instances=True)
 
 
+def test_non_dataclass_reuse_instances() -> None:
+    dt = datetime.datetime.fromisoformat("2020-01-01 00:00:00+00:00")
+    assert "2020-01-01T00:00:00+00:00" == serde.to_dict(dt, reuse_instances=False)  # type: ignore
+    assert dt == serde.to_dict(dt, reuse_instances=True)
+    assert dt is serde.to_dict(dt, reuse_instances=True)
+    assert "2020-01-01T00:00:00+00:00" == serde.to_tuple(dt, reuse_instances=False)  # type: ignore
+    assert dt == serde.to_dict(dt, reuse_instances=True)
+    assert dt is serde.to_dict(dt, reuse_instances=True)
+
+
 def test_non_dataclass():
     @serde.serde
     class Foo:
@@ -480,10 +491,8 @@ def test_toml():
     class Foo:
         v: Set[int]
 
-    # TODO: Should raise SerdeError
-    with pytest.raises(TypeError):
-        f = Foo({1, 2, 3})
-        serde.toml.to_toml(f)
+    f = Foo({1, 2, 3})
+    serde.toml.to_toml(f)
 
 
 @pytest.mark.parametrize("se,de", all_formats)