Repository: wyfo/apischema

Issue:
Tuple passthrough should be toggleable
*apischema* doesn't allow to deserialize `tuple` by default. However, it can be an issue for example with the idiom `deserialize(MyType, serialize(MyType, my_obj))` if `MyType` contains some tuples of primitives (see https://github.com/pcdshub/whatrecord/issues/110). User could also expect the serialized data to contains only lists, for instance to write test assertions.

As other passthrough optimization, I think it should be deactivated by default




Discussion:

Patch:
diff --git a/apischema/conversions/utils.py b/apischema/conversions/utils.py
index 505c872..cd8d8e1 100644
--- a/apischema/conversions/utils.py
+++ b/apischema/conversions/utils.py
@@ -1,4 +1,4 @@
-from inspect import Parameter, isclass, signature
+from inspect import Parameter, signature
 from typing import Any, Callable, Dict, Generic, Optional, Tuple, Type, cast
 
 from apischema.types import AnyType
@@ -32,7 +32,7 @@ def converter_types(
         else:
             parameters = list(signature(converter).parameters.values())
     except ValueError:  # builtin types
-        if target is None and isclass(converter):
+        if target is None and is_type(converter):
             target = cast(Type[Any], converter)
         if source is None:
             raise TypeError("Converter source is unknown") from None
@@ -51,7 +51,7 @@ def converter_types(
         if source is not None and target is not None:
             return source, target
         types = get_type_hints(converter, None, namespace, include_extras=True)
-        if not types and isclass(converter):
+        if not types and is_type(converter):
             types = get_type_hints(
                 converter.__new__, None, namespace, include_extras=True
             ) or get_type_hints(
@@ -63,12 +63,12 @@ def converter_types(
             except KeyError:
                 raise TypeError("converter source is unknown") from None
         if target is None:
-            try:
-                target = types.pop("return")
-            except KeyError:
-                if isclass(converter):
-                    target = cast(Type, converter)
-                else:
+            if is_type(converter):
+                target = cast(Type, converter)
+            else:
+                try:
+                    target = types.pop("return")
+                except KeyError:
                     raise TypeError("converter target is unknown") from None
     return source, target
 
diff --git a/apischema/serialization/__init__.py b/apischema/serialization/__init__.py
index 73b7058..c319693 100644
--- a/apischema/serialization/__init__.py
+++ b/apischema/serialization/__init__.py
@@ -84,6 +84,7 @@ class PassThroughOptions:
     any: bool = False
     collections: bool = False
     enums: bool = False
+    tuple: bool = False
     types: Union[Collection[AnyType], Callable[[AnyType], bool]] = ()
 
     def __post_init__(self):
@@ -177,12 +178,7 @@ class SerializationMethodVisitor(
         cls_to_check = Mapping if is_typed_dict(cls) else cls
 
         def wrapper(obj: Any) -> Any:
-            if isinstance(obj, cls_to_check):
-                try:
-                    return method(obj)
-                except Exception:
-                    pass
-            return fallback(obj)
+            return method(obj) if isinstance(obj, cls_to_check) else fallback(obj)
 
         return wrapper
 
@@ -198,9 +194,13 @@ class SerializationMethodVisitor(
                 # using map is faster than comprehension
                 return list(map(serialize_value, obj))
 
-        elif issubclass(cls, (list, tuple)) or (
-            self.pass_through_options.collections
-            and not issubclass(cls, collections.abc.Set)
+        elif (
+            issubclass(cls, list)
+            or (issubclass(cls, tuple) and self.pass_through_options.tuple)
+            or (
+                self.pass_through_options.collections
+                and not issubclass(cls, collections.abc.Set)
+            )
         ):
             method = identity
         else:
@@ -354,7 +354,7 @@ class SerializationMethodVisitor(
     def tuple(self, types: Sequence[AnyType]) -> SerializationMethod:
         elt_serializers = list(enumerate(map(self.visit, types)))
         if all(method is identity for _, method in elt_serializers):
-            return identity
+            return identity if self.pass_through_options.tuple else list  # type: ignore
 
         def method(obj: Any) -> Any:
             return [serialize_elt(obj[i]) for i, serialize_elt in elt_serializers]
diff --git a/apischema/serialization/serialized_methods.py b/apischema/serialization/serialized_methods.py
index 0ba02d4..80e5726 100644
--- a/apischema/serialization/serialized_methods.py
+++ b/apischema/serialization/serialized_methods.py
@@ -1,7 +1,7 @@
 from collections import defaultdict
 from dataclasses import dataclass
 from functools import wraps
-from inspect import Parameter, isclass, signature
+from inspect import Parameter, signature
 from typing import (
     Any,
     Callable,
@@ -24,7 +24,7 @@ from apischema.methods import method_registerer
 from apischema.ordering import Ordering
 from apischema.schemas import Schema
 from apischema.types import AnyType, Undefined, UndefinedType
-from apischema.typing import generic_mro, get_type_hints
+from apischema.typing import generic_mro, get_type_hints, is_type
 from apischema.utils import (
     deprecate_kwargs,
     get_args2,
@@ -61,7 +61,7 @@ class SerializedMethod:
     def types(self, owner: AnyType = None) -> Mapping[str, AnyType]:
         types = get_type_hints(self.func, include_extras=True)
         if "return" not in types:
-            if isclass(self.func):
+            if is_type(self.func):
                 types["return"] = self.func
             else:
                 raise TypeError("Function must be typed")
diff --git a/examples/serialization.py b/examples/serialization.py
index a62b0dc..1656467 100644
--- a/examples/serialization.py
+++ b/examples/serialization.py
@@ -10,7 +10,7 @@ class Foo:
 
 
 assert serialize(Foo, Foo("baz")) == {"bar": "baz"}
-assert serialize(tuple[int, int], (0, 1)) == (0, 1)
+assert serialize(tuple[int, int], (0, 1)) == [0, 1]
 assert (
     serialize(Any, {"key": ("value", 42)})
     == serialize({"key": ("value", 42)})


Test patch:
diff --git a/tests/integration/test_type_converter.py b/tests/integration/test_type_converter.py
new file mode 100644
index 0000000..cbd1433
--- /dev/null
+++ b/tests/integration/test_type_converter.py
@@ -0,0 +1,12 @@
+from apischema import deserialize, deserializer
+
+
+@deserializer
+class Foo:
+    def __init__(self, bar: int) -> None:
+        self.bar = bar
+
+
+def test_type_converter():
+    foo = deserialize(Foo, 42)
+    assert isinstance(foo, Foo) and foo.bar == 42