Skip to content

Commit 01de89f

Browse files
authored
Fix incremental speed regression (#4331)
Fixes #4135. We now decide more carefully whether a module has an error -- only if there's an error for that specific file or for any of its (transitive) dependencies (including the SCC it's a member of). Previously, once an error was detected, *all* modules processed later were considered to be at risk.
1 parent 26239c5 commit 01de89f

File tree

2 files changed

+37
-4
lines changed

2 files changed

+37
-4
lines changed

mypy/build.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -1474,6 +1474,9 @@ class State:
14741474
# Whether to ignore all errors
14751475
ignore_all = False
14761476

1477+
# Whether the module has an error or any of its dependencies have one.
1478+
transitive_error = False
1479+
14771480
# Type checker used for checking this file. Use type_checker() for
14781481
# access and to construct this on demand.
14791482
_type_checker = None # type: Optional[TypeChecker]
@@ -1944,7 +1947,7 @@ def write_cache(self) -> None:
19441947
if self.manager.options.quick_and_dirty:
19451948
is_errors = self.manager.errors.is_errors_for_file(self.path)
19461949
else:
1947-
is_errors = self.manager.errors.is_errors()
1950+
is_errors = self.transitive_error
19481951
if is_errors:
19491952
delete_cache(self.id, self.path, self.manager)
19501953
self.meta = None
@@ -2257,6 +2260,12 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
22572260
else:
22582261
fresh_msg = "stale due to deps (%s)" % " ".join(sorted(stale_deps))
22592262

2263+
# Initialize transitive_error for all SCC members from union
2264+
# of transitive_error of dependencies.
2265+
if any(graph[dep].transitive_error for dep in deps if dep in graph):
2266+
for id in scc:
2267+
graph[id].transitive_error = True
2268+
22602269
scc_str = " ".join(scc)
22612270
if fresh:
22622271
if not maybe_reuse_in_memory_tree(graph, scc, manager):
@@ -2272,6 +2281,11 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
22722281
# single fresh SCC. This is intentional -- we don't need those modules
22732282
# loaded if there are no more stale SCCs to be rechecked.
22742283
#
2284+
# Also note we shouldn't have to worry about transitive_error here,
2285+
# since modules with transitive errors aren't written to the cache,
2286+
# and if any dependencies were changed, this SCC would be stale.
2287+
# (Also, in quick_and_dirty mode we don't care about transitive errors.)
2288+
#
22752289
# TODO: see if it's possible to determine if we need to process only a
22762290
# _subset_ of the past SCCs instead of having to process them all.
22772291
for prev_scc in fresh_scc_queue:
@@ -2416,6 +2430,7 @@ def can_reuse_in_memory_tree(graph: Graph, scc: List[str], manager: BuildManager
24162430
# Check that all dependencies were loaded from memory.
24172431
# If not, some dependency was reparsed but the interface hash
24182432
# wasn't changed -- in that case we can't reuse the tree.
2433+
# TODO: Pass deps in from process_graph(), via maybe_reuse_in_memory_tree()?
24192434
deps = set(dep for id in scc for dep in graph[id].dependencies if dep in graph)
24202435
deps -= set(scc) # Subtract the SCC itself (else nothing will be safe)
24212436
if all(graph[dep].is_from_saved_cache for dep in deps):
@@ -2464,6 +2479,9 @@ def process_stale_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No
24642479
for id in stale:
24652480
if graph[id].type_check_second_pass():
24662481
more = True
2482+
if any(manager.errors.is_errors_for_file(graph[id].xpath) for id in stale):
2483+
for id in stale:
2484+
graph[id].transitive_error = True
24672485
for id in stale:
24682486
graph[id].finish_passes()
24692487
graph[id].write_cache()

test-data/unit/check-incremental.test

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-- Checks for incremental mode (see testcheck.py).
2-
-- Each test is run twice, once with a cold cache, once with a warm cache.
2+
-- Each test is run at least twice, once with a cold cache, once with a warm cache.
33
-- Before the tests are run again, in step N any *.py.N files are copied to
4-
-- *.py.
4+
-- *.py. There are at least two runs; more as long as there are *.py.N files.
55
--
66
-- You can add an empty section like `[delete mod.py.2]` to delete `mod.py`
77
-- before the second run.
@@ -1113,7 +1113,7 @@ val = "foo"
11131113

11141114
[builtins fixtures/module_all.pyi]
11151115
[rechecked main, c, c.submodule]
1116-
[stale]
1116+
[stale c]
11171117
[out2]
11181118
tmp/c/submodule.py:2: error: Incompatible types in assignment (expression has type "str", variable has type "int")
11191119
tmp/main.py:7: error: "C" has no attribute "foo"
@@ -3432,3 +3432,18 @@ extra = 1
34323432

34333433
[out1]
34343434
[out2]
3435+
3436+
[case testErrorsAffectDependentsOnly]
3437+
# cmd: mypy -m m.a m.b m.c
3438+
[file m/__init__.py]
3439+
[file m/a.py]
3440+
1 + '' # Deliberate error
3441+
[file m/b.py]
3442+
import m.a # Depends on module with error
3443+
[file m/c.py]
3444+
import m # No error here
3445+
[rechecked m.a, m.b]
3446+
[out1]
3447+
tmp/m/a.py:1: error: Unsupported operand types for + ("int" and "str")
3448+
[out2]
3449+
tmp/m/a.py:1: error: Unsupported operand types for + ("int" and "str")

0 commit comments

Comments
 (0)