Skip to content

Commit 169b224

Browse files
authored
Allow importing a submodule ahead of its parent module (#2237)
Fixes #2133. The solution is not to bail out early when the parent module is unavailable -- if all the imports are submodules of a package that's acceptable (the bug was that in incremental mode it's possible that the parent is fresh and has not been entered into the modules dict yet, but the bail-out caused the import to be marked as missing).
1 parent 9c80d8c commit 169b224

File tree

2 files changed

+67
-47
lines changed

2 files changed

+67
-47
lines changed

mypy/semanal.py

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -913,11 +913,13 @@ def add_submodules_to_parent_modules(self, id: str, module_public: bool) -> None
913913
"""
914914
while '.' in id:
915915
parent, child = id.rsplit('.', 1)
916-
modules_loaded = parent in self.modules and id in self.modules
917-
if modules_loaded and child not in self.modules[parent].names:
918-
sym = SymbolTableNode(MODULE_REF, self.modules[id], parent,
919-
module_public=module_public)
920-
self.modules[parent].names[child] = sym
916+
parent_mod = self.modules.get(parent)
917+
if parent_mod and child not in parent_mod.names:
918+
child_mod = self.modules.get(id)
919+
if child_mod:
920+
sym = SymbolTableNode(MODULE_REF, child_mod, parent,
921+
module_public=module_public)
922+
parent_mod.names[child] = sym
921923
id = parent
922924

923925
def add_module_symbol(self, id: str, as_id: str, module_public: bool,
@@ -931,48 +933,47 @@ def add_module_symbol(self, id: str, as_id: str, module_public: bool,
931933

932934
def visit_import_from(self, imp: ImportFrom) -> None:
933935
import_id = self.correct_relative_import(imp)
934-
if import_id in self.modules:
935-
module = self.modules[import_id]
936-
self.add_submodules_to_parent_modules(import_id, True)
937-
for id, as_id in imp.names:
938-
node = module.names.get(id)
939-
940-
# If the module does not contain a symbol with the name 'id',
941-
# try checking if it's a module instead.
942-
if id not in module.names or node.kind == UNBOUND_IMPORTED:
943-
possible_module_id = import_id + '.' + id
944-
mod = self.modules.get(possible_module_id)
945-
if mod is not None:
946-
node = SymbolTableNode(MODULE_REF, mod, import_id)
947-
self.add_submodules_to_parent_modules(possible_module_id, True)
948-
949-
if node and node.kind != UNBOUND_IMPORTED:
950-
node = self.normalize_type_alias(node, imp)
951-
if not node:
952-
return
953-
imported_id = as_id or id
954-
existing_symbol = self.globals.get(imported_id)
955-
if existing_symbol:
956-
# Import can redefine a variable. They get special treatment.
957-
if self.process_import_over_existing_name(
958-
imported_id, existing_symbol, node, imp):
959-
continue
960-
# 'from m import x as x' exports x in a stub file.
961-
module_public = not self.is_stub_file or as_id is not None
962-
symbol = SymbolTableNode(node.kind, node.node,
963-
self.cur_mod_id,
964-
node.type_override,
965-
module_public=module_public)
966-
self.add_symbol(imported_id, symbol, imp)
967-
else:
968-
message = "Module '{}' has no attribute '{}'".format(import_id, id)
969-
extra = self.undefined_name_extra_info('{}.{}'.format(import_id, id))
970-
if extra:
971-
message += " {}".format(extra)
972-
self.fail(message, imp)
973-
else:
974-
# Missing module.
975-
for id, as_id in imp.names:
936+
self.add_submodules_to_parent_modules(import_id, True)
937+
module = self.modules.get(import_id)
938+
for id, as_id in imp.names:
939+
node = module.names.get(id) if module else None
940+
941+
# If the module does not contain a symbol with the name 'id',
942+
# try checking if it's a module instead.
943+
if not node or node.kind == UNBOUND_IMPORTED:
944+
possible_module_id = import_id + '.' + id
945+
mod = self.modules.get(possible_module_id)
946+
if mod is not None:
947+
node = SymbolTableNode(MODULE_REF, mod, import_id)
948+
self.add_submodules_to_parent_modules(possible_module_id, True)
949+
950+
if node and node.kind != UNBOUND_IMPORTED:
951+
node = self.normalize_type_alias(node, imp)
952+
if not node:
953+
return
954+
imported_id = as_id or id
955+
existing_symbol = self.globals.get(imported_id)
956+
if existing_symbol:
957+
# Import can redefine a variable. They get special treatment.
958+
if self.process_import_over_existing_name(
959+
imported_id, existing_symbol, node, imp):
960+
continue
961+
# 'from m import x as x' exports x in a stub file.
962+
module_public = not self.is_stub_file or as_id is not None
963+
symbol = SymbolTableNode(node.kind, node.node,
964+
self.cur_mod_id,
965+
node.type_override,
966+
module_public=module_public)
967+
self.add_symbol(imported_id, symbol, imp)
968+
elif module:
969+
# Missing attribute.
970+
message = "Module '{}' has no attribute '{}'".format(import_id, id)
971+
extra = self.undefined_name_extra_info('{}.{}'.format(import_id, id))
972+
if extra:
973+
message += " {}".format(extra)
974+
self.fail(message, imp)
975+
else:
976+
# Missing module.
976977
self.add_unknown_symbol(as_id or id, imp, is_import=True)
977978

978979
def process_import_over_existing_name(self,

test-data/unit/check-incremental.test

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1615,3 +1615,22 @@ from funcs import callee
16151615
from classes import Outer
16161616
def caller(a: Outer.Inner) -> int:
16171617
callee(a)
1618+
1619+
[case testIncrementalLoadsParentAfterChild]
1620+
# cmd: mypy -m r.s
1621+
1622+
[file r/__init__.py]
1623+
from . import s
1624+
1625+
[file r/m.py]
1626+
class R: pass
1627+
1628+
[file r/s.py]
1629+
from . import m
1630+
R = m.R
1631+
a = None # type: R
1632+
1633+
[file r/s.py.next]
1634+
from . import m
1635+
R = m.R
1636+
a = None # type: R

0 commit comments

Comments
 (0)