Skip to content

Commit ac90292

Browse files
authored
New files shouldn't trigger a coarse-grained rebuild in fg cache mode (#4669)
1 parent 96e44e0 commit ac90292

File tree

4 files changed

+151
-4
lines changed

4 files changed

+151
-4
lines changed

mypy/build.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,7 @@ def __init__(self, data_dir: str,
588588
self.data_dir = data_dir
589589
self.errors = errors
590590
self.errors.set_ignore_prefix(ignore_prefix)
591+
self.only_load_from_cache = options.use_fine_grained_cache
591592
self.lib_path = tuple(lib_path)
592593
self.source_set = source_set
593594
self.reports = reports
@@ -1674,6 +1675,13 @@ def __init__(self,
16741675
for id, line in zip(self.meta.dependencies, self.meta.dep_lines)}
16751676
self.child_modules = set(self.meta.child_modules)
16761677
else:
1678+
# In fine-grained cache mode, pretend we only know about modules that
1679+
# have cache information and defer handling new modules until the
1680+
# fine-grained update.
1681+
if manager.only_load_from_cache:
1682+
manager.log("Deferring module to fine-grained update %s (%s)" % (path, id))
1683+
raise ModuleNotFound
1684+
16771685
# Parse the file (and then some) to get the dependencies.
16781686
self.parse_file()
16791687
self.compute_dependencies()
@@ -2093,6 +2101,15 @@ def dispatch(sources: List[BuildSource], manager: BuildManager) -> Graph:
20932101
manager.log("Mypy version %s" % __version__)
20942102
t0 = time.time()
20952103
graph = load_graph(sources, manager)
2104+
2105+
# This is a kind of unfortunate hack to work around some of fine-grained's
2106+
# fragility: if we have loaded less than 50% of the specified files from
2107+
# cache in fine-grained cache mode, load the graph again honestly.
2108+
if manager.options.use_fine_grained_cache and len(graph) < 0.50 * len(sources):
2109+
manager.log("Redoing load_graph because too much was missing")
2110+
manager.only_load_from_cache = False
2111+
graph = load_graph(sources, manager)
2112+
20962113
t1 = time.time()
20972114
manager.add_stats(graph_size=len(graph),
20982115
stubs_found=sum(g.path is not None and g.path.endswith('.pyi')
@@ -2206,7 +2223,7 @@ def load_graph(sources: List[BuildSource], manager: BuildManager,
22062223
there are syntax errors.
22072224
"""
22082225

2209-
graph = old_graph or {} # type: Graph
2226+
graph = old_graph if old_graph is not None else {} # type: Graph
22102227

22112228
# The deque is used to implement breadth-first traversal.
22122229
# TODO: Consider whether to go depth-first instead. This may

mypy/server/update.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ def __init__(self,
174174
# this directly reflected in load_graph's interface.
175175
self.options.cache_dir = os.devnull
176176
manager.saved_cache = {}
177+
manager.only_load_from_cache = False
177178
# Active triggers during the last update
178179
self.triggered = [] # type: List[str]
179180

mypy/test/testfinegrained.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
7575
return
7676

7777
main_src = '\n'.join(testcase.input)
78-
sources_override = self.parse_sources(main_src)
78+
step = 1
79+
sources_override = self.parse_sources(main_src, step)
7980
messages, manager, graph = self.build(main_src, testcase, sources_override,
8081
build_cache=self.use_cache,
8182
enable_cache=self.use_cache)
@@ -92,6 +93,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
9293
steps = testcase.find_steps()
9394
all_triggered = []
9495
for operations in steps:
96+
step += 1
9597
modules = []
9698
for op in operations:
9799
if isinstance(op, UpdateFile):
@@ -102,6 +104,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
102104
# Delete file
103105
os.remove(op.path)
104106
modules.append((op.module, op.path))
107+
sources_override = self.parse_sources(main_src, step)
105108
if sources_override is not None:
106109
modules = [(module, path)
107110
for module, path in sources_override
@@ -181,14 +184,22 @@ def format_triggered(self, triggered: List[List[str]]) -> List[str]:
181184
result.append(('%d: %s' % (n + 2, ', '.join(filtered))).strip())
182185
return result
183186

184-
def parse_sources(self, program_text: str) -> Optional[List[Tuple[str, str]]]:
187+
def parse_sources(self, program_text: str,
188+
incremental_step: int) -> Optional[List[Tuple[str, str]]]:
185189
"""Return target (module, path) tuples for a test case, if not using the defaults.
186190
187191
These are defined through a comment like '# cmd: main a.py' in the test case
188192
description.
189193
"""
190-
# TODO: Support defining separately for each incremental step.
191194
m = re.search('# cmd: mypy ([a-zA-Z0-9_./ ]+)$', program_text, flags=re.MULTILINE)
195+
regex = '# cmd{}: mypy ([a-zA-Z0-9_./ ]+)$'.format(incremental_step)
196+
alt_m = re.search(regex, program_text, flags=re.MULTILINE)
197+
if alt_m is not None and incremental_step > 1:
198+
# Optionally return a different command if in a later step
199+
# of incremental mode, otherwise default to reusing the
200+
# original cmd.
201+
m = alt_m
202+
192203
if m:
193204
# The test case wants to use a non-default set of files.
194205
paths = m.group(1).strip().split()

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

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,3 +971,121 @@ x = Foo()
971971
==
972972
==
973973
main:2: error: Too few arguments for "foo" of "Foo"
974+
975+
-- This series of tests is designed to test adding a new module that
976+
-- doesn't appear in the cache, for cache mode. They aren't run only
977+
-- in cache mode, though, because they are still perfectly good
978+
-- regular tests.
979+
[case testAddModuleAfterCache1]
980+
# cmd: mypy main a.py
981+
# cmd2: mypy main a.py b.py
982+
# cmd3: mypy main a.py b.py
983+
import a
984+
[file a.py]
985+
pass
986+
[file a.py.2]
987+
import b
988+
b.foo(0)
989+
[file b.py.2]
990+
def foo() -> None: pass
991+
[file b.py.3]
992+
def foo(x: int) -> None: pass
993+
[out]
994+
==
995+
a.py:2: error: Too many arguments for "foo"
996+
==
997+
998+
[case testAddModuleAfterCache2]
999+
# cmd: mypy main a.py
1000+
# cmd2: mypy main a.py b.py
1001+
# cmd3: mypy main a.py b.py
1002+
# flags: --ignore-missing-imports --follow-imports=skip
1003+
import a
1004+
[file a.py]
1005+
import b
1006+
b.foo(0)
1007+
[file b.py.2]
1008+
def foo() -> None: pass
1009+
[file b.py.3]
1010+
def foo(x: int) -> None: pass
1011+
[out]
1012+
==
1013+
a.py:2: error: Too many arguments for "foo"
1014+
==
1015+
1016+
[case testAddModuleAfterCache3]
1017+
# cmd: mypy main a.py
1018+
# cmd2: mypy main a.py b.py c.py d.py e.py f.py g.py
1019+
# flags: --ignore-missing-imports --follow-imports=skip
1020+
import a
1021+
[file a.py]
1022+
import b, c, d, e, f, g
1023+
[file b.py.2]
1024+
[file c.py.2]
1025+
[file d.py.2]
1026+
[file e.py.2]
1027+
[file f.py.2]
1028+
[file g.py.2]
1029+
[out]
1030+
==
1031+
1032+
[case testAddModuleAfterCache4]
1033+
# cmd: mypy main a.py
1034+
# cmd2: mypy main a.py b.py
1035+
# cmd3: mypy main a.py b.py
1036+
# flags: --ignore-missing-imports --follow-imports=skip
1037+
import a
1038+
import b
1039+
[file a.py]
1040+
def foo() -> None: pass
1041+
[file b.py.2]
1042+
import a
1043+
a.foo(10)
1044+
[file a.py.3]
1045+
def foo(x: int) -> None: pass
1046+
[out]
1047+
==
1048+
b.py:2: error: Too many arguments for "foo"
1049+
==
1050+
1051+
[case testAddModuleAfterCache5]
1052+
# cmd: mypy main a.py
1053+
# cmd2: mypy main a.py b.py
1054+
# cmd3: mypy main a.py b.py
1055+
# flags: --ignore-missing-imports --follow-imports=skip
1056+
import a
1057+
import b
1058+
[file a.py]
1059+
def foo(x: int) -> None: pass
1060+
[file a.py.2]
1061+
def foo() -> None: pass
1062+
[file b.py.2]
1063+
import a
1064+
a.foo(10)
1065+
[file a.py.3]
1066+
def foo(x: int) -> None: pass
1067+
[out]
1068+
==
1069+
b.py:2: error: Too many arguments for "foo"
1070+
==
1071+
1072+
[case testAddModuleAfterCache6]
1073+
# cmd: mypy main a.py
1074+
# cmd2: mypy main a.py b.py
1075+
# cmd3: mypy main a.py b.py
1076+
# flags: --ignore-missing-imports --follow-imports=skip
1077+
import a
1078+
[file a.py]
1079+
import b
1080+
b.foo()
1081+
[file a.py.2]
1082+
import b
1083+
b.foo(0)
1084+
[file b.py.2]
1085+
def foo() -> None: pass
1086+
[file b.py.3]
1087+
def foo(x: int) -> None: pass
1088+
[out]
1089+
==
1090+
a.py:2: error: Too many arguments for "foo"
1091+
==

0 commit comments

Comments
 (0)