Repository: python/mypy

Issue:
Bogus "Function is missing a type annotation for one or more arguments" with attrs
When checking this file with mypy (0.641 or git master) and `--disallow-incomplete-defs`:

```python
import attr

@attr.s
class A:
    a = attr.ib()
```

I get an unexpected:

```
at.py:3: error: Function is missing a type annotation for one or more arguments
```

(line 3 being the `@attr.s` decorator). If I'm interpreting [what `--disallow-incomplete-defs` does](https://mypy.readthedocs.io/en/latest/command_line.html#untyped-definitions-and-calls) correctly, it should only warn if there's a partial type annotation (as opposed to none at all, which `--disallow-untyped-defs` would warn about).

Also, the error message is confusing - I don't define a function at all there. With `--show-error-context`, it says:

```
at.py: note: In member "__init__" of class "A":
```

FWIW, I (finally) started using mypy with a rather big project ([qutebrowser](https://qutebrowser.org/)) and I'm quite happy with it so far - the approach of gradual typing and the awesome documentation make it very easy to get a benefit without having to annotate the entire codebase at once.


Discussion:
cc @euresti 
Hmm. The issue here is that the generated `__init__` method **is** only partially annotated.  Because it  looks like this:

```
def __init__(self, a) -> None:
   ...
```

I'm not sure how one would fix the plugin to work around this.  (Is this also an issue with `dataclasses`?)
Oh and feel free to add the attrs label to this bug.
I think when you create a method in `plugins.common.add_method` (calling `FuncDef`), you can add some `type_annotation`s to `Argument`s (didn't test this however).
Oh, wait, it is already there, hm...
OK, I think this line in `plugins/attrs.py` may cause it:
```python
init_type = AnyType(TypeOfAny.unannotated)
```
Hm, but the actual problem is that `-> None` is always set. So it is actually not so trivial decision (sorry for thinking out loud).
Maybe conditionally use `AnyType(TypeOfAny.unannotated)` for the return of `__init__` if none of the arguments is annotated?
Just took a quick look at the plugin, and like you say, I see various solutions:

- Using something other than `TypeOfAny.unannotated` for the generated arguments - perhaps `TypeOfAny.implementation_artifact`? ("Does this Any come from an implementation limitation/bug?")
- Using `AnyType(TypeOfAny.unannotated)` for the return if no arguments are annotated (like you say) - after all, the return type annotation for `__init__` probably isn't really significant anyways (i.e., the things mypy checks don't get any worse by this being `Any` rather than `None`).
- Somehow suppressing that error message in this case, if possible.

Though I wonder if the same will also happen for more generated methods (say, `__lt__` and such) after fixing it for `__init__`.
The first one is not really an option, because the following code is unannotated and we don't want it to be magically marked as fully annotated:
```
import attr

@attr.s
class A:
    a = attr.ib()
```

Whereas this is fully annotated.
```
import attr

@attr.s
class A:
    a: Any = attr.ib()
```

It's the difference between saying
```
def foo(a):
   pass
```
and 
```
def foo(a: Any) -> None:
   pass
```

I don't know if I'll have time until mid-December to work on this though.
>The first one is not really an option, because the following code is unannotated and we don't want it to be magically marked as fully annotated

Perhaps this is okay if the plugin tells the user that the class attribute is unannotated? It seems that would make understanding the error a lot easier to understand.
> > The first one is not really an option, because the following code is unannotated and we don't want it to be magically marked as fully annotated
> 
> Perhaps this is okay if the plugin tells the user that the class attribute is unannotated? It seems that would make understanding the error a lot easier to understand.

That's already done with `--disallow-untyped-defs`:

```
at.py: note: In member "__init__" of class "A":
at.py:3: error: Function is missing a type annotation for one or more arguments
at.py: note: In class "A":
at.py:5: error: Need type annotation for 'a'
```

but with `--disallow-incomplete-defs` and no annotations at all, I'd expect no warning/error.
I was wrong.  The actual message comes from this check:

```
        def is_unannotated_any(t: Type) -> bool:
            return isinstance(t, AnyType) and t.type_of_any == TypeOfAny.unannotated

        has_explicit_annotation = (isinstance(fdef.type, CallableType)
                                   and any(not is_unannotated_any(t)
                                           for t in fdef.type.arg_types + [fdef.type.ret_type]))
```

So the problem is that I'm generating
```
def __init__(self, foo: Any(unannotated)) -> None:
    ...
```

(You can't set the return value to anything other than `None` or you trigger another error.)

The simple solution is to set the `type_annotation` of the args to `AnyType(TypeOfAny.implementation_artifact)` if they are all unannotated, but that feels weird.

The correct solution is to generate the following code: 
```
def __init__(self, foo):
   ...
```
which means `fdef.type` is `None` and `arg.type_annotation` is `None`.  That does require a bit more changes to the `add_method` method.

Is this still being looked at?
AFAICT, no. I think it is OK, if you want to try fixing it. Otherwise, hopefully @euresti will have time to do this at some point.

Resolving patch:
diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py
--- a/mypy/plugins/attrs.py
+++ b/mypy/plugins/attrs.py
@@ -639,11 +639,21 @@ def _make_frozen(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute]
 def _add_init(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute],
               adder: 'MethodAdder') -> None:
     """Generate an __init__ method for the attributes and add it to the class."""
-    adder.add_method(
-        '__init__',
-        [attribute.argument(ctx) for attribute in attributes if attribute.init],
-        NoneType()
-    )
+    args = [attribute.argument(ctx) for attribute in attributes if attribute.init]
+    if all(
+        # We use getattr rather than instance checks because the variable.type
+        # might be wrapped into a Union or some other type, but even non-Any
+        # types reliably track the fact that the argument was not annotated.
+        getattr(arg.variable.type, "type_of_any", None) == TypeOfAny.unannotated
+        for arg in args
+    ):
+        # This workaround makes --disallow-incomplete-defs usable with attrs,
+        # but is definitely suboptimal as a long-term solution.
+        # See https://github.com/python/mypy/issues/5954 for discussion.
+        for a in args:
+            a.variable.type = AnyType(TypeOfAny.implementation_artifact)
+            a.type_annotation = AnyType(TypeOfAny.implementation_artifact)
+    adder.add_method('__init__', args, NoneType())
 
 
 class MethodAdder:


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
@@ -75,7 +75,7 @@ A(1, [2], '3', 4, 5)  # E: Too many arguments for "A"
 # flags: --disallow-untyped-defs
 import attr
 @attr.s
-class A:  # E: Function is missing a type annotation for one or more arguments
+class A:
     a = attr.ib()  # E: Need type annotation for 'a'
     _b = attr.ib()  # E: Need type annotation for '_b'
     c = attr.ib(18)  # E: Need type annotation for 'c'
@@ -1121,7 +1121,7 @@ reveal_type(B)  # N: Revealed type is 'def (x: __main__.C) -> __main__.B'
 import attr
 
 @attr.s
-class B:  # E: Function is missing a type annotation for one or more arguments
+class B:
     x = attr.ib()  # E: Need type annotation for 'x'
 
 reveal_type(B)  # N: Revealed type is 'def (x: Any) -> __main__.B'
diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test
--- a/test-data/unit/check-flags.test
+++ b/test-data/unit/check-flags.test
@@ -1029,6 +1029,31 @@ def f(i: int, s):
 main:3: error: Function is missing a return type annotation
 main:3: error: Function is missing a type annotation for one or more arguments
 
+[case testDisallowIncompleteDefsAttrsNoAnnotations]
+# flags: --disallow-incomplete-defs
+import attr
+
+@attr.s()
+class Unannotated:
+    foo = attr.ib()
+
+[case testDisallowIncompleteDefsAttrsWithAnnotations]
+# flags: --disallow-incomplete-defs
+import attr
+
+@attr.s()
+class Annotated:
+    bar: int = attr.ib()
+
+[case testDisallowIncompleteDefsAttrsPartialAnnotations]
+# flags: --disallow-incomplete-defs
+import attr
+
+@attr.s()
+class PartiallyAnnotated:  # E: Function is missing a type annotation for one or more arguments
+    bar: int = attr.ib()
+    baz = attr.ib()
+
 [case testAlwaysTrueAlwaysFalseFlags]
 # flags: --always-true=YOLO --always-true=YOLO1 --always-false=BLAH1 --always-false BLAH --ignore-missing-imports
 from somewhere import YOLO, BLAH