Skip to content

Commit e81e25e

Browse files
committed
Added type checking of __getattr__ method signatures
1 parent e0494fc commit e81e25e

File tree

4 files changed

+33
-4
lines changed

4 files changed

+33
-4
lines changed

mypy/checker.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,8 @@ def check_func_def(self, defn: FuncItem, typ: Callable, name: str) -> None:
506506

507507
if name in nodes.reverse_op_method_set:
508508
self.check_reverse_op_method(item, typ, name)
509+
elif name == '__getattr__':
510+
self.check_getattr_method(typ, defn)
509511

510512
# Push return type.
511513
self.return_types.append(typ.ret_type)
@@ -707,6 +709,15 @@ def check_inplace_operator_method(self, defn: FuncBase) -> None:
707709
if fail:
708710
self.msg.signatures_incompatible(method, other_method, defn)
709711

712+
def check_getattr_method(self, typ: Callable, context: Context) -> None:
713+
method_type = Callable([AnyType(), self.named_type('builtins.str')],
714+
[nodes.ARG_POS, nodes.ARG_POS],
715+
[None],
716+
AnyType(),
717+
self.named_type('builtins.function'))
718+
if not is_subtype(typ, method_type):
719+
self.msg.invalid_signature(typ, context)
720+
710721
def expand_typevars(self, defn: FuncItem,
711722
typ: Callable) -> List[Tuple[FuncItem, Callable]]:
712723
# TODO use generator

mypy/checkmember.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,15 +145,14 @@ def analyse_member_var_access(name: str, itype: Instance, info: TypeInfo,
145145
elif isinstance(v, FuncDef):
146146
assert False, "Did not expect a function"
147147
elif not v and name not in ['__getattr__', '__setattr__']:
148-
if is_lvalue:
149-
pass # TODO
150-
else:
148+
if not is_lvalue:
151149
method = info.get_method('__getattr__')
152150
if method:
153151
typ = map_instance_to_supertype(itype, method.info)
154152
getattr_type = expand_type_by_instance(
155153
method_type(method, builtin_type('builtins.function')), typ)
156-
return getattr_type.ret_type
154+
if isinstance(getattr_type, Callable):
155+
return getattr_type.ret_type
157156

158157
# Could not find the member.
159158
if is_super:

mypy/messages.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,9 @@ def yield_from_invalid_operand_type(self, expr: Type, context: Context) -> Type:
698698
self.fail('"yield from" can\'t be applied to {}'.format(text), context)
699699
return AnyType()
700700

701+
def invalid_signature(self, func_type: Type, context: Context) -> None:
702+
self.fail('Invalid signature "{}"'.format(func_type), context)
703+
701704

702705
def capitalize(s: str) -> str:
703706
"""Capitalize the first character of a string."""

mypy/test/data/check-classes.test

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,22 @@ b = a.bar
11471147
main, line 9: Incompatible types in assignment (expression has type "A", variable has type "B")
11481148

11491149

1150+
[case testGetAttrSignature]
1151+
class A:
1152+
def __getattr__(self, x: str) -> A: pass
1153+
class B:
1154+
def __getattr__(self, x: A) -> B: pass
1155+
class C:
1156+
def __getattr__(self, x: str, y: str) -> C: pass
1157+
class D:
1158+
def __getattr__(self, x: str) -> None: pass
1159+
[out]
1160+
main: In member "__getattr__" of class "B":
1161+
main, line 4: Invalid signature "def (self: __main__.B, x: __main__.A) -> __main__.B"
1162+
main: In member "__getattr__" of class "C":
1163+
main, line 6: Invalid signature "def (self: __main__.C, x: builtins.str, y: builtins.str) -> __main__.C"
1164+
1165+
11501166
-- Callable objects
11511167
-- ----------------
11521168

0 commit comments

Comments
 (0)