Repository: python/mypy

Issue:
Add support for incomplete stubs as defined in PEP 484
> As a better solution, PEP 484 states that a top-level `def __getattr__(name) -> Any: ...` in a stub should mean that the stub is incomplete. (I'd completely forgotten about this, sorry.) See also #5. I don't think mypy currently supports this, and I can't find an existing issue for that, so I propose we should open an issue there.

https://github.com/python/typeshed/issues/708


Discussion:

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -634,7 +634,7 @@ def is_implicit_any(t: Type) -> bool:
                 if name in nodes.reverse_op_method_set:
                     self.check_reverse_op_method(item, typ, name)
                 elif name in ('__getattr__', '__getattribute__'):
-                    self.check_getattr_method(typ, defn)
+                    self.check_getattr_method(typ, defn, name)
                 elif name == '__setattr__':
                     self.check_setattr_method(typ, defn)
                 # Refuse contravariant return type variable
@@ -927,12 +927,27 @@ 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) -> None:
-        method_type = CallableType([AnyType(), self.named_type('builtins.str')],
-                                   [nodes.ARG_POS, nodes.ARG_POS],
-                                   [None, None],
-                                   AnyType(),
-                                   self.named_type('builtins.function'))
+    def check_getattr_method(self, typ: CallableType, context: Context, name: str) -> None:
+        if len(self.scope.stack) == 1:
+            # module-level __getattr__
+            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
+            method_type = CallableType([self.named_type('builtins.str')],
+                                       [nodes.ARG_POS],
+                                       [None],
+                                       AnyType(),
+                                       self.named_type('builtins.function'))
+        else:
+            method_type = CallableType([AnyType(), self.named_type('builtins.str')],
+                                       [nodes.ARG_POS, nodes.ARG_POS],
+                                       [None, None],
+                                       AnyType(),
+                                       self.named_type('builtins.function'))
         if not is_subtype(typ, method_type):
             self.msg.invalid_signature(typ, context)
 
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -3117,6 +3117,18 @@ 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:
+                # 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):
+                    if isinstance(getattr_defn.node.type, CallableType):
+                        typ = getattr_defn.node.type.ret_type
+                    else:
+                        typ = AnyType()
+                    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-modules.test b/test-data/unit/check-modules.test
--- a/test-data/unit/check-modules.test
+++ b/test-data/unit/check-modules.test
@@ -1640,3 +1640,77 @@ m = n  # E: Cannot assign multiple modules to name 'm' without explicit 'types.M
 [file n.py]
 
 [builtins fixtures/module.pyi]
+
+[case testModuleLevelGetattr]
+import has_getattr
+
+reveal_type(has_getattr.any_attribute)  # E: Revealed type is 'Any'
+
+[file has_getattr.pyi]
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
+
+[builtins fixtures/module.pyi]
+
+[case testModuleLevelGetattrReturnType]
+import has_getattr
+
+reveal_type(has_getattr.any_attribute)  # E: Revealed type is 'builtins.str'
+
+[file has_getattr.pyi]
+def __getattr__(name: str) -> str: ...
+
+[builtins fixtures/module.pyi]
+
+[case testModuleLevelGetattrInvalidSignature]
+import has_getattr
+
+reveal_type(has_getattr.any_attribute)
+
+[file has_getattr.pyi]
+def __getattr__(x: int, y: str) -> str: ...
+
+[out]
+tmp/has_getattr.pyi:1: error: Invalid signature "def (builtins.int, builtins.str) -> builtins.str"
+main:3: error: Revealed type is 'builtins.str'
+
+[builtins fixtures/module.pyi]
+
+[case testModuleLevelGetattrNotCallable]
+import has_getattr
+
+reveal_type(has_getattr.any_attribute)  # E: Revealed type is 'Any'  # E: Module has no attribute "any_attribute"
+
+[file has_getattr.pyi]
+__getattr__ = 3
+
+[builtins fixtures/module.pyi]
+
+[case testModuleLevelGetattrUntyped]
+import has_getattr
+reveal_type(has_getattr.any_attribute)  # E: Revealed type is 'Any'
+
+[file has_getattr.pyi]
+def __getattr__(name): ...
+
+[builtins fixtures/module.pyi]
+
+[case testModuleLevelGetattrNotStub]
+
+import has_getattr
+reveal_type(has_getattr.any_attribute)
+
+[file has_getattr.py]
+def __getattr__(name): ...
+
+[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 testModuleLevelGetattribute]
+
+def __getattribute__(): ...  # E: __getattribute__ is not valid at the module level