Repository: python/mypy

Issue:
Support __getattr__
Support overloading attribute access by defining a `__getattr__` method. If a class has a `__getattr__` method, mypy should let programs access any attributes, and the type would be the return type of `__getattr__`. Example:

``` python
class A:
    def __getattr__(self, name: str) -> int: return 2
a = A()
print(a.foobar)   # okay, type is int
```



Discussion:

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -506,6 +506,8 @@ def check_func_def(self, defn: FuncItem, typ: Callable, name: str) -> None:
 
             if name in nodes.reverse_op_method_set:
                 self.check_reverse_op_method(item, typ, name)
+            elif name == '__getattr__':
+                self.check_getattr_method(typ, defn)
 
             # Push return type.
             self.return_types.append(typ.ret_type)
@@ -707,6 +709,15 @@ 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: Callable, context: Context) -> None:
+        method_type = Callable([AnyType(), self.named_type('builtins.str')],
+                               [nodes.ARG_POS, nodes.ARG_POS],
+                               [None],
+                               AnyType(),
+                               self.named_type('builtins.function'))
+        if not is_subtype(typ, method_type):
+            self.msg.invalid_signature(typ, context)
+
     def expand_typevars(self, defn: FuncItem,
                         typ: Callable) -> List[Tuple[FuncItem, Callable]]:
         # TODO use generator
diff --git a/mypy/checkmember.py b/mypy/checkmember.py
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -55,7 +55,8 @@ def analyse_member_access(name: str, typ: Type, node: Context, is_lvalue: bool,
         else:
             # Not a method.
             return analyse_member_var_access(name, typ, info, node,
-                                             is_lvalue, is_super, msg,
+                                             is_lvalue, is_super, builtin_type,
+                                             msg,
                                              report_type=report_type)
     elif isinstance(typ, AnyType):
         # The base object has dynamic type.
@@ -93,6 +94,7 @@ def analyse_member_access(name: str, typ: Type, node: Context, is_lvalue: bool,
 
 def analyse_member_var_access(name: str, itype: Instance, info: TypeInfo,
                               node: Context, is_lvalue: bool, is_super: bool,
+                              builtin_type: Function[[str], Instance],
                               msg: MessageBuilder,
                               report_type: Type = None) -> Type:
     """Analyse attribute access that does not target a method.
@@ -142,6 +144,15 @@ def analyse_member_var_access(name: str, itype: Instance, info: TypeInfo,
             return AnyType()
     elif isinstance(v, FuncDef):
         assert False, "Did not expect a function"
+    elif not v and name not in ['__getattr__', '__setattr__']:
+        if not is_lvalue:
+            method = info.get_method('__getattr__')
+            if method:
+                typ = map_instance_to_supertype(itype, method.info)
+                getattr_type = expand_type_by_instance(
+                    method_type(method, builtin_type('builtins.function')), typ)
+                if isinstance(getattr_type, Callable):
+                    return getattr_type.ret_type
 
     # Could not find the member.
     if is_super:
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -698,6 +698,9 @@ def yield_from_invalid_operand_type(self, expr: Type, context: Context) -> Type:
         self.fail('"yield from" can\'t be applied to {}'.format(text), context)
         return AnyType()
 
+    def invalid_signature(self, func_type: Type, context: Context) -> None:
+        self.fail('Invalid signature "{}"'.format(func_type), context)
+
 
 def capitalize(s: str) -> str:
     """Capitalize the first character of a string."""


Test patch:
diff --git a/mypy/test/data/check-classes.test b/mypy/test/data/check-classes.test
--- a/mypy/test/data/check-classes.test
+++ b/mypy/test/data/check-classes.test
@@ -1133,6 +1133,36 @@ main, line 8: Argument 1 of "__iadd__" incompatible with "__add__" of supertype
 main, line 8: Signatures of "__iadd__" and "__add__" are incompatible
 
 
+[case testGetAttr]
+from typing import Undefined
+a, b = Undefined, Undefined  # type: A, B
+class A:
+    def __getattr__(self, x: str) -> A:
+        return A()
+class B: pass
+
+a = a.foo
+b = a.bar
+[out]
+main, line 9: Incompatible types in assignment (expression has type "A", variable has type "B")
+
+
+[case testGetAttrSignature]
+class A:
+    def __getattr__(self, x: str) -> A: pass
+class B:
+    def __getattr__(self, x: A) -> B: pass
+class C:
+    def __getattr__(self, x: str, y: str) -> C: pass
+class D:
+    def __getattr__(self, x: str) -> None: pass
+[out]
+main: In member "__getattr__" of class "B":
+main, line 4: Invalid signature "def (self: __main__.B, x: __main__.A) -> __main__.B"
+main: In member "__getattr__" of class "C":
+main, line 6: Invalid signature "def (self: __main__.C, x: builtins.str, y: builtins.str) -> __main__.C"
+
+
 -- Callable objects
 -- ----------------