Skip to content

Commit c38edf3

Browse files
carljmilevkivskyi
authored andcommitted
Support accessing modules imported in class bodies within methods. (#3450)
* Support accessing modules imported in class bodies within methods. * Use self.type to simplify getting current class. * Avoid getting confused by the first arg of a staticmethod.
1 parent f60d874 commit c38edf3

File tree

3 files changed

+53
-15
lines changed

3 files changed

+53
-15
lines changed

mypy/semanal.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2927,21 +2927,34 @@ def visit_member_expr(self, expr: MemberExpr) -> None:
29272927
if full_name in obsolete_name_mapping:
29282928
self.fail("Module%s has no attribute %r (it's now called %r)" % (
29292929
mod_name, expr.name, obsolete_name_mapping[full_name]), expr)
2930-
elif isinstance(base, RefExpr) and isinstance(base.node, TypeInfo):
2931-
n = base.node.names.get(expr.name)
2932-
if n is not None and (n.kind == MODULE_REF or isinstance(n.node, TypeInfo)):
2933-
# This branch handles the case C.bar where C is a class and
2934-
# bar is a type definition or a module resulting from
2935-
# `import bar` inside class C. Here base.node is a TypeInfo,
2936-
# and again we look up the name in its namespace.
2937-
# This is done only when bar is a module or a type; other
2938-
# things (e.g. methods) are handled by other code in checkmember.
2939-
n = self.normalize_type_alias(n, expr)
2940-
if not n:
2941-
return
2942-
expr.kind = n.kind
2943-
expr.fullname = n.fullname
2944-
expr.node = n.node
2930+
elif isinstance(base, RefExpr):
2931+
# This branch handles the case C.bar (or cls.bar or self.bar inside
2932+
# a classmethod/method), where C is a class and bar is a type
2933+
# definition or a module resulting from `import bar` (or a module
2934+
# assignment) inside class C. We look up bar in the class' TypeInfo
2935+
# namespace. This is done only when bar is a module or a type;
2936+
# other things (e.g. methods) are handled by other code in
2937+
# checkmember.
2938+
type_info = None
2939+
if isinstance(base.node, TypeInfo):
2940+
# C.bar where C is a class
2941+
type_info = base.node
2942+
elif isinstance(base.node, Var) and self.type and self.function_stack:
2943+
# check for self.bar or cls.bar in method/classmethod
2944+
func_def = self.function_stack[-1]
2945+
if not func_def.is_static and isinstance(func_def.type, CallableType):
2946+
formal_arg = func_def.type.argument_by_name(base.node.name())
2947+
if formal_arg and formal_arg.pos == 0:
2948+
type_info = self.type
2949+
if type_info:
2950+
n = type_info.names.get(expr.name)
2951+
if n is not None and (n.kind == MODULE_REF or isinstance(n.node, TypeInfo)):
2952+
n = self.normalize_type_alias(n, expr)
2953+
if not n:
2954+
return
2955+
expr.kind = n.kind
2956+
expr.fullname = n.fullname
2957+
expr.node = n.node
29452958

29462959
def visit_op_expr(self, expr: OpExpr) -> None:
29472960
expr.left.accept(self)

test-data/unit/check-modules.test

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,3 +1416,26 @@ reveal_type(f()) # E: Revealed type is 'types.ModuleType'
14161416
reveal_type(types) # E: Revealed type is 'types.ModuleType'
14171417

14181418
[builtins fixtures/module.pyi]
1419+
1420+
[case testClassImportAccessedInMethod]
1421+
class C:
1422+
import m
1423+
def foo(self) -> None:
1424+
x = self.m.a
1425+
reveal_type(x) # E: Revealed type is 'builtins.str'
1426+
# ensure we distinguish self from other variables
1427+
y = 'hello'
1428+
z = y.m.a # E: "str" has no attribute "m"
1429+
@classmethod
1430+
def cmethod(cls) -> None:
1431+
y = cls.m.a
1432+
reveal_type(y) # E: Revealed type is 'builtins.str'
1433+
@staticmethod
1434+
def smethod(foo: int) -> None:
1435+
# we aren't confused by first arg of a staticmethod
1436+
y = foo.m.a # E: "int" has no attribute "m"
1437+
1438+
[file m.py]
1439+
a = 'foo'
1440+
1441+
[builtins fixtures/module.pyi]

test-data/unit/fixtures/module.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ class tuple: pass
1717
class dict(Generic[T, S]): pass
1818
class ellipsis: pass
1919

20+
classmethod = object()
21+
staticmethod = object()

0 commit comments

Comments
 (0)