Skip to content

Commit 30c46ab

Browse files
authored
Properly track module_hidden and module_public for incomplete symbols (#8450)
This fixes some crash bugs involving import * from an import cycle.
1 parent ef0b0df commit 30c46ab

File tree

5 files changed

+71
-15
lines changed

5 files changed

+71
-15
lines changed

mypy/fixup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
def fixup_module(tree: MypyFile, modules: Dict[str, MypyFile],
2323
allow_missing: bool) -> None:
2424
node_fixer = NodeFixer(modules, allow_missing)
25-
node_fixer.visit_symbol_table(tree.names)
25+
node_fixer.visit_symbol_table(tree.names, tree.fullname)
2626

2727

2828
# TODO: Fix up .info when deserializing, i.e. much earlier.
@@ -42,7 +42,7 @@ def visit_type_info(self, info: TypeInfo) -> None:
4242
if info.defn:
4343
info.defn.accept(self)
4444
if info.names:
45-
self.visit_symbol_table(info.names)
45+
self.visit_symbol_table(info.names, info.fullname)
4646
if info.bases:
4747
for base in info.bases:
4848
base.accept(self.type_fixer)
@@ -64,7 +64,7 @@ def visit_type_info(self, info: TypeInfo) -> None:
6464
self.current_info = save_info
6565

6666
# NOTE: This method *definitely* isn't part of the NodeVisitor API.
67-
def visit_symbol_table(self, symtab: SymbolTable) -> None:
67+
def visit_symbol_table(self, symtab: SymbolTable, table_fullname: str) -> None:
6868
# Copy the items because we may mutate symtab.
6969
for key, value in list(symtab.items()):
7070
cross_ref = value.cross_ref
@@ -76,7 +76,7 @@ def visit_symbol_table(self, symtab: SymbolTable) -> None:
7676
stnode = lookup_qualified_stnode(self.modules, cross_ref,
7777
self.allow_missing)
7878
if stnode is not None:
79-
assert stnode.node is not None
79+
assert stnode.node is not None, (table_fullname + "." + key, cross_ref)
8080
value.node = stnode.node
8181
elif not self.allow_missing:
8282
assert False, "Could not find cross-ref %s" % (cross_ref,)

mypy/nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3030,6 +3030,7 @@ def serialize(self, prefix: str, name: str) -> JsonDict:
30303030
and fullname != prefix + '.' + name
30313031
and not (isinstance(self.node, Var)
30323032
and self.node.from_module_getattr)):
3033+
assert not isinstance(self.node, PlaceholderNode)
30333034
data['cross_ref'] = fullname
30343035
return data
30353036
data['node'] = self.node.serialize()

mypy/semanal.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,13 +1742,25 @@ def process_imported_symbol(self,
17421742
fullname: str,
17431743
context: ImportBase) -> None:
17441744
imported_id = as_id or id
1745+
# 'from m import x as x' exports x in a stub file or when implicit
1746+
# re-exports are disabled.
1747+
module_public = (
1748+
not self.is_stub_file
1749+
and self.options.implicit_reexport
1750+
or as_id is not None
1751+
)
1752+
module_hidden = not module_public and fullname not in self.modules
1753+
17451754
if isinstance(node.node, PlaceholderNode):
17461755
if self.final_iteration:
17471756
self.report_missing_module_attribute(module_id, id, imported_id, context)
17481757
return
17491758
else:
17501759
# This might become a type.
1751-
self.mark_incomplete(imported_id, node.node, becomes_typeinfo=True)
1760+
self.mark_incomplete(imported_id, node.node,
1761+
module_public=module_public,
1762+
module_hidden=module_hidden,
1763+
becomes_typeinfo=True)
17521764
existing_symbol = self.globals.get(imported_id)
17531765
if (existing_symbol and not isinstance(existing_symbol.node, PlaceholderNode) and
17541766
not isinstance(node.node, PlaceholderNode)):
@@ -1760,14 +1772,6 @@ def process_imported_symbol(self,
17601772
# Imports are special, some redefinitions are allowed, so wait until
17611773
# we know what is the new symbol node.
17621774
return
1763-
# 'from m import x as x' exports x in a stub file or when implicit
1764-
# re-exports are disabled.
1765-
module_public = (
1766-
not self.is_stub_file
1767-
and self.options.implicit_reexport
1768-
or as_id is not None
1769-
)
1770-
module_hidden = not module_public and fullname not in self.modules
17711775
# NOTE: we take the original node even for final `Var`s. This is to support
17721776
# a common pattern when constants are re-exported (same applies to import *).
17731777
self.add_imported_symbol(imported_id, node, context,
@@ -1866,6 +1870,7 @@ def visit_import_all(self, i: ImportAll) -> None:
18661870
self.add_imported_symbol(name, node, i,
18671871
module_public=module_public,
18681872
module_hidden=not module_public)
1873+
18691874
else:
18701875
# Don't add any dummy symbols for 'from x import *' if 'x' is unknown.
18711876
pass
@@ -4338,6 +4343,7 @@ def add_imported_symbol(self,
43384343
module_public: bool = True,
43394344
module_hidden: bool = False) -> None:
43404345
"""Add an alias to an existing symbol through import."""
4346+
assert not module_hidden or not module_public
43414347
symbol = SymbolTableNode(node.kind, node.node,
43424348
module_public=module_public,
43434349
module_hidden=module_hidden)
@@ -4421,7 +4427,9 @@ def record_incomplete_ref(self) -> None:
44214427
self.num_incomplete_refs += 1
44224428

44234429
def mark_incomplete(self, name: str, node: Node,
4424-
becomes_typeinfo: bool = False) -> None:
4430+
becomes_typeinfo: bool = False,
4431+
module_public: bool = True,
4432+
module_hidden: bool = False) -> None:
44254433
"""Mark a definition as incomplete (and defer current analysis target).
44264434
44274435
Also potentially mark the current namespace as incomplete.
@@ -4440,7 +4448,9 @@ def mark_incomplete(self, name: str, node: Node,
44404448
assert self.statement
44414449
placeholder = PlaceholderNode(fullname, node, self.statement.line,
44424450
becomes_typeinfo=becomes_typeinfo)
4443-
self.add_symbol(name, placeholder, context=dummy_context())
4451+
self.add_symbol(name, placeholder,
4452+
module_public=module_public, module_hidden=module_hidden,
4453+
context=dummy_context())
44444454
self.missing_names.add(name)
44454455

44464456
def is_incomplete_namespace(self, fullname: str) -> bool:

test-data/unit/check-incremental.test

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5361,3 +5361,26 @@ reveal_type(z)
53615361
tmp/c.py:2: note: Revealed type is 'a.A'
53625362
[out2]
53635363
tmp/c.py:2: note: Revealed type is 'a.<subclass of "A" and "B">'
5364+
5365+
[case testStubFixupIssues]
5366+
import a
5367+
[file a.py]
5368+
import p
5369+
[file a.py.2]
5370+
import p
5371+
p.N
5372+
5373+
[file p/__init__.pyi]
5374+
from p.util import *
5375+
5376+
[file p/util.pyi]
5377+
from p.params import N
5378+
class Test: ...
5379+
x: N
5380+
5381+
[file p/params.pyi]
5382+
import p.util
5383+
class N(p.util.Test):
5384+
...
5385+
[out2]
5386+
tmp/a.py:2: error: "object" has no attribute "N"

test-data/unit/fine-grained.test

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9572,3 +9572,25 @@ c.py:2: note: Revealed type is 'a.A'
95729572
==
95739573
c.py:2: note: Revealed type is 'a.<subclass of "A" and "B">'
95749574

9575+
[case testStubFixupIssues]
9576+
[file a.py]
9577+
import p
9578+
[file a.py.2]
9579+
import p
9580+
# a change
9581+
9582+
[file p/__init__.pyi]
9583+
from p.util import *
9584+
9585+
[file p/util.pyi]
9586+
from p.params import N
9587+
class Test: ...
9588+
9589+
[file p/params.pyi]
9590+
import p.util
9591+
class N(p.util.Test):
9592+
...
9593+
9594+
[builtins fixtures/list.pyi]
9595+
[out]
9596+
==

0 commit comments

Comments
 (0)