Repository: python/mypy

Issue:
bug with attrs converters
@euresti I'm getting new errors when running mypy against the attrs doctests:

```
Line 1, in converters1
Failed example:
    import attr
    @attr.s
    class C(object):
        x = attr.ib(converter=int)
    o = C("1")
    o.x
Expected nothing
Got:
    main:4: error: Unsupported converter function, only named functions are currently supported
```

I assume this is trying to ward off lambdas, but getting tripped up by builtins?

Btw, Is there an existing issue for creating a mypy plugin search path, so that we can move the attrs plugin into the attrs project?



Discussion:
>Btw, Is there an existing issue for creating a mypy plugin search path, so that we can move the attrs plugin into the attrs project?

I think https://github.com/python/mypy/issues/3916 is what you are looking for?
That's the one, thanks.  I made a quick PR for it.

On Tue, Mar 13, 2018 at 5:18 PM Ethan Smith <notifications@github.com>
wrote:

> Btw, Is there an existing issue for creating a mypy plugin search path, so
> that we can move the attrs plugin into the attrs project?
>
> I think #3916 <https://github.com/python/mypy/issues/3916> is what you
> are looking for?
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/python/mypy/issues/4729#issuecomment-372863036>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AAD3E9CRxzoEnxmauX7veIEC13pccTrkks5teGHngaJpZM4SpCoA>
> .
>


Resolving patch:
diff --git a/mypy/plugin.py b/mypy/plugin.py
--- a/mypy/plugin.py
+++ b/mypy/plugin.py
@@ -90,6 +90,10 @@ def anal_type(self, t: Type, *,
     def class_type(self, info: TypeInfo) -> Type:
         raise NotImplementedError
 
+    @abstractmethod
+    def builtin_type(self, fully_qualified_name: str) -> Instance:
+        raise NotImplementedError
+
     @abstractmethod
     def lookup_fully_qualified(self, name: str) -> SymbolTableNode:
         raise NotImplementedError
diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py
--- a/mypy/plugins/attrs.py
+++ b/mypy/plugins/attrs.py
@@ -9,11 +9,11 @@
     Context, Argument, Var, ARG_OPT, ARG_POS, TypeInfo, AssignmentStmt,
     TupleExpr, ListExpr, NameExpr, CallExpr, RefExpr, FuncBase,
     is_class_var, TempNode, Decorator, MemberExpr, Expression, FuncDef, Block,
-    PassStmt, SymbolTableNode, MDEF, JsonDict
+    PassStmt, SymbolTableNode, MDEF, JsonDict, OverloadedFuncDef
 )
 from mypy.types import (
     Type, AnyType, TypeOfAny, CallableType, NoneTyp, TypeVarDef, TypeVarType,
-    Overloaded, Instance
+    Overloaded, Instance, UnionType, FunctionLike
 )
 from mypy.typevars import fill_typevars
 
@@ -60,13 +60,35 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument:
                 # The converter may be a local variable. Check there too.
                 converter = ctx.api.lookup_qualified(self.converter_name, self.info, True)
 
-            if (converter
-                    and converter.type
-                    and isinstance(converter.type, CallableType)
-                    and converter.type.arg_types):
-                init_type = ctx.api.anal_type(converter.type.arg_types[0])
-            else:
-                ctx.api.fail("Cannot determine type of converter function", self.context)
+            # Get the type of the converter.
+            converter_type = None
+            if converter and isinstance(converter.node, TypeInfo):
+                from mypy.checkmember import type_object_type  # To avoid import cycle.
+                converter_type = type_object_type(converter.node, ctx.api.builtin_type)
+            elif converter and isinstance(converter.node, OverloadedFuncDef):
+                converter_type = converter.node.type
+            elif converter and converter.type:
+                converter_type = converter.type
+
+            init_type = None
+            if isinstance(converter_type, CallableType) and converter_type.arg_types:
+                init_type = ctx.api.anal_type(converter_type.arg_types[0])
+            elif isinstance(converter_type, Overloaded):
+                types = []  # type: List[Type]
+                for item in converter_type.items():
+                    # Walk the overloads looking for methods that can accept one argument.
+                    num_arg_types = len(item.arg_types)
+                    if not num_arg_types:
+                        continue
+                    if num_arg_types > 1 and any(kind == ARG_POS for kind in item.arg_kinds[1:]):
+                        continue
+                    types.append(item.arg_types[0])
+                # Make a union of all the valid types.
+                if types:
+                    args = UnionType.make_simplified_union(types)
+                    init_type = ctx.api.anal_type(args)
+            if not init_type:
+                ctx.api.fail("Cannot determine __init__ type from converter", self.context)
                 init_type = AnyType(TypeOfAny.from_error)
         elif self.converter_name == '':
             # This means we had a converter but it's not of a type we can infer.
@@ -351,16 +373,17 @@ def _get_converter_name(ctx: 'mypy.plugin.ClassDefContext',
     """Return the full name of the converter if it exists and is a simple function."""
     # TODO: Support complex converters, e.g. lambdas, calls, etc.
     if converter:
-        if (isinstance(converter, RefExpr)
-                and converter.node
-                and isinstance(converter.node, FuncBase)
-                and converter.node.type
-                and isinstance(converter.node.type, CallableType)
-                and converter.node.type.arg_types):
-            return converter.node.fullname()
+        if isinstance(converter, RefExpr) and converter.node:
+            if (isinstance(converter.node, FuncBase)
+                    and converter.node.type
+                    and isinstance(converter.node.type, FunctionLike)):
+                return converter.node.fullname()
+            elif isinstance(converter.node, TypeInfo):
+                return converter.node.fullname()
+
         # Signal that we have an unsupported converter.
         ctx.api.fail(
-            "Unsupported converter function, only named functions are currently supported",
+            "Unsupported converter, only named functions and types are currently supported",
             converter
         )
         return ''


Test patch:
diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test
--- a/test-data/unit/check-attr.test
+++ b/test-data/unit/check-attr.test
@@ -532,6 +532,32 @@ class C:
 
 [builtins fixtures/list.pyi]
 
+[case testAttrsUsingBadConverter]
+import attr
+from typing import overload
+@overload
+def bad_overloaded_converter(x: int, y: int) -> int:
+    ...
+@overload
+def bad_overloaded_converter(x: str, y: str) -> str:
+    ...
+def bad_overloaded_converter(x, y=7):
+    return x
+def bad_converter() -> str:
+    return ''
+@attr.dataclass
+class A:
+    bad: str = attr.ib(converter=bad_converter)
+    bad_overloaded: int = attr.ib(converter=bad_overloaded_converter)
+reveal_type(A)
+[out]
+main:15: error: Cannot determine __init__ type from converter
+main:15: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], str]"
+main:16: error: Cannot determine __init__ type from converter
+main:16: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], int]"
+main:17: error: Revealed type is 'def (bad: Any, bad_overloaded: Any) -> __main__.A'
+[builtins fixtures/list.pyi]
+
 [case testAttrsUsingUnsupportedConverter]
 import attr
 class Thing:
@@ -542,9 +568,9 @@ def factory(default: int):
    ...
 @attr.s
 class C:
-    x: str = attr.ib(converter=thing.do_it)  # E: Unsupported converter function, only named functions are currently supported
-    y: str = attr.ib(converter=lambda x: x)  # E: Unsupported converter function, only named functions are currently supported
-    z: str = attr.ib(converter=factory(8))  # E: Unsupported converter function, only named functions are currently supported
+    x: str = attr.ib(converter=thing.do_it)  # E: Unsupported converter, only named functions and types are currently supported
+    y: str = attr.ib(converter=lambda x: x)  # E: Unsupported converter, only named functions and types are currently supported
+    z: str = attr.ib(converter=factory(8))   # E: Unsupported converter, only named functions and types are currently supported
 reveal_type(C)  # E: Revealed type is 'def (x: Any, y: Any, z: Any) -> __main__.C'
 [builtins fixtures/list.pyi]
 
@@ -567,6 +593,24 @@ reveal_type(A)  # E: Revealed type is 'def (x: builtins.int) -> __main__.A'
 reveal_type(A(15).x)  # E: Revealed type is 'builtins.str'
 [builtins fixtures/list.pyi]
 
+[case testAttrsUsingConverterWithTypes]
+from typing import overload
+import attr
+
+@attr.dataclass
+class A:
+    x: str
+
+@attr.s
+class C:
+    x: complex = attr.ib(converter=complex)
+    y: int = attr.ib(converter=int)
+    z: A = attr.ib(converter=A)
+
+o = C("1", "2", "3")
+o = C(1, 2, "3")
+[builtins fixtures/attr.pyi]
+
 [case testAttrsCmpWithSubclasses]
 import attr
 @attr.s
diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test
--- a/test-data/unit/check-incremental.test
+++ b/test-data/unit/check-incremental.test
@@ -3984,6 +3984,42 @@ main:2: error: Revealed type is 'def (x: builtins.list[builtins.int]) -> a.a.A'
 [out2]
 main:2: error: Revealed type is 'def (x: builtins.list[builtins.int]) -> a.a.A'
 
+[case testAttrsIncrementalConverterType]
+from a import C
+import attr
+o = C("1", "2", "3", "4")
+o = C(1, 2, "3", 4)
+reveal_type(C)
+@attr.s
+class D(C):
+    x: str = attr.ib()
+reveal_type(D)
+[file a.py]
+from typing import overload
+import attr
+@attr.dataclass
+class A:
+    x: str
+@overload
+def parse(x: int) -> int:
+   ...
+@overload
+def parse(x: str, y: str = '') -> int:
+   ...
+def parse(x, y): ...
+@attr.s
+class C:
+    a: complex = attr.ib(converter=complex)
+    b: int = attr.ib(converter=int)
+    c: A = attr.ib(converter=A)
+    d: int = attr.ib(converter=parse)
+[builtins fixtures/attr.pyi]
+[out1]
+main:5: error: Revealed type is 'def (a: Union[builtins.float, builtins.str], b: Union[builtins.str, builtins.bytes, builtins.int], c: builtins.str, d: Union[builtins.int, builtins.str]) -> a.C'
+main:9: error: Revealed type is 'def (a: Union[builtins.float, builtins.str], b: Union[builtins.str, builtins.bytes, builtins.int], c: builtins.str, d: Union[builtins.int, builtins.str], x: builtins.str) -> __main__.D'
+[out2]
+main:5: error: Revealed type is 'def (a: Union[builtins.float, builtins.str], b: Union[builtins.str, builtins.bytes, builtins.int], c: builtins.str, d: Union[builtins.int, builtins.str]) -> a.C'
+main:9: error: Revealed type is 'def (a: Union[builtins.float, builtins.str], b: Union[builtins.str, builtins.bytes, builtins.int], c: builtins.str, d: Union[builtins.int, builtins.str], x: builtins.str) -> __main__.D'
 
 [case testAttrsIncrementalThreeRuns]
 from a import A