Skip to content

Commit 0b7597c

Browse files
authored
Fix issues related to indirect imports in import cycles (#4695)
This adds supports for some cases of importing an imported name within an import cycle. Originally they could result in false positives or false negatives. The idea is to use a new node type ImportedName in semantic analysis pass 1 to represent an indirect reference to a name in another module. It will get resolved in semantic analysis pass 2. ImportedName is not yet used everywhere where it could make sense and this doesn't fix all related issues with import cycles. Also did a bit of refactoring of type semantic analysis to avoid passing multiple callback functions. Fixes #4049. Fixes #4429. Fixes #4682. Inspired by (and borrowed test cases from) #4495 by @carljm. Supersedes #4495
1 parent 5eb3c62 commit 0b7597c

File tree

11 files changed

+583
-81
lines changed

11 files changed

+583
-81
lines changed

mypy/nodes.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,38 @@ def accept(self, visitor: StatementVisitor[T]) -> T:
325325
return visitor.visit_import_all(self)
326326

327327

328+
class ImportedName(SymbolNode):
329+
"""Indirect reference to a fullname stored in symbol table.
330+
331+
This node is not present in the original program as such. This is
332+
just a temporary artifact in binding imported names. After semantic
333+
analysis pass 2, these references should be replaced with direct
334+
reference to a real AST node.
335+
336+
Note that this is neither a Statement nor an Expression so this
337+
can't be visited.
338+
"""
339+
340+
def __init__(self, target_fullname: str) -> None:
341+
self.target_fullname = target_fullname
342+
343+
def name(self) -> str:
344+
return self.target_fullname.split('.')[-1]
345+
346+
def fullname(self) -> str:
347+
return self.target_fullname
348+
349+
def serialize(self) -> JsonDict:
350+
assert False, "ImportedName leaked from semantic analysis"
351+
352+
@classmethod
353+
def deserialize(cls, data: JsonDict) -> 'ImportedName':
354+
assert False, "ImportedName should never be serialized"
355+
356+
def __str__(self) -> str:
357+
return 'ImportedName(%s)' % self.target_fullname
358+
359+
328360
class FuncBase(Node):
329361
"""Abstract base class for function-like nodes"""
330362

mypy/semanal.py

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
YieldFromExpr, NamedTupleExpr, TypedDictExpr, NonlocalDecl, SymbolNode,
5656
SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr,
5757
YieldExpr, ExecStmt, Argument, BackquoteExpr, ImportBase, AwaitExpr,
58-
IntExpr, FloatExpr, UnicodeExpr, EllipsisExpr, TempNode, EnumCallExpr,
58+
IntExpr, FloatExpr, UnicodeExpr, EllipsisExpr, TempNode, EnumCallExpr, ImportedName,
5959
COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES, ARG_OPT, nongen_builtins,
6060
collections_type_aliases, get_member_expr_fullname,
6161
)
@@ -84,7 +84,7 @@
8484
from mypy.plugin import Plugin, ClassDefContext, SemanticAnalyzerPluginInterface
8585
from mypy import join
8686
from mypy.util import get_prefix, correct_relative_import
87-
from mypy.semanal_shared import PRIORITY_FALLBACKS
87+
from mypy.semanal_shared import SemanticAnalyzerInterface, PRIORITY_FALLBACKS
8888
from mypy.scope import Scope
8989

9090

@@ -174,7 +174,9 @@
174174
}
175175

176176

177-
class SemanticAnalyzerPass2(NodeVisitor[None], SemanticAnalyzerPluginInterface):
177+
class SemanticAnalyzerPass2(NodeVisitor[None],
178+
SemanticAnalyzerInterface,
179+
SemanticAnalyzerPluginInterface):
178180
"""Semantically analyze parsed mypy files.
179181
180182
The analyzer binds names and does various consistency checks for a
@@ -1488,6 +1490,8 @@ def visit_import_from(self, imp: ImportFrom) -> None:
14881490
module = self.modules.get(import_id)
14891491
for id, as_id in imp.names:
14901492
node = module.names.get(id) if module else None
1493+
node = self.dereference_module_cross_ref(node)
1494+
14911495
missing = False
14921496
possible_module_id = import_id + '.' + id
14931497

@@ -1516,11 +1520,14 @@ def visit_import_from(self, imp: ImportFrom) -> None:
15161520
ast_node = Var(name, type=typ)
15171521
symbol = SymbolTableNode(GDEF, ast_node)
15181522
self.add_symbol(name, symbol, imp)
1519-
return
1523+
continue
15201524
if node and node.kind != UNBOUND_IMPORTED and not node.module_hidden:
15211525
node = self.normalize_type_alias(node, imp)
15221526
if not node:
1523-
return
1527+
# Normalization failed because target is not defined. Avoid duplicate
1528+
# error messages by marking the imported name as unknown.
1529+
self.add_unknown_symbol(as_id or id, imp, is_import=True)
1530+
continue
15241531
imported_id = as_id or id
15251532
existing_symbol = self.globals.get(imported_id)
15261533
if existing_symbol:
@@ -1550,6 +1557,29 @@ def visit_import_from(self, imp: ImportFrom) -> None:
15501557
# Missing module.
15511558
self.add_unknown_symbol(as_id or id, imp, is_import=True)
15521559

1560+
def dereference_module_cross_ref(
1561+
self, node: Optional[SymbolTableNode]) -> Optional[SymbolTableNode]:
1562+
"""Dereference cross references to other modules (if any).
1563+
1564+
If the node is not a cross reference, return it unmodified.
1565+
"""
1566+
seen = set() # type: Set[str]
1567+
# Continue until we reach a node that's nota cross reference (or until we find
1568+
# nothing).
1569+
while node and isinstance(node.node, ImportedName):
1570+
fullname = node.node.fullname()
1571+
if fullname in self.modules:
1572+
# This is a module reference.
1573+
return SymbolTableNode(MODULE_REF, self.modules[fullname])
1574+
if fullname in seen:
1575+
# Looks like a reference cycle. Just break it.
1576+
# TODO: Generate a more specific error message.
1577+
node = None
1578+
break
1579+
node = self.lookup_fully_qualified_or_none(fullname)
1580+
seen.add(fullname)
1581+
return node
1582+
15531583
def process_import_over_existing_name(self,
15541584
imported_id: str, existing_symbol: SymbolTableNode,
15551585
module_symbol: SymbolTableNode,
@@ -1573,6 +1603,14 @@ def process_import_over_existing_name(self,
15731603

15741604
def normalize_type_alias(self, node: SymbolTableNode,
15751605
ctx: Context) -> Optional[SymbolTableNode]:
1606+
"""If node refers to a built-in type alias, normalize it.
1607+
1608+
An example normalization is 'typing.List' -> '__builtins__.list'.
1609+
1610+
By default, if the node doesn't refer to a built-in type alias, return
1611+
the original node. If normalization fails because the target isn't
1612+
defined, return None.
1613+
"""
15761614
normalized = False
15771615
fullname = node.fullname
15781616
if fullname in type_aliases:
@@ -1612,7 +1650,10 @@ def visit_import_all(self, i: ImportAll) -> None:
16121650
if i_id in self.modules:
16131651
m = self.modules[i_id]
16141652
self.add_submodules_to_parent_modules(i_id, True)
1615-
for name, node in m.names.items():
1653+
for name, orig_node in m.names.items():
1654+
node = self.dereference_module_cross_ref(orig_node)
1655+
if node is None:
1656+
continue
16161657
new_node = self.normalize_type_alias(node, i)
16171658
# if '__all__' exists, all nodes not included have had module_public set to
16181659
# False, and we can skip checking '_' because it's been explicitly included.
@@ -1670,11 +1711,8 @@ def type_analyzer(self, *,
16701711
third_pass: bool = False) -> TypeAnalyser:
16711712
if tvar_scope is None:
16721713
tvar_scope = self.tvar_scope
1673-
tpan = TypeAnalyser(self.lookup_qualified,
1674-
self.lookup_fully_qualified,
1714+
tpan = TypeAnalyser(self,
16751715
tvar_scope,
1676-
self.fail,
1677-
self.note,
16781716
self.plugin,
16791717
self.options,
16801718
self.is_typeshed_stub_file,
@@ -1803,11 +1841,8 @@ def analyze_alias(self, rvalue: Expression,
18031841
dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic())
18041842
global_scope = not self.type and not self.function_stack
18051843
res = analyze_type_alias(rvalue,
1806-
self.lookup_qualified,
1807-
self.lookup_fully_qualified,
1844+
self,
18081845
self.tvar_scope,
1809-
self.fail,
1810-
self.note,
18111846
self.plugin,
18121847
self.options,
18131848
self.is_typeshed_stub_file,
@@ -3408,6 +3443,7 @@ def visit_member_expr(self, expr: MemberExpr) -> None:
34083443
# else:
34093444
# names = file.names
34103445
n = file.names.get(expr.name, None) if file is not None else None
3446+
n = self.dereference_module_cross_ref(n)
34113447
if n and not n.module_hidden:
34123448
n = self.normalize_type_alias(n, expr)
34133449
if not n:
@@ -3813,22 +3849,21 @@ def lookup_fully_qualified(self, name: str) -> SymbolTableNode:
38133849
n = next_sym.node
38143850
return n.names[parts[-1]]
38153851

3816-
def lookup_fully_qualified_or_none(self, name: str) -> Optional[SymbolTableNode]:
3817-
"""Lookup a fully qualified name.
3852+
def lookup_fully_qualified_or_none(self, fullname: str) -> Optional[SymbolTableNode]:
3853+
"""Lookup a fully qualified name that refers to a module-level definition.
38183854
38193855
Don't assume that the name is defined. This happens in the global namespace --
3820-
the local module namespace is ignored.
3856+
the local module namespace is ignored. This does not dereference indirect
3857+
refs.
3858+
3859+
Note that this can't be used for names nested in class namespaces.
38213860
"""
3822-
assert '.' in name
3823-
parts = name.split('.')
3824-
n = self.modules[parts[0]]
3825-
for i in range(1, len(parts) - 1):
3826-
next_sym = n.names.get(parts[i])
3827-
if not next_sym:
3828-
return None
3829-
assert isinstance(next_sym.node, MypyFile)
3830-
n = next_sym.node
3831-
return n.names.get(parts[-1])
3861+
assert '.' in fullname
3862+
module, name = fullname.rsplit('.', maxsplit=1)
3863+
if module not in self.modules:
3864+
return None
3865+
filenode = self.modules[module]
3866+
return filenode.names.get(name)
38323867

38333868
def qualified_name(self, n: str) -> str:
38343869
if self.type is not None:
@@ -3861,6 +3896,8 @@ def is_module_scope(self) -> bool:
38613896

38623897
def add_symbol(self, name: str, node: SymbolTableNode,
38633898
context: Context) -> None:
3899+
# NOTE: This logic mostly parallels SemanticAnalyzerPass1.add_symbol. If you change
3900+
# this, you may have to change the other method as well.
38643901
if self.is_func_scope():
38653902
assert self.locals[-1] is not None
38663903
if name in self.locals[-1]:
@@ -3873,8 +3910,10 @@ def add_symbol(self, name: str, node: SymbolTableNode,
38733910
self.type.names[name] = node
38743911
else:
38753912
existing = self.globals.get(name)
3876-
if existing and (not isinstance(node.node, MypyFile) or
3877-
existing.node != node.node) and existing.kind != UNBOUND_IMPORTED:
3913+
if (existing
3914+
and (not isinstance(node.node, MypyFile) or existing.node != node.node)
3915+
and existing.kind != UNBOUND_IMPORTED
3916+
and not isinstance(existing.node, ImportedName)):
38783917
# Modules can be imported multiple times to support import
38793918
# of multiple submodules of a package (e.g. a.x and a.y).
38803919
ok = False

mypy/semanal_pass1.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@
2323
from mypy.nodes import (
2424
MypyFile, SymbolTable, SymbolTableNode, Var, Block, AssignmentStmt, FuncDef, Decorator,
2525
ClassDef, TypeInfo, ImportFrom, Import, ImportAll, IfStmt, WhileStmt, ForStmt, WithStmt,
26-
TryStmt, OverloadedFuncDef, Lvalue, Context, LDEF, GDEF, MDEF, UNBOUND_IMPORTED, MODULE_REF,
27-
implicit_module_attrs
26+
TryStmt, OverloadedFuncDef, Lvalue, Context, ImportedName, LDEF, GDEF, MDEF, UNBOUND_IMPORTED,
27+
MODULE_REF, implicit_module_attrs
2828
)
2929
from mypy.types import Type, UnboundType, UnionType, AnyType, TypeOfAny, NoneTyp
3030
from mypy.semanal import SemanticAnalyzerPass2, infer_reachability_of_if_statement
31+
from mypy.semanal_shared import create_indirect_imported_name
3132
from mypy.options import Options
3233
from mypy.sametypes import is_same_type
3334
from mypy.visitor import NodeVisitor
35+
from mypy.util import correct_relative_import
3436

3537

3638
class SemanticAnalyzerPass1(NodeVisitor[None]):
@@ -62,6 +64,7 @@ def visit_file(self, file: MypyFile, fnam: str, mod_id: str, options: Options) -
6264
self.pyversion = options.python_version
6365
self.platform = options.platform
6466
sem.cur_mod_id = mod_id
67+
sem.cur_mod_node = file
6568
sem.errors.set_file(fnam, mod_id)
6669
sem.globals = SymbolTable()
6770
sem.global_decls = [set()]
@@ -148,7 +151,8 @@ def visit_func_def(self, func: FuncDef) -> None:
148151
if at_module and func.name() in sem.globals:
149152
# Already defined in this module.
150153
original_sym = sem.globals[func.name()]
151-
if original_sym.kind == UNBOUND_IMPORTED:
154+
if (original_sym.kind == UNBOUND_IMPORTED or
155+
isinstance(original_sym.node, ImportedName)):
152156
# Ah this is an imported name. We can't resolve them now, so we'll postpone
153157
# this until the main phase of semantic analysis.
154158
return
@@ -241,7 +245,12 @@ def visit_import_from(self, node: ImportFrom) -> None:
241245
for name, as_name in node.names:
242246
imported_name = as_name or name
243247
if imported_name not in self.sem.globals:
244-
self.add_symbol(imported_name, SymbolTableNode(UNBOUND_IMPORTED, None), node)
248+
sym = create_indirect_imported_name(self.sem.cur_mod_node,
249+
node.id,
250+
node.relative,
251+
name)
252+
if sym:
253+
self.add_symbol(imported_name, sym, context=node)
245254

246255
def visit_import(self, node: Import) -> None:
247256
node.is_top_level = self.sem.is_module_scope()
@@ -312,8 +321,9 @@ def kind_by_scope(self) -> int:
312321

313322
def add_symbol(self, name: str, node: SymbolTableNode,
314323
context: Context) -> None:
315-
# This is related to SemanticAnalyzerPass2.add_symbol. Since both methods will
316-
# be called on top-level definitions, they need to co-operate.
324+
# NOTE: This is closely related to SemanticAnalyzerPass2.add_symbol. Since both methods
325+
# will be called on top-level definitions, they need to co-operate. If you change
326+
# this, you may have to change the other method as well.
317327
if self.sem.is_func_scope():
318328
assert self.sem.locals[-1] is not None
319329
if name in self.sem.locals[-1]:
@@ -325,8 +335,10 @@ def add_symbol(self, name: str, node: SymbolTableNode,
325335
else:
326336
assert self.sem.type is None # Pass 1 doesn't look inside classes
327337
existing = self.sem.globals.get(name)
328-
if existing and (not isinstance(node.node, MypyFile) or
329-
existing.node != node.node) and existing.kind != UNBOUND_IMPORTED:
338+
if (existing
339+
and (not isinstance(node.node, MypyFile) or existing.node != node.node)
340+
and existing.kind != UNBOUND_IMPORTED
341+
and not isinstance(existing.node, ImportedName)):
330342
# Modules can be imported multiple times to support import
331343
# of multiple submodules of a package (e.g. a.x and a.y).
332344
ok = False

0 commit comments

Comments
 (0)