Skip to content

Commit e91bc30

Browse files
committed
Explicitly don't handle import from
1 parent e407531 commit e91bc30

File tree

3 files changed

+77
-28
lines changed

3 files changed

+77
-28
lines changed

mypy/build.py

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,26 @@ def getmtime(self, path: str) -> int:
744744
else:
745745
return int(self.metastore.getmtime(path))
746746

747+
def correct_rel_imp(self, file: MypyFile, imp: ImportFrom | ImportAll) -> str:
748+
"""Function to correct for relative imports."""
749+
file_id = file.fullname
750+
rel = imp.relative
751+
if rel == 0:
752+
return imp.id
753+
if os.path.basename(file.path).startswith("__init__."):
754+
rel -= 1
755+
if rel != 0:
756+
file_id = ".".join(file_id.split(".")[:-rel])
757+
new_id = file_id + "." + imp.id if imp.id else file_id
758+
759+
if not new_id:
760+
self.errors.set_file(file.path, file.name, self.options)
761+
self.errors.report(
762+
imp.line, 0, "No parent module -- cannot perform relative import", blocker=True
763+
)
764+
765+
return new_id
766+
747767
def all_imported_modules_in_file(self, file: MypyFile) -> list[tuple[int, str, int]]:
748768
"""Find all reachable import statements in a file.
749769
@@ -752,27 +772,6 @@ def all_imported_modules_in_file(self, file: MypyFile) -> list[tuple[int, str, i
752772
753773
Can generate blocking errors on bogus relative imports.
754774
"""
755-
756-
def correct_rel_imp(imp: ImportFrom | ImportAll) -> str:
757-
"""Function to correct for relative imports."""
758-
file_id = file.fullname
759-
rel = imp.relative
760-
if rel == 0:
761-
return imp.id
762-
if os.path.basename(file.path).startswith("__init__."):
763-
rel -= 1
764-
if rel != 0:
765-
file_id = ".".join(file_id.split(".")[:-rel])
766-
new_id = file_id + "." + imp.id if imp.id else file_id
767-
768-
if not new_id:
769-
self.errors.set_file(file.path, file.name, self.options)
770-
self.errors.report(
771-
imp.line, 0, "No parent module -- cannot perform relative import", blocker=True
772-
)
773-
774-
return new_id
775-
776775
res: list[tuple[int, str, int]] = []
777776
for imp in file.imports:
778777
if not imp.is_unreachable:
@@ -787,7 +786,7 @@ def correct_rel_imp(imp: ImportFrom | ImportAll) -> str:
787786
ancestors.append(part)
788787
res.append((ancestor_pri, ".".join(ancestors), imp.line))
789788
elif isinstance(imp, ImportFrom):
790-
cur_id = correct_rel_imp(imp)
789+
cur_id = self.correct_rel_imp(file, imp)
791790
all_are_submodules = True
792791
# Also add any imported names that are submodules.
793792
pri = import_priority(imp, PRI_MED)
@@ -807,7 +806,7 @@ def correct_rel_imp(imp: ImportFrom | ImportAll) -> str:
807806
res.append((pri, cur_id, imp.line))
808807
elif isinstance(imp, ImportAll):
809808
pri = import_priority(imp, PRI_HIGH)
810-
res.append((pri, correct_rel_imp(imp), imp.line))
809+
res.append((pri, self.correct_rel_imp(file, imp), imp.line))
811810

812811
# Sort such that module (e.g. foo.bar.baz) comes before its ancestors (e.g. foo
813812
# and foo.bar) so that, if FindModuleCache finds the target module in a
@@ -2901,9 +2900,7 @@ def dispatch(sources: list[BuildSource], manager: BuildManager, stdout: TextIO)
29012900
graph = load_graph(sources, manager)
29022901

29032902
for id in graph:
2904-
ancestors = graph[id].ancestors
2905-
assert ancestors is not None
2906-
manager.import_map[id] = set(graph[id].dependencies + graph[id].suppressed + ancestors)
2903+
manager.import_map[id] = set(graph[id].dependencies + graph[id].suppressed)
29072904

29082905
t1 = time.time()
29092906
manager.add_stats(

mypy/semanal.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,13 @@ def __init__(
537537
self.type_expression_full_parse_failure_count: int = 0 # Failed full parses
538538

539539
# Imports of submodules transitively visible from given module.
540+
# This is needed to support patterns like this
541+
# [a.py]
542+
# import b
543+
# import foo
544+
# foo.bar # <- this should work even if bar is not re-exported in foo
545+
# [b.py]
546+
# import foo.bar
540547
self.transitive_submodule_imports: dict[str, set[str]] = {}
541548

542549
# mypyc doesn't properly handle implementing an abstractproperty
@@ -6642,7 +6649,7 @@ def get_module_symbol(self, node: MypyFile, name: str) -> SymbolTableNode | None
66426649
sym = names.get(name)
66436650
if not sym:
66446651
fullname = module + "." + name
6645-
if fullname in self.modules and self.is_visible_import(fullname):
6652+
if fullname in self.modules and self.is_visible_import(module, fullname):
66466653
sym = SymbolTableNode(GDEF, self.modules[fullname])
66476654
elif self.is_incomplete_namespace(module):
66486655
self.record_incomplete_ref()
@@ -6661,12 +6668,20 @@ def get_module_symbol(self, node: MypyFile, name: str) -> SymbolTableNode | None
66616668
sym = None
66626669
return sym
66636670

6664-
def is_visible_import(self, id: str) -> bool:
6671+
def is_visible_import(self, base_id: str, id: str) -> bool:
6672+
if base_id not in self.transitive_submodule_imports:
6673+
# This is a performance optimization for a common pattern. If one module
6674+
# in a codebase uses import numpy as np; np.foo.bar, then it is likely that
6675+
# other modules use similar pattern as well. So we pre-compute transitive
6676+
# dependencies for np, to avoid possible duplicate work in the future.
6677+
self.add_transitive_submodule_imports(base_id)
66656678
if self.cur_mod_id not in self.transitive_submodule_imports:
66666679
self.add_transitive_submodule_imports(self.cur_mod_id)
66676680
return id in self.transitive_submodule_imports[self.cur_mod_id]
66686681

66696682
def add_transitive_submodule_imports(self, mod_id: str) -> None:
6683+
if mod_id not in self.import_map:
6684+
return
66706685
todo = self.import_map[mod_id]
66716686
seen = {mod_id}
66726687
result = {mod_id}

test-data/unit/check-incremental.test

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7543,8 +7543,43 @@ tmp/a.py:3: error: "object" has no attribute "submod"
75437543
tmp/a.py:3: error: "object" has no attribute "submod"
75447544

75457545
[case testIncrementalAccessSubmoduleWithoutExplicitImportNested]
7546+
import a
7547+
7548+
[file a.py]
7549+
import pandas
7550+
pandas.core.dtypes
7551+
7552+
[file a.py.2]
7553+
import pandas
7554+
pandas.core.dtypes
7555+
# touch
7556+
7557+
[file pandas/__init__.py]
7558+
import pandas.core.api
7559+
7560+
[file pandas/core/__init__.py]
7561+
[file pandas/core/api.py]
7562+
import pandas.core.dtypes.dtypes
7563+
7564+
[file pandas/core/dtypes/__init__.py]
7565+
[file pandas/core/dtypes/dtypes.py]
7566+
X = 0
7567+
[out]
7568+
[out2]
7569+
7570+
[case testIncrementalAccessSubmoduleWithoutExplicitImportNestedFrom]
7571+
import a
7572+
7573+
[file a.py]
75467574
import pandas
75477575

7576+
# Although this actually works at runtime, we do not support this, since
7577+
# this would cause major slowdown for a rare edge case. This test verifies
7578+
# that we fail consistently on cold and warm runs.
7579+
pandas.core.dtypes
7580+
7581+
[file a.py.2]
7582+
import pandas
75487583
pandas.core.dtypes
75497584

75507585
[file pandas/__init__.py]
@@ -7558,4 +7593,6 @@ from pandas.core.dtypes.dtypes import X
75587593
[file pandas/core/dtypes/dtypes.py]
75597594
X = 0
75607595
[out]
7596+
tmp/a.py:6: error: "object" has no attribute "dtypes"
75617597
[out2]
7598+
tmp/a.py:2: error: "object" has no attribute "dtypes"

0 commit comments

Comments
 (0)