From 90f2bc84a3dc7b7ed020121ad7a9adf824efdc8f Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 24 May 2017 20:49:47 -0700 Subject: [PATCH 1/3] Support accessing modules imported in class bodies within methods. --- mypy/semanal.py | 51 +++++++++++++++++++++--------- test-data/unit/check-modules.test | 16 ++++++++++ test-data/unit/fixtures/module.pyi | 1 + 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index d16be31e8e18..7bac80913910 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2931,21 +2931,42 @@ def visit_member_expr(self, expr: MemberExpr) -> None: if full_name in obsolete_name_mapping: self.fail("Module%s has no attribute %r (it's now called %r)" % ( mod_name, expr.name, obsolete_name_mapping[full_name]), expr) - elif isinstance(base, RefExpr) and isinstance(base.node, TypeInfo): - n = base.node.names.get(expr.name) - if n is not None and (n.kind == MODULE_REF or isinstance(n.node, TypeInfo)): - # This branch handles the case C.bar where C is a class and - # bar is a type definition or a module resulting from - # `import bar` inside class C. Here base.node is a TypeInfo, - # and again we look up the name in its namespace. - # This is done only when bar is a module or a type; other - # things (e.g. methods) are handled by other code in checkmember. - n = self.normalize_type_alias(n, expr) - if not n: - return - expr.kind = n.kind - expr.fullname = n.fullname - expr.node = n.node + elif isinstance(base, RefExpr): + # This branch handles the case C.bar (or cls.bar or self.bar inside + # a classmethod/method), where C is a class and bar is a type + # definition or a module resulting from `import bar` (or a module + # assignment) inside class C. We look up bar in the class' TypeInfo + # namespace. This is done only when bar is a module or a type; + # other things (e.g. methods) are handled by other code in + # checkmember. + type_info = None + if isinstance(base.node, TypeInfo): + # C.bar where C is a class + type_info = base.node + elif isinstance(base.node, Var) and self.function_stack: + # check for self.bar or cls.bar in method/classmethod + func = self.function_stack[-1].type + if isinstance(func, CallableType): + formal_arg = func.argument_by_name(base.node.name()) + if formal_arg and formal_arg.pos == 0: + if ( + isinstance(formal_arg.typ, CallableType) and + isinstance(formal_arg.typ.ret_type, Instance) + ): + # classmethod + type_info = formal_arg.typ.ret_type.type + elif isinstance(formal_arg.typ, Instance): + # instance method + type_info = formal_arg.typ.type + if type_info: + n = type_info.names.get(expr.name) + if n is not None and (n.kind == MODULE_REF or isinstance(n.node, TypeInfo)): + n = self.normalize_type_alias(n, expr) + if not n: + return + expr.kind = n.kind + expr.fullname = n.fullname + expr.node = n.node def visit_op_expr(self, expr: OpExpr) -> None: expr.left.accept(self) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 66050601d26f..a1394507bd53 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1416,3 +1416,19 @@ reveal_type(f()) # E: Revealed type is 'types.ModuleType' reveal_type(types) # E: Revealed type is 'types.ModuleType' [builtins fixtures/module.pyi] + +[case testClassImportAccessedInMethod] +class C: + import m + def foo(self) -> None: + x = self.m.a + reveal_type(x) # E: Revealed type is 'builtins.str' + @classmethod + def cmethod(cls) -> None: + y = cls.m.a + reveal_type(y) # E: Revealed type is 'builtins.str' + +[file m.py] +a = 'foo' + +[builtins fixtures/module.pyi] diff --git a/test-data/unit/fixtures/module.pyi b/test-data/unit/fixtures/module.pyi index b130d795d25c..db95e8ba579b 100644 --- a/test-data/unit/fixtures/module.pyi +++ b/test-data/unit/fixtures/module.pyi @@ -17,3 +17,4 @@ class tuple: pass class dict(Generic[T, S]): pass class ellipsis: pass +classmethod = object() From 90a196f800d768ce4cecf07f3c2fa97edbec9724 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 30 May 2017 13:26:42 -0700 Subject: [PATCH 2/3] Use self.type to simplify getting current class. --- mypy/semanal.py | 12 ++---------- test-data/unit/check-modules.test | 3 +++ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 7bac80913910..49d3ce04c84b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2943,21 +2943,13 @@ def visit_member_expr(self, expr: MemberExpr) -> None: if isinstance(base.node, TypeInfo): # C.bar where C is a class type_info = base.node - elif isinstance(base.node, Var) and self.function_stack: + elif isinstance(base.node, Var) and self.type and self.function_stack: # check for self.bar or cls.bar in method/classmethod func = self.function_stack[-1].type if isinstance(func, CallableType): formal_arg = func.argument_by_name(base.node.name()) if formal_arg and formal_arg.pos == 0: - if ( - isinstance(formal_arg.typ, CallableType) and - isinstance(formal_arg.typ.ret_type, Instance) - ): - # classmethod - type_info = formal_arg.typ.ret_type.type - elif isinstance(formal_arg.typ, Instance): - # instance method - type_info = formal_arg.typ.type + type_info = self.type if type_info: n = type_info.names.get(expr.name) if n is not None and (n.kind == MODULE_REF or isinstance(n.node, TypeInfo)): diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index a1394507bd53..f7dc15059bd4 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1423,6 +1423,9 @@ class C: def foo(self) -> None: x = self.m.a reveal_type(x) # E: Revealed type is 'builtins.str' + # ensure we distinguish self from other variables + y = 'hello' + z = y.m.a # E: "str" has no attribute "m" @classmethod def cmethod(cls) -> None: y = cls.m.a From a67cef30d4c2a5406ae48c05b99b4dd0efcde0f0 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 30 May 2017 13:42:01 -0700 Subject: [PATCH 3/3] Avoid getting confused by the first arg of a staticmethod. --- mypy/semanal.py | 6 +++--- test-data/unit/check-modules.test | 4 ++++ test-data/unit/fixtures/module.pyi | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 49d3ce04c84b..cb29377a2f9c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2945,9 +2945,9 @@ def visit_member_expr(self, expr: MemberExpr) -> None: type_info = base.node elif isinstance(base.node, Var) and self.type and self.function_stack: # check for self.bar or cls.bar in method/classmethod - func = self.function_stack[-1].type - if isinstance(func, CallableType): - formal_arg = func.argument_by_name(base.node.name()) + func_def = self.function_stack[-1] + if not func_def.is_static and isinstance(func_def.type, CallableType): + formal_arg = func_def.type.argument_by_name(base.node.name()) if formal_arg and formal_arg.pos == 0: type_info = self.type if type_info: diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index f7dc15059bd4..ec3eb7e9e523 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1430,6 +1430,10 @@ class C: def cmethod(cls) -> None: y = cls.m.a reveal_type(y) # E: Revealed type is 'builtins.str' + @staticmethod + def smethod(foo: int) -> None: + # we aren't confused by first arg of a staticmethod + y = foo.m.a # E: "int" has no attribute "m" [file m.py] a = 'foo' diff --git a/test-data/unit/fixtures/module.pyi b/test-data/unit/fixtures/module.pyi index db95e8ba579b..44a4dfe0c277 100644 --- a/test-data/unit/fixtures/module.pyi +++ b/test-data/unit/fixtures/module.pyi @@ -18,3 +18,4 @@ class dict(Generic[T, S]): pass class ellipsis: pass classmethod = object() +staticmethod = object()