Skip to content

Commit 9eb2468

Browse files
authored
New semantic analyzer: don't add submodules to symbol tables (#7005)
Previously we added each submodule implicitly to the the symbol table of the parent package. This PR removes this and instead we look up names from the modules dictionary if they aren't found in the symbol table. The change only affects the new semantic analyzer. This provides a foundation that should make #6828 much easier to address. It also arguably cleans up the code. Also refactor some related code to avoid duplication. This isn't a pure refactor since some error messages are slightly different.
1 parent 6866191 commit 9eb2468

File tree

6 files changed

+82
-150
lines changed

6 files changed

+82
-150
lines changed

mypy/build.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,20 +1876,17 @@ def patch_dependency_parents(self) -> None:
18761876
details.
18771877
18781878
However, this patching process can occur after `a` has been parsed and
1879-
serialized during increment mode. Consequently, we need to repeat this
1879+
serialized during incremental mode. Consequently, we need to repeat this
18801880
patch when deserializing a cached file.
18811881
18821882
This function should be called only when processing fresh SCCs -- the
18831883
semantic analyzer will perform this patch for us when processing stale
18841884
SCCs.
18851885
"""
1886-
Analyzer = Union[SemanticAnalyzerPass2, NewSemanticAnalyzer] # noqa
1887-
if self.manager.options.new_semantic_analyzer:
1888-
analyzer = self.manager.new_semantic_analyzer # type: Analyzer
1889-
else:
1886+
if not self.manager.options.new_semantic_analyzer:
18901887
analyzer = self.manager.semantic_analyzer
1891-
for dep in self.dependencies:
1892-
analyzer.add_submodules_to_parent_modules(dep, True)
1888+
for dep in self.dependencies:
1889+
analyzer.add_submodules_to_parent_modules(dep, True)
18931890

18941891
def fix_suppressed_dependencies(self, graph: Graph) -> None:
18951892
"""Corrects whether dependencies are considered stale in silent mode.

mypy/newsemanal/semanal.py

Lines changed: 43 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@
9999
Plugin, ClassDefContext, SemanticAnalyzerPluginInterface,
100100
DynamicClassDefContext
101101
)
102-
from mypy.util import (
103-
get_prefix, correct_relative_import, unmangle, module_prefix
104-
)
102+
from mypy.util import correct_relative_import, unmangle, module_prefix
105103
from mypy.scope import Scope
106104
from mypy.newsemanal.semanal_shared import (
107105
SemanticAnalyzerInterface, set_callable_name, calculate_tuple_fallback, PRIORITY_FALLBACKS
@@ -1621,44 +1619,6 @@ def visit_import(self, i: Import) -> None:
16211619
base = id.split('.')[0]
16221620
self.add_module_symbol(base, base, module_public=module_public,
16231621
context=i, module_hidden=not module_public)
1624-
self.add_submodules_to_parent_modules(id, module_public)
1625-
1626-
def add_submodules_to_parent_modules(self, id: str, module_public: bool) -> None:
1627-
"""Recursively adds a reference to a newly loaded submodule to its parent.
1628-
1629-
When you import a submodule in any way, Python will add a reference to that
1630-
submodule to its parent. So, if you do something like `import A.B` or
1631-
`from A import B` or `from A.B import Foo`, Python will add a reference to
1632-
module A.B to A's namespace.
1633-
1634-
Note that this "parent patching" process is completely independent from any
1635-
changes made to the *importer's* namespace. For example, if you have a file
1636-
named `foo.py` where you do `from A.B import Bar`, then foo's namespace will
1637-
be modified to contain a reference to only Bar. Independently, A's namespace
1638-
will be modified to contain a reference to `A.B`.
1639-
"""
1640-
while '.' in id:
1641-
parent, child = id.rsplit('.', 1)
1642-
parent_mod = self.modules.get(parent)
1643-
if parent_mod and self.allow_patching(parent_mod, child):
1644-
child_mod = self.modules.get(id)
1645-
if child_mod:
1646-
sym = SymbolTableNode(GDEF, child_mod,
1647-
module_public=module_public,
1648-
no_serialize=True)
1649-
else:
1650-
# Construct a dummy Var with Any type.
1651-
any_type = AnyType(TypeOfAny.from_unimported_type,
1652-
missing_import_name=id)
1653-
var = Var(child, any_type)
1654-
var._fullname = child
1655-
var.is_ready = True
1656-
var.is_suppressed_import = True
1657-
sym = SymbolTableNode(GDEF, var,
1658-
module_public=module_public,
1659-
no_serialize=True)
1660-
parent_mod.names[child] = sym
1661-
id = parent
16621622

16631623
def allow_patching(self, parent_mod: MypyFile, child: str) -> bool:
16641624
if child not in parent_mod.names:
@@ -1671,7 +1631,6 @@ def allow_patching(self, parent_mod: MypyFile, child: str) -> bool:
16711631
def visit_import_from(self, imp: ImportFrom) -> None:
16721632
self.statement = imp
16731633
import_id = self.correct_relative_import(imp)
1674-
self.add_submodules_to_parent_modules(import_id, True)
16751634
module = self.modules.get(import_id)
16761635
for id, as_id in imp.names:
16771636
node = module.names.get(id) if module else None
@@ -1687,13 +1646,12 @@ def visit_import_from(self, imp: ImportFrom) -> None:
16871646
if mod is not None:
16881647
kind = self.current_symbol_kind()
16891648
node = SymbolTableNode(kind, mod)
1690-
self.add_submodules_to_parent_modules(possible_module_id, True)
16911649
elif possible_module_id in self.missing_modules:
16921650
missing = True
16931651
# If it is still not resolved, check for a module level __getattr__
16941652
if (module and not node and (module.is_stub or self.options.python_version >= (3, 7))
16951653
and '__getattr__' in module.names):
1696-
# We use the fullname of the orignal definition so that we can
1654+
# We use the fullname of the original definition so that we can
16971655
# detect whether two imported names refer to the same thing.
16981656
fullname = import_id + '.' + id
16991657
gvar = self.create_getattr_var(module.names['__getattr__'], imported_id, fullname)
@@ -1823,7 +1781,6 @@ def visit_import_all(self, i: ImportAll) -> None:
18231781
# Any names could be missing from the current namespace if the target module
18241782
# namespace is incomplete.
18251783
self.mark_incomplete('*', i)
1826-
self.add_submodules_to_parent_modules(i_id, True)
18271784
for name, node in m.names.items():
18281785
if node is None:
18291786
continue
@@ -3457,61 +3414,16 @@ def check_fixed_args(self, expr: CallExpr, numargs: int,
34573414
def visit_member_expr(self, expr: MemberExpr) -> None:
34583415
base = expr.expr
34593416
base.accept(self)
3460-
# Bind references to module attributes.
34613417
if isinstance(base, RefExpr) and isinstance(base.node, MypyFile):
3462-
# This branch handles the case foo.bar where foo is a module.
3463-
# In this case base.node is the module's MypyFile and we look up
3464-
# bar in its namespace. This must be done for all types of bar.
3465-
file = base.node
3466-
# TODO: Should we actually use this? Not sure if this makes a difference.
3467-
# if file.fullname() == self.cur_mod_id:
3468-
# names = self.globals
3469-
# else:
3470-
# names = file.names
3471-
n = file.names.get(expr.name, None)
3472-
if n and not n.module_hidden:
3473-
n = self.rebind_symbol_table_node(n)
3474-
if n:
3475-
if isinstance(n.node, PlaceholderNode):
3476-
self.process_placeholder(expr.name, 'attribute', expr)
3477-
return
3478-
# TODO: What if None?
3479-
expr.kind = n.kind
3480-
expr.fullname = n.fullname
3481-
expr.node = n.node
3482-
elif (file is not None and (file.is_stub or self.options.python_version >= (3, 7))
3483-
and '__getattr__' in file.names):
3484-
# If there is a module-level __getattr__, then any attribute on the module is valid
3485-
# per PEP 484.
3486-
getattr_defn = file.names['__getattr__']
3487-
if not getattr_defn:
3488-
typ = AnyType(TypeOfAny.from_error) # type: Type
3489-
elif isinstance(getattr_defn.node, (FuncDef, Var)):
3490-
if isinstance(getattr_defn.node.type, CallableType):
3491-
typ = getattr_defn.node.type.ret_type
3492-
else:
3493-
typ = AnyType(TypeOfAny.from_error)
3494-
else:
3495-
typ = AnyType(TypeOfAny.from_error)
3496-
expr.kind = GDEF
3497-
expr.fullname = '{}.{}'.format(file.fullname(), expr.name)
3498-
expr.node = Var(expr.name, type=typ)
3499-
else:
3500-
if self.is_incomplete_namespace(file.fullname()):
3501-
self.record_incomplete_ref()
3418+
# Handle module attribute.
3419+
sym = self.get_module_symbol(base.node, expr.name)
3420+
if sym:
3421+
if isinstance(sym.node, PlaceholderNode):
3422+
self.process_placeholder(expr.name, 'attribute', expr)
35023423
return
3503-
# We only catch some errors here; the rest will be
3504-
# caught during type checking.
3505-
#
3506-
# This way we can report a larger number of errors in
3507-
# one type checker run. If we reported errors here,
3508-
# the build would terminate after semantic analysis
3509-
# and we wouldn't be able to report any type errors.
3510-
full_name = '%s.%s' % (file.fullname() if file is not None else None, expr.name)
3511-
mod_name = " '%s'" % file.fullname() if file is not None else ''
3512-
if full_name in obsolete_name_mapping:
3513-
self.fail("Module%s has no attribute %r (it's now called %r)" % (
3514-
mod_name, expr.name, obsolete_name_mapping[full_name]), expr)
3424+
expr.kind = sym.kind
3425+
expr.fullname = sym.fullname
3426+
expr.node = sym.node
35153427
elif isinstance(base, RefExpr):
35163428
# This branch handles the case C.bar (or cls.bar or self.bar inside
35173429
# a classmethod/method), where C is a class and bar is a type
@@ -3851,10 +3763,11 @@ def lookup_qualified(self, name: str, ctx: Context,
38513763
if sym:
38523764
for i in range(1, len(parts)):
38533765
node = sym.node
3766+
part = parts[i]
38543767
if isinstance(node, TypeInfo):
3855-
nextsym = node.get(parts[i])
3768+
nextsym = node.get(part)
38563769
elif isinstance(node, MypyFile):
3857-
nextsym = self.get_module_symbol(node, parts[i:])
3770+
nextsym = self.get_module_symbol(node, part)
38583771
namespace = node.fullname()
38593772
elif isinstance(node, PlaceholderNode):
38603773
return sym
@@ -3881,27 +3794,40 @@ def lookup_type_node(self, expr: Expression) -> Optional[SymbolTableNode]:
38813794
return n
38823795
return None
38833796

3884-
def get_module_symbol(self, node: MypyFile, parts: List[str]) -> Optional[SymbolTableNode]:
3885-
"""Look up a symbol from the module symbol table."""
3886-
# TODO: Use this logic in more places?
3797+
def get_module_symbol(self, node: MypyFile, name: str) -> Optional[SymbolTableNode]:
3798+
"""Look up a symbol from a module.
3799+
3800+
Return None if no matching symbol could be bound.
3801+
"""
38873802
module = node.fullname()
38883803
names = node.names
3889-
# Rebind potential references to old version of current module in
3890-
# fine-grained incremental mode.
3891-
if module == self.cur_mod_id:
3892-
names = self.globals
3893-
sym = names.get(parts[0], None)
3894-
if (not sym
3895-
and '__getattr__' in names
3896-
and not self.is_incomplete_namespace(module)
3897-
and (node.is_stub or self.options.python_version >= (3, 7))):
3898-
fullname = module + '.' + '.'.join(parts)
3899-
gvar = self.create_getattr_var(names['__getattr__'],
3900-
parts[0], fullname)
3901-
if gvar:
3902-
sym = SymbolTableNode(GDEF, gvar)
3804+
sym = names.get(name)
3805+
if not sym:
3806+
fullname = module + '.' + name
3807+
if fullname in self.modules:
3808+
sym = SymbolTableNode(GDEF, self.modules[fullname])
3809+
elif self.is_incomplete_namespace(module):
3810+
self.record_incomplete_ref()
3811+
elif ('__getattr__' in names
3812+
and (node.is_stub
3813+
or self.options.python_version >= (3, 7))):
3814+
gvar = self.create_getattr_var(names['__getattr__'], name, fullname)
3815+
if gvar:
3816+
sym = SymbolTableNode(GDEF, gvar)
3817+
elif self.is_missing_module(fullname):
3818+
# We use the fullname of the original definition so that we can
3819+
# detect whether two names refer to the same thing.
3820+
var_type = AnyType(TypeOfAny.from_unimported_type)
3821+
v = Var(name, type=var_type)
3822+
v._fullname = fullname
3823+
sym = SymbolTableNode(GDEF, v)
3824+
elif sym.module_hidden:
3825+
sym = None
39033826
return sym
39043827

3828+
def is_missing_module(self, module: str) -> bool:
3829+
return self.options.ignore_missing_imports or module in self.missing_modules
3830+
39053831
def implicit_symbol(self, sym: SymbolTableNode, name: str, parts: List[str],
39063832
source_type: AnyType) -> SymbolTableNode:
39073833
"""Create symbol for a qualified name reference through Any type."""
@@ -4308,24 +4234,6 @@ def process_placeholder(self, name: str, kind: str, ctx: Context) -> None:
43084234
def cannot_resolve_name(self, name: str, kind: str, ctx: Context) -> None:
43094235
self.fail('Cannot resolve {} "{}" (possible cyclic definition)'.format(kind, name), ctx)
43104236

4311-
def rebind_symbol_table_node(self, n: SymbolTableNode) -> Optional[SymbolTableNode]:
4312-
"""If node refers to old version of module, return reference to new version.
4313-
4314-
If the reference is removed in the new version, return None.
4315-
"""
4316-
# TODO: Handle type variables and other sorts of references
4317-
if isinstance(n.node, (FuncDef, OverloadedFuncDef, TypeInfo, Var, TypeAlias)):
4318-
# TODO: Why is it possible for fullname() to be None, even though it's not
4319-
# annotated as Optional[str]?
4320-
# TODO: Do this for all modules in the set of modified files
4321-
# TODO: This doesn't work for things nested within classes
4322-
if n.node.fullname() and get_prefix(n.node.fullname()) == self.cur_mod_id:
4323-
# This is an indirect reference to a name defined in the current module.
4324-
# Rebind it.
4325-
return self.globals.get(n.node.name())
4326-
# No need to rebind.
4327-
return n
4328-
43294237
def qualified_name(self, name: str) -> str:
43304238
if self.type is not None:
43314239
return self.type._fullname + '.' + name

test-data/unit/check-modules.test

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2557,6 +2557,10 @@ import whatever.works
25572557
import a.b
25582558
x = whatever.works.f()
25592559
y = a.b.f()
2560+
xx: whatever.works.C
2561+
yy: a.b.C
2562+
xx2: whatever.works.C.D
2563+
yy2: a.b.C.D
25602564
[file a/__init__.pyi]
25612565
# empty
25622566
[out]

test-data/unit/check-newsemanal.test

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2497,3 +2497,22 @@ def force(x: Literal[42]) -> None: pass
24972497
force(reveal_type(var)) # E: Revealed type is 'Literal[42]'
24982498

24992499
class Yes: ...
2500+
2501+
[case testNewAnalyzerImportCycleWithIgnoreMissingImports]
2502+
# flags: --ignore-missing-imports
2503+
import p
2504+
reveal_type(p.get) # E: Revealed type is 'def () -> builtins.int'
2505+
2506+
[file p/__init__.pyi]
2507+
from . import api
2508+
get = api.get
2509+
2510+
[file p/api.pyi]
2511+
import p
2512+
2513+
def get() -> int: ...
2514+
2515+
[case testUseObsoleteNameForTypeVar3]
2516+
import typing
2517+
t = typing.typevar('t') # E: Module has no attribute "typevar"
2518+
[builtins fixtures/module.pyi]

test-data/unit/fine-grained-modules.test

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,7 @@ class Baz:
812812
==
813813

814814
[case testDeleteFileWithinPackage]
815+
# flags: --new-semantic-analyzer
815816
import a
816817
[file a.py]
817818
import m.x
@@ -826,6 +827,7 @@ a.py:2: error: Too many arguments for "g"
826827
==
827828
a.py:1: error: Cannot find module named 'm.x'
828829
a.py:1: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
830+
a.py:2: error: Module has no attribute "x"
829831

830832
[case testDeletePackage1]
831833
import p.a
@@ -859,6 +861,7 @@ main:1: error: Cannot find module named 'p'
859861
main:1: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
860862

861863
[case testDeletePackage3]
864+
# flags: --new-semantic-analyzer
862865
import p.a
863866
p.a.f(1)
864867
[file p/__init__.py]
@@ -868,14 +871,15 @@ def f(x: str) -> None: pass
868871
[delete p/__init__.py.3]
869872
[builtins fixtures/module.pyi]
870873
[out]
871-
main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str"
874+
main:3: error: Argument 1 to "f" has incompatible type "int"; expected "str"
872875
==
873-
main:1: error: Cannot find module named 'p.a'
874-
main:1: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
876+
main:2: error: Cannot find module named 'p.a'
877+
main:2: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
878+
main:3: error: Module has no attribute "a"
875879
==
876-
main:1: error: Cannot find module named 'p.a'
877-
main:1: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
878-
main:1: error: Cannot find module named 'p'
880+
main:2: error: Cannot find module named 'p.a'
881+
main:2: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
882+
main:2: error: Cannot find module named 'p'
879883

880884
[case testDeletePackage4]
881885
import p.a

test-data/unit/semanal-errors.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,11 +1273,11 @@ main:1: error: Name 'typevar' is not defined
12731273
main:1: note: Did you forget to import it from "typing"? (Suggestion: "from typing import TypeVar")
12741274

12751275
[case testUseObsoleteNameForTypeVar3]
1276+
# flags: --no-new-semantic-analyzer
12761277
import typing
12771278
t = typing.typevar('t')
12781279
[out]
1279-
main:2: error: Module 'typing' has no attribute 'typevar' (it's now called 'typing.TypeVar')
1280-
--' (work around syntax highlighting :-/)
1280+
main:3: error: Module 'typing' has no attribute 'typevar' (it's now called 'typing.TypeVar')
12811281

12821282
[case testInvalidTypeAnnotation]
12831283
import typing

0 commit comments

Comments
 (0)