Skip to content

Commit 64c0acd

Browse files
Michael0x2agvanrossum
authored andcommitted
Fix importing new files in incremental mode (#1865)
This pull request makes mypy re-parse __init__.py files within modules when a new file is added to that module before incremental mode is run a second time. Previously, when a file was modified to contain a new "from x import y" where "y" is the newly created submodule, mypy would assume that "y" was instead a new attribute added to the "x" module. Fixes #1848.
1 parent c623cae commit 64c0acd

File tree

3 files changed

+61
-3
lines changed

3 files changed

+61
-3
lines changed

mypy/build.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ def default_lib_path(data_dir: str, pyversion: Tuple[int, int]) -> List[str]:
281281
('data_mtime', float), # mtime of data_json
282282
('data_json', str), # path of <id>.data.json
283283
('suppressed', List[str]), # dependencies that weren't imported
284+
('child_modules', List[str]), # all submodules of the given module
284285
('options', Optional[Dict[str, bool]]), # build options
285286
('dep_prios', List[int]),
286287
])
@@ -726,6 +727,7 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
726727
meta.get('data_mtime'),
727728
data_json,
728729
meta.get('suppressed', []),
730+
meta.get('child_modules', []),
729731
meta.get('options'),
730732
meta.get('dep_prios', []),
731733
)
@@ -749,6 +751,7 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
749751
if st.st_mtime != m.mtime or st.st_size != m.size:
750752
manager.log('Metadata abandoned because of modified file {}'.format(path))
751753
return None
754+
752755
# It's a match on (id, path, mtime, size).
753756
# Check data_json; assume if its mtime matches it's good.
754757
# TODO: stat() errors
@@ -777,7 +780,7 @@ def random_string() -> str:
777780

778781
def write_cache(id: str, path: str, tree: MypyFile,
779782
dependencies: List[str], suppressed: List[str],
780-
dep_prios: List[int],
783+
child_modules: List[str], dep_prios: List[int],
781784
manager: BuildManager) -> None:
782785
"""Write cache files for a module.
783786
@@ -817,6 +820,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
817820
'data_mtime': data_mtime,
818821
'dependencies': dependencies,
819822
'suppressed': suppressed,
823+
'child_modules': child_modules,
820824
'options': select_options_affecting_cache(manager.options),
821825
'dep_prios': dep_prios,
822826
}
@@ -999,6 +1003,9 @@ class State:
9991003
# Parent package, its parent, etc.
10001004
ancestors = None # type: Optional[List[str]]
10011005

1006+
# A list of all direct submodules of a given module
1007+
child_modules = None # type: Optional[Set[str]]
1008+
10021009
# List of (path, line number) tuples giving context for import
10031010
import_context = None # type: List[Tuple[str, int]]
10041011

@@ -1096,11 +1103,13 @@ def __init__(self,
10961103
assert len(self.meta.dependencies) == len(self.meta.dep_prios)
10971104
self.priorities = {id: pri
10981105
for id, pri in zip(self.meta.dependencies, self.meta.dep_prios)}
1106+
self.child_modules = set(self.meta.child_modules)
10991107
self.dep_line_map = {}
11001108
else:
11011109
# Parse the file (and then some) to get the dependencies.
11021110
self.parse_file()
11031111
self.suppressed = []
1112+
self.child_modules = set()
11041113

11051114
def skipping_ancestor(self, id: str, path: str, ancestor_for: 'State') -> None:
11061115
# TODO: Read the path (the __init__.py file) and return
@@ -1147,7 +1156,13 @@ def is_fresh(self) -> bool:
11471156
# self.meta.dependencies when a dependency is dropped due to
11481157
# suppression by --silent-imports. However when a suppressed
11491158
# dependency is added back we find out later in the process.
1150-
return self.meta is not None and self.dependencies == self.meta.dependencies
1159+
return (self.meta is not None
1160+
and self.dependencies == self.meta.dependencies
1161+
and self.child_modules == set(self.meta.child_modules))
1162+
1163+
def has_new_submodules(self) -> bool:
1164+
"""Return if this module has new submodules after being loaded from a warm cache."""
1165+
return self.meta is not None and self.child_modules != set(self.meta.child_modules)
11511166

11521167
def mark_stale(self) -> None:
11531168
"""Throw away the cache data for this file, marking it as stale."""
@@ -1308,7 +1323,7 @@ def write_cache(self) -> None:
13081323
if self.path and self.manager.options.incremental and not self.manager.errors.is_errors():
13091324
dep_prios = [self.priorities.get(dep, PRI_HIGH) for dep in self.dependencies]
13101325
write_cache(self.id, self.path, self.tree,
1311-
list(self.dependencies), list(self.suppressed),
1326+
list(self.dependencies), list(self.suppressed), list(self.child_modules),
13121327
dep_prios,
13131328
self.manager)
13141329

@@ -1366,6 +1381,11 @@ def load_graph(sources: List[BuildSource], manager: BuildManager) -> Graph:
13661381
assert newst.id not in graph, newst.id
13671382
graph[newst.id] = newst
13681383
new.append(newst)
1384+
if dep in st.ancestors and dep in graph:
1385+
graph[dep].child_modules.add(st.id)
1386+
for id, g in graph.items():
1387+
if g.has_new_submodules():
1388+
g.parse_file()
13691389
return graph
13701390

13711391

mypy/test/data.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def __init__(self, name, input, output, file, line, lastline,
124124

125125
def set_up(self) -> None:
126126
super().set_up()
127+
encountered_files = set()
127128
self.clean_up = []
128129
for path, content in self.files:
129130
dir = os.path.dirname(path)
@@ -133,6 +134,13 @@ def set_up(self) -> None:
133134
f.write(content)
134135
f.close()
135136
self.clean_up.append((False, path))
137+
encountered_files.add(path)
138+
if path.endswith(".next"):
139+
# Make sure new files introduced in the second run are accounted for
140+
renamed_path = path[:-5]
141+
if renamed_path not in encountered_files:
142+
encountered_files.add(renamed_path)
143+
self.clean_up.append((False, renamed_path))
136144

137145
def add_dirs(self, dir: str) -> List[str]:
138146
"""Add all subdirectories required to create dir.

test-data/unit/check-incremental.test

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,33 @@ import parent.a
155155
[builtins fixtures/args.py]
156156
[stale]
157157
[out]
158+
159+
[case testIncrementalReferenceNewFileWithImportFrom]
160+
from parent import a
161+
162+
[file parent/__init__.py]
163+
164+
[file parent/a.py]
165+
166+
[file parent/a.py.next]
167+
from parent import b
168+
169+
[file parent/b.py.next]
170+
171+
[stale parent, parent.a, parent.b]
172+
[out]
173+
174+
[case testIncrementalReferenceExistingFileWithImportFrom]
175+
from parent import a, b
176+
177+
[file parent/__init__.py]
178+
179+
[file parent/a.py]
180+
181+
[file parent/b.py]
182+
183+
[file parent/a.py.next]
184+
from parent import b
185+
186+
[stale parent.a]
187+
[out]

0 commit comments

Comments
 (0)