Repository: wyfo/apischema

Issue:
`as_str` deserialization fails if `__init__` has return annotation
Here's a strange one:

If we use `as_str` on a class where the `__init__` method has the return annotation `-> None` (or any other return annotation, but any other annotation would be wrong), then the `deserialize` function fails with `apischema.visitor.Unsupported`.

Minimal example:

```python
from apischema import deserialize
from apischema.conversions import as_str


class StrHolder:
    def __init__(self, val: str) -> None:
        self.val = val

    def __str__(self) -> str:
        return self.val


as_str(StrHolder)

res = deserialize(StrHolder, "foo")

```

The above fails with `apischema.visitor.Unsupported: <class '__main__.StrHolder'>`

If we remove the `-> None` then it works fine:

```python
from apischema import deserialize
from apischema.conversions import as_str


class StrHolder:
    def __init__(self, val: str):
        self.val = val

    def __str__(self) -> str:
        return self.val


as_str(StrHolder)

res = deserialize(StrHolder, "foo")

```

Omitting the `-> None` is fine as a workaround; the worst part is that it doesn't make sense and is very had to track down 😅

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/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")


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