Skip to content

Commit 8127807

Browse files
authored
New semantic analyzer: fix access to imported name in class body (#6943)
We shouldn't use the line number of an imported name to decide whether it's defined when the line number refers to another file.
1 parent d51d85b commit 8127807

File tree

7 files changed

+98
-43
lines changed

7 files changed

+98
-43
lines changed

mypy/build.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
from mypy.checker import TypeChecker
4343
from mypy.indirection import TypeIndirectionVisitor
4444
from mypy.errors import Errors, CompileError, ErrorInfo, report_internal_error
45-
from mypy.util import DecodeError, decode_python_encoding, is_sub_path, get_mypy_comments
45+
from mypy.util import (
46+
DecodeError, decode_python_encoding, is_sub_path, get_mypy_comments, module_prefix
47+
)
4648
if MYPY:
4749
from mypy.report import Reports # Avoid unconditional slow import
4850
from mypy import moduleinfo
@@ -875,7 +877,7 @@ def invert_deps(deps: Dict[str, Set[str]],
875877
fake module FAKE_ROOT_MODULE if none are.
876878
"""
877879
# Lazy import to speed up startup
878-
from mypy.server.target import module_prefix, trigger_to_target
880+
from mypy.server.target import trigger_to_target
879881

880882
# Prepopulate the map for all the modules that have been processed,
881883
# so that we always generate files for processed modules (even if

mypy/newsemanal/semanal.py

+32-16
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@
9999
Plugin, ClassDefContext, SemanticAnalyzerPluginInterface,
100100
DynamicClassDefContext
101101
)
102-
from mypy.util import get_prefix, correct_relative_import, unmangle, split_module_names
102+
from mypy.util import (
103+
get_prefix, correct_relative_import, unmangle, module_prefix
104+
)
103105
from mypy.scope import Scope
104106
from mypy.newsemanal.semanal_shared import (
105107
SemanticAnalyzerInterface, set_callable_name, calculate_tuple_fallback, PRIORITY_FALLBACKS
@@ -3752,20 +3754,7 @@ def lookup(self, name: str, ctx: Context,
37523754
if self.type and not self.is_func_scope() and name in self.type.names:
37533755
node = self.type.names[name]
37543756
if not node.implicit:
3755-
# Only allow access to class attributes textually after
3756-
# the definition, so that it's possible to fall back to the
3757-
# outer scope. Example:
3758-
#
3759-
# class X: ...
3760-
#
3761-
# class C:
3762-
# X = X # Initializer refers to outer scope
3763-
#
3764-
# Nested classes are an exception, since we want to support
3765-
# arbitrary forward references in type annotations.
3766-
if (node.node is None
3767-
or node.node.line < self.statement.line
3768-
or isinstance(node.node, TypeInfo)):
3757+
if self.is_active_symbol_in_class_body(node.node):
37693758
return node
37703759
else:
37713760
# Defined through self.x assignment
@@ -3798,6 +3787,33 @@ def lookup(self, name: str, ctx: Context,
37983787
return implicit_node
37993788
return None
38003789

3790+
def is_active_symbol_in_class_body(self, node: Optional[SymbolNode]) -> bool:
3791+
"""Can a symbol defined in class body accessed at current statement?
3792+
3793+
Only allow access to class attributes textually after
3794+
the definition, so that it's possible to fall back to the
3795+
outer scope. Example:
3796+
3797+
class X: ...
3798+
3799+
class C:
3800+
X = X # Initializer refers to outer scope
3801+
3802+
Nested classes are an exception, since we want to support
3803+
arbitrary forward references in type annotations.
3804+
"""
3805+
# TODO: Forward reference to name imported in class body is not
3806+
# caught.
3807+
return (node is None
3808+
or node.line < self.statement.line
3809+
or not self.is_defined_in_current_module(node.fullname())
3810+
or isinstance(node, TypeInfo))
3811+
3812+
def is_defined_in_current_module(self, fullname: Optional[str]) -> bool:
3813+
if fullname is None:
3814+
return False
3815+
return module_prefix(self.modules, fullname) == self.cur_mod_id
3816+
38013817
def lookup_qualified(self, name: str, ctx: Context,
38023818
suppress_errors: bool = False) -> Optional[SymbolTableNode]:
38033819
if '.' not in name:
@@ -4455,7 +4471,7 @@ def attribute_already_defined(self,
44554471

44564472
def is_local_name(self, name: str) -> bool:
44574473
"""Does name look like reference to a definition in the current module?"""
4458-
return self.cur_mod_id in split_module_names(name) or '.' not in name
4474+
return self.is_defined_in_current_module(name) or '.' not in name
44594475

44604476
def fail(self,
44614477
msg: str,

mypy/server/target.py

-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
from typing import Iterable, Tuple, List, Optional
2-
3-
41
def trigger_to_target(s: str) -> str:
52
assert s[0] == '<'
63
# Strip off the angle brackets
@@ -9,22 +6,3 @@ def trigger_to_target(s: str) -> str:
96
if s[-1] == ']':
107
s = s.split('[')[0]
118
return s
12-
13-
14-
def module_prefix(modules: Iterable[str], target: str) -> Optional[str]:
15-
result = split_target(modules, target)
16-
if result is None:
17-
return None
18-
return result[0]
19-
20-
21-
def split_target(modules: Iterable[str], target: str) -> Optional[Tuple[str, str]]:
22-
remaining = [] # type: List[str]
23-
while True:
24-
if target in modules:
25-
return target, '.'.join(remaining)
26-
components = target.rsplit('.', 1)
27-
if len(components) == 1:
28-
return None
29-
target = components[0]
30-
remaining.insert(0, components[1])

mypy/server/update.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,9 @@
140140
from mypy.server.aststrip import strip_target
141141
from mypy.server.aststripnew import strip_target_new, SavedAttributes
142142
from mypy.server.deps import get_dependencies_of_target
143-
from mypy.server.target import module_prefix, split_target, trigger_to_target
143+
from mypy.server.target import trigger_to_target
144144
from mypy.server.trigger import make_trigger, WILDCARD_TAG
145+
from mypy.util import module_prefix, split_target
145146
from mypy.typestate import TypeState
146147

147148
MYPY = False

mypy/suggestions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
reverse_builtin_aliases,
3939
)
4040
from mypy.server.update import FineGrainedBuildManager
41-
from mypy.server.target import module_prefix, split_target
41+
from mypy.util import module_prefix, split_target
4242
from mypy.plugin import Plugin, FunctionContext, MethodContext
4343
from mypy.traverser import TraverserVisitor
4444
from mypy.checkexpr import has_any_type

mypy/util.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import re
55
import subprocess
66
import sys
7-
from typing import TypeVar, List, Tuple, Optional, Dict, Sequence
7+
from typing import TypeVar, List, Tuple, Optional, Dict, Sequence, Iterable
88

99
MYPY = False
1010
if MYPY:
@@ -33,6 +33,25 @@ def split_module_names(mod_name: str) -> List[str]:
3333
return out
3434

3535

36+
def module_prefix(modules: Iterable[str], target: str) -> Optional[str]:
37+
result = split_target(modules, target)
38+
if result is None:
39+
return None
40+
return result[0]
41+
42+
43+
def split_target(modules: Iterable[str], target: str) -> Optional[Tuple[str, str]]:
44+
remaining = [] # type: List[str]
45+
while True:
46+
if target in modules:
47+
return target, '.'.join(remaining)
48+
components = target.rsplit('.', 1)
49+
if len(components) == 1:
50+
return None
51+
target = components[0]
52+
remaining.insert(0, components[1])
53+
54+
3655
def short_type(obj: object) -> str:
3756
"""Return the last component of the type name of an object.
3857

test-data/unit/check-newsemanal.test

+39
Original file line numberDiff line numberDiff line change
@@ -2407,3 +2407,42 @@ class C:
24072407
def g(self) -> None: pass
24082408

24092409
reveal_type(C.X) # E: Revealed type is 'Any'
2410+
2411+
[case testNewAnalyzerImportedNameUsedInClassBody]
2412+
import m
2413+
2414+
[file m.py]
2415+
class C:
2416+
from mm import f
2417+
@dec(f)
2418+
def m(self): pass
2419+
2420+
def dec(f): pass
2421+
[file mm.py]
2422+
# 1 padding to increase line number of 'f'
2423+
# 2 padding
2424+
# 3 padding
2425+
# 4 padding
2426+
# 5 padding
2427+
# 6 padding
2428+
def f(): pass
2429+
2430+
2431+
[case testNewAnalyzerImportedNameUsedInClassBody2]
2432+
import m
2433+
2434+
[file m/__init__.py]
2435+
class C:
2436+
from m.m import f
2437+
@dec(f)
2438+
def m(self): pass
2439+
2440+
def dec(f): pass
2441+
[file m/m.py]
2442+
# 1 padding to increase line number of 'f'
2443+
# 2 padding
2444+
# 3 padding
2445+
# 4 padding
2446+
# 5 padding
2447+
# 6 padding
2448+
def f(): pass

0 commit comments

Comments
 (0)