Repository: python/mypy

Issue:
Naming a closure __getattr__ triggers `Invalid signature`
Mypy won't let me name a closure `__getattr__`. Since it's not a method I'm not sure why mypy would care what the signature is. This is somewhat important, though, as Pytho 3.7 supports `__getattr__` functions in modules.

* Are you reporting a bug, or opening a feature request? bug
* Please insert below the code you are checking with mypy,
  or a mock-up repro if the source is private. We would appreciate
  if you try to simplify your case to a minimal repro.
```python
def foo():
    def __getattr__():
        pass
    return __getattr__
```
* What is the actual behavior/output? `tester.py:2: error: Invalid signature "def () -> Any"`
* What is the behavior/output you expect? No error
* What are the versions of mypy and Python you are using? CPython master, mypy 0.59
  Do you see the same issue after installing mypy from Git master? yes
* What are the mypy flags you are using? (For example --strict-optional) None



Discussion:
Oh `check_getattr_method` checks whether we are at a class scope by `len(self.scope.stack) == 1` which is obviously wrong. Moreover it still allows `__getattr__` only in stubs. @JelleZijlstra could you please take a look at this?

(This is indeed relatively high priority.)
@ilevkivskyi I guess it would be good to make sure your PEP is supported by mypy 😉 
I'll take a look at this soon.

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -1076,32 +1076,35 @@ def check_inplace_operator_method(self, defn: FuncBase) -> None:
             if fail:
                 self.msg.signatures_incompatible(method, other_method, defn)
 
-    def check_getattr_method(self, typ: CallableType, context: Context, name: str) -> None:
+    def check_getattr_method(self, typ: Type, context: Context, name: str) -> None:
         if len(self.scope.stack) == 1:
-            # module-level __getattr__
+            # module scope
             if name == '__getattribute__':
                 self.msg.fail('__getattribute__ is not valid at the module level', context)
                 return
-            elif name == '__getattr__' and not self.is_stub:
-                self.msg.fail('__getattr__ is not valid at the module level outside a stub file',
-                              context)
-                return
+            # __getattr__ is fine at the module level as of Python 3.7 (PEP 562). We could
+            # show an error for Python < 3.7, but that would be annoying in code that supports
+            # both 3.7 and older versions.
             method_type = CallableType([self.named_type('builtins.str')],
                                        [nodes.ARG_POS],
                                        [None],
                                        AnyType(TypeOfAny.special_form),
                                        self.named_type('builtins.function'))
-        else:
+        elif self.scope.active_class():
             method_type = CallableType([AnyType(TypeOfAny.special_form),
                                         self.named_type('builtins.str')],
                                        [nodes.ARG_POS, nodes.ARG_POS],
                                        [None, None],
                                        AnyType(TypeOfAny.special_form),
                                        self.named_type('builtins.function'))
+        else:
+            return
         if not is_subtype(typ, method_type):
-            self.msg.invalid_signature(typ, context)
+            self.msg.invalid_signature_for_special_method(typ, context, name)
 
-    def check_setattr_method(self, typ: CallableType, context: Context) -> None:
+    def check_setattr_method(self, typ: Type, context: Context) -> None:
+        if not self.scope.active_class():
+            return
         method_type = CallableType([AnyType(TypeOfAny.special_form),
                                     self.named_type('builtins.str'),
                                     AnyType(TypeOfAny.special_form)],
@@ -1110,7 +1113,7 @@ def check_setattr_method(self, typ: CallableType, context: Context) -> None:
                                    NoneTyp(),
                                    self.named_type('builtins.function'))
         if not is_subtype(typ, method_type):
-            self.msg.invalid_signature(typ, context)
+            self.msg.invalid_signature_for_special_method(typ, context, '__setattr__')
 
     def expand_typevars(self, defn: FuncItem,
                         typ: CallableType) -> List[Tuple[FuncItem, CallableType]]:
@@ -1483,6 +1486,22 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
         else:
             lvalue_type, index_lvalue, inferred = self.check_lvalue(lvalue)
 
+            # If we're assigning to __getattr__ or similar methods, check that the signature is
+            # valid.
+            if isinstance(lvalue, NameExpr) and lvalue.node:
+                name = lvalue.node.name()
+                if name in ('__setattr__', '__getattribute__', '__getattr__'):
+                    # If an explicit type is given, use that.
+                    if lvalue_type:
+                        signature = lvalue_type
+                    else:
+                        signature = self.expr_checker.accept(rvalue)
+                    if signature:
+                        if name == '__setattr__':
+                            self.check_setattr_method(signature, lvalue)
+                        else:
+                            self.check_getattr_method(signature, lvalue, name)
+
             if isinstance(lvalue, RefExpr):
                 if self.check_compatibility_all_supers(lvalue, lvalue_type, rvalue):
                     # We hit an error on this line; don't check for any others
@@ -1541,6 +1560,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
                     return
                 if rvalue_type and infer_lvalue_type and not isinstance(lvalue_type, PartialType):
                     self.binder.assign_type(lvalue, rvalue_type, lvalue_type, False)
+
             elif index_lvalue:
                 self.check_indexed_assignment(index_lvalue, rvalue, lvalue)
 
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -967,6 +967,10 @@ def yield_from_invalid_operand_type(self, expr: Type, context: Context) -> Type:
     def invalid_signature(self, func_type: Type, context: Context) -> None:
         self.fail('Invalid signature "{}"'.format(func_type), context)
 
+    def invalid_signature_for_special_method(
+            self, func_type: Type, context: Context, method_name: str) -> None:
+        self.fail('Invalid signature "{}" for "{}"'.format(func_type, method_name), context)
+
     def reveal_type(self, typ: Type, context: Context) -> None:
         self.fail('Revealed type is \'{}\''.format(typ), context)
 
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -1335,11 +1335,11 @@ def visit_import_from(self, imp: ImportFrom) -> None:
                     self.add_submodules_to_parent_modules(possible_module_id, True)
                 elif possible_module_id in self.missing_modules:
                     missing = True
-            # If it is still not resolved, and the module is a stub
-            # check for a module level __getattr__
-            if module and not node and module.is_stub and '__getattr__' in module.names:
+            # If it is still not resolved, check for a module level __getattr__
+            if (module and not node and (module.is_stub or self.options.python_version >= (3, 7))
+                    and '__getattr__' in module.names):
                 getattr_defn = module.names['__getattr__']
-                if isinstance(getattr_defn.node, FuncDef):
+                if isinstance(getattr_defn.node, (FuncDef, Var)):
                     if isinstance(getattr_defn.node.type, CallableType):
                         typ = getattr_defn.node.type.ret_type
                     else:
@@ -2715,18 +2715,23 @@ def visit_member_expr(self, expr: MemberExpr) -> None:
                     expr.kind = n.kind
                     expr.fullname = n.fullname
                     expr.node = n.node
-            elif file is not None and file.is_stub and '__getattr__' in file.names:
+            elif (file is not None and (file.is_stub or self.options.python_version >= (3, 7))
+                    and '__getattr__' in file.names):
                 # If there is a module-level __getattr__, then any attribute on the module is valid
                 # per PEP 484.
-                getattr_defn = file.names['__getattr__']
-                if isinstance(getattr_defn.node, FuncDef):
+                getattr_defn = self.normalize_type_alias(file.names['__getattr__'], expr)
+                if not getattr_defn:
+                    typ = AnyType(TypeOfAny.from_error)  # type: Type
+                elif isinstance(getattr_defn.node, (FuncDef, Var)):
                     if isinstance(getattr_defn.node.type, CallableType):
                         typ = getattr_defn.node.type.ret_type
                     else:
-                        typ = AnyType(TypeOfAny.special_form)
-                    expr.kind = MDEF
-                    expr.fullname = '{}.{}'.format(file.fullname(), expr.name)
-                    expr.node = Var(expr.name, type=typ)
+                        typ = AnyType(TypeOfAny.from_error)
+                else:
+                    typ = AnyType(TypeOfAny.from_error)
+                expr.kind = MDEF
+                expr.fullname = '{}.{}'.format(file.fullname(), expr.name)
+                expr.node = Var(expr.name, type=typ)
             else:
                 # We only catch some errors here; the rest will be
                 # caught during type checking.


Test patch:
diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test
--- a/test-data/unit/check-classes.test
+++ b/test-data/unit/check-classes.test
@@ -1862,7 +1862,7 @@ main:6: error: Return type of "__iadd__" incompatible with "__add__" of supertyp
 main:8: error: Argument 1 of "__iadd__" incompatible with "__add__" of supertype "A"
 main:8: error: Signatures of "__iadd__" and "__add__" are incompatible
 
-[case testGetAttribute]
+[case testGetattribute]
 
 a, b = None, None # type: A, B
 class A:
@@ -1875,7 +1875,7 @@ b = a.bar
 [out]
 main:9: error: Incompatible types in assignment (expression has type "A", variable has type "B")
 
-[case testGetAttributeSignature]
+[case testGetattributeSignature]
 class A:
     def __getattribute__(self, x: str) -> A: pass
 class B:
@@ -1885,10 +1885,10 @@ class C:
 class D:
     def __getattribute__(self, x: str) -> None: pass
 [out]
-main:4: error: Invalid signature "def (__main__.B, __main__.A) -> __main__.B"
-main:6: error: Invalid signature "def (__main__.C, builtins.str, builtins.str) -> __main__.C"
+main:4: error: Invalid signature "def (__main__.B, __main__.A) -> __main__.B" for "__getattribute__"
+main:6: error: Invalid signature "def (__main__.C, builtins.str, builtins.str) -> __main__.C" for "__getattribute__"
 
-[case testGetAttr]
+[case testGetattr]
 
 a, b = None, None # type: A, B
 class A:
@@ -1901,7 +1901,19 @@ b = a.bar
 [out]
 main:9: error: Incompatible types in assignment (expression has type "A", variable has type "B")
 
-[case testGetAttrSignature]
+[case testNestedGetattr]
+def foo() -> object:
+    def __getattr__() -> None:  # no error because not in a class
+        pass
+    return __getattr__
+
+class X:
+    def foo(self) -> object:
+        def __getattr__() -> None:  # no error because not directly inside a class
+            pass
+        return __getattr__
+
+[case testGetattrSignature]
 class A:
     def __getattr__(self, x: str) -> A: pass
 class B:
@@ -1911,10 +1923,10 @@ class C:
 class D:
     def __getattr__(self, x: str) -> None: pass
 [out]
-main:4: error: Invalid signature "def (__main__.B, __main__.A) -> __main__.B"
-main:6: error: Invalid signature "def (__main__.C, builtins.str, builtins.str) -> __main__.C"
+main:4: error: Invalid signature "def (__main__.B, __main__.A) -> __main__.B" for "__getattr__"
+main:6: error: Invalid signature "def (__main__.C, builtins.str, builtins.str) -> __main__.C" for "__getattr__"
 
-[case testSetAttr]
+[case testSetattr]
 from typing import Union, Any
 class A:
     def __setattr__(self, name: str, value: Any) -> None: ...
@@ -1936,7 +1948,7 @@ c = C()
 c.fail = 4  # E: Incompatible types in assignment (expression has type "int", variable has type "str")
 
 class D:
-    __setattr__ = 'hello'
+    __setattr__ = 'hello'  # E: Invalid signature "builtins.str" for "__setattr__"
 
 d = D()
 d.crash = 4  # E: "D" has no attribute "crash"
@@ -1957,14 +1969,16 @@ s = Sub()
 s.success = 4
 s.fail = 'fail'  # E: Incompatible types in assignment (expression has type "str", variable has type "int")
 
-[case testSetAttrSignature]
+[case testSetattrSignature]
+from typing import Any
+
 class Test:
-    def __setattr__() -> None: ...  # E: Method must have at least one argument # E: Invalid signature "def ()"
+    def __setattr__() -> None: ...  # E: Method must have at least one argument # E: Invalid signature "def ()" for "__setattr__"
 t = Test()
 t.crash = 'test'  # E: "Test" has no attribute "crash"
 
 class A:
-    def __setattr__(self): ...  # E: Invalid signature "def (self: Any) -> Any"
+    def __setattr__(self): ...  # E: Invalid signature "def (self: Any) -> Any" for "__setattr__"
 a = A()
 a.test = 4  # E: "A" has no attribute "test"
 
@@ -1974,11 +1988,14 @@ b = B()
 b.integer = 5
 
 class C:
-    def __setattr__(self, name: int, value: int) -> None: ...  # E: Invalid signature "def (__main__.C, builtins.int, builtins.int)"
+    def __setattr__(self, name: int, value: int) -> None: ...  # E: Invalid signature "def (__main__.C, builtins.int, builtins.int)" for "__setattr__"
 c = C()
 c.check = 13
 
-[case testGetAttrAndSetattr]
+class X:
+    __setattr__ = ...  # type: Any
+
+[case testGetattrAndSetattr]
 from typing import Any
 class A:
     def __setattr__(self, name: str, value: Any) -> None: ...
diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test
--- a/test-data/unit/check-modules.test
+++ b/test-data/unit/check-modules.test
@@ -1895,7 +1895,7 @@ reveal_type(has_getattr.any_attribute)
 def __getattr__(x: int, y: str) -> str: ...
 
 [out]
-tmp/has_getattr.pyi:1: error: Invalid signature "def (builtins.int, builtins.str) -> builtins.str"
+tmp/has_getattr.pyi:1: error: Invalid signature "def (builtins.int, builtins.str) -> builtins.str" for "__getattr__"
 main:3: error: Revealed type is 'builtins.str'
 
 [builtins fixtures/module.pyi]
@@ -1903,11 +1903,15 @@ main:3: error: Revealed type is 'builtins.str'
 [case testModuleLevelGetattrNotCallable]
 import has_getattr
 
-reveal_type(has_getattr.any_attribute)  # E: Revealed type is 'Any'  # E: Module has no attribute "any_attribute"
+reveal_type(has_getattr.any_attribute)
 
 [file has_getattr.pyi]
 __getattr__ = 3
 
+[out]
+tmp/has_getattr.pyi:1: error: Invalid signature "builtins.int" for "__getattr__"
+main:3: error: Revealed type is 'Any'
+
 [builtins fixtures/module.pyi]
 
 [case testModuleLevelGetattrUntyped]
@@ -1919,18 +1923,24 @@ def __getattr__(name): ...
 
 [builtins fixtures/module.pyi]
 
-[case testModuleLevelGetattrNotStub]
-
+[case testModuleLevelGetattrNotStub36]
+# flags: --python-version 3.6
 import has_getattr
-reveal_type(has_getattr.any_attribute)
+reveal_type(has_getattr.any_attribute)  # E: Revealed type is 'Any'  # E: Module has no attribute "any_attribute"
 
 [file has_getattr.py]
-def __getattr__(name): ...
+def __getattr__(name) -> str: ...
 
-[out]
-tmp/has_getattr.py:1: error: __getattr__ is not valid at the module level outside a stub file
-main:3: error: Revealed type is 'Any'
-main:3: error: Module has no attribute "any_attribute"
+[builtins fixtures/module.pyi]
+
+[case testModuleLevelGetattrNotStub37]
+# flags: --python-version 3.7
+
+import has_getattr
+reveal_type(has_getattr.any_attribute)  # E: Revealed type is 'builtins.str'
+
+[file has_getattr.py]
+def __getattr__(name) -> str: ...
 
 [builtins fixtures/module.pyi]
 
@@ -1957,18 +1967,25 @@ def __getattr__(name: str) -> int: ...
 
 [builtins fixtures/module.pyi]
 
-[case testModuleLevelGetattrImportFromNotStub]
-from non_stub import name
-reveal_type(name)
+[case testModuleLevelGetattrImportFromNotStub36]
+# flags: --python-version 3.6
+from non_stub import name  # E: Module 'non_stub' has no attribute 'name'
+reveal_type(name)  # E: Revealed type is 'Any'
 
 [file non_stub.py]
 from typing import Any
 def __getattr__(name: str) -> Any: ...
 
-[out]
-tmp/non_stub.py:2: error: __getattr__ is not valid at the module level outside a stub file
-main:1: error: Module 'non_stub' has no attribute 'name'
-main:2: error: Revealed type is 'Any'
+[builtins fixtures/module.pyi]
+
+[case testModuleLevelGetattrImportFromNotStub37]
+# flags: --python-version 3.7
+from non_stub import name
+reveal_type(name)  # E: Revealed type is 'Any'
+
+[file non_stub.py]
+from typing import Any
+def __getattr__(name: str) -> Any: ...
 
 [builtins fixtures/module.pyi]
 
@@ -1988,6 +2005,60 @@ main:3: error: Revealed type is 'Any'
 
 [builtins fixtures/module.pyi]
 
+[case testModuleLevelGetattrAssignedGood]
+# flags: --python-version 3.7
+import non_stub
+reveal_type(non_stub.name)  # E: Revealed type is 'builtins.int'
+
+[file non_stub.py]
+from typing import Callable
+
+def make_getattr_good() -> Callable[[str], int]: ...
+__getattr__ = make_getattr_good()  # OK
+
+[case testModuleLevelGetattrAssignedBad]
+# flags: --python-version 3.7
+import non_stub
+reveal_type(non_stub.name)
+
+[file non_stub.py]
+from typing import Callable
+
+def make_getattr_bad() -> Callable[[], int]: ...
+__getattr__ = make_getattr_bad()
+
+[out]
+tmp/non_stub.py:4: error: Invalid signature "def () -> builtins.int" for "__getattr__"
+main:3: error: Revealed type is 'builtins.int'
+
+[case testModuleLevelGetattrImportedGood]
+# flags: --python-version 3.7
+import non_stub
+reveal_type(non_stub.name)  # E: Revealed type is 'builtins.int'
+
+[file non_stub.py]
+from has_getattr import __getattr__
+
+[file has_getattr.py]
+def __getattr__(name: str) -> int: ...
+
+[case testModuleLevelGetattrImportedBad]
+# flags: --python-version 3.7
+import non_stub
+reveal_type(non_stub.name)
+
+[file non_stub.py]
+from has_getattr import __getattr__
+
+[file has_getattr.py]
+def __getattr__() -> int: ...
+
+[out]
+tmp/has_getattr.py:1: error: Invalid signature "def () -> builtins.int" for "__getattr__"
+main:3: error: Revealed type is 'builtins.int'
+
+[builtins fixtures/module.pyi]
+
 [case testFailedImportFromTwoModules]
 import c
 import b