Skip to content

Commit 51fb765

Browse files
ilevkivskyigvanrossum
authored andcommitted
Delete cache for a module if errors are found (#4045)
Fixes #4043 Fixes #3852 Fixes #3052 This is a simple-minded solution for inconsistent cache state problem discovered in #4043.
1 parent d23fc47 commit 51fb765

File tree

2 files changed

+110
-5
lines changed

2 files changed

+110
-5
lines changed

mypy/build.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import sys
2222
import time
2323
from os.path import dirname, basename
24+
import errno
2425

2526
from typing import (AbstractSet, Dict, Iterable, Iterator, List, cast, Any,
2627
NamedTuple, Optional, Set, Tuple, Union, Callable)
@@ -469,7 +470,7 @@ class BuildManager:
469470
all_types: Map {Expression: Type} collected from all modules
470471
options: Build options
471472
missing_modules: Set of modules that could not be imported encountered so far
472-
stale_modules: Set of modules that needed to be rechecked
473+
stale_modules: Set of modules that needed to be rechecked (only used by tests)
473474
version_id: The current mypy version (based on commit id when possible)
474475
plugin: Active mypy plugin(s)
475476
errors: Used for reporting all errors
@@ -1165,6 +1166,25 @@ def write_cache(id: str, path: str, tree: MypyFile,
11651166
return interface_hash
11661167

11671168

1169+
def delete_cache(id: str, path: str, manager: BuildManager) -> None:
1170+
"""Delete cache files for a module.
1171+
1172+
The cache files for a module are deleted when mypy finds errors there.
1173+
This avoids inconsistent states with cache files from different mypy runs,
1174+
see #4043 for an example.
1175+
"""
1176+
path = os.path.abspath(path)
1177+
meta_json, data_json = get_cache_names(id, path, manager)
1178+
manager.log('Deleting {} {} {} {}'.format(id, path, meta_json, data_json))
1179+
1180+
for filename in [data_json, meta_json]:
1181+
try:
1182+
os.remove(filename)
1183+
except OSError as e:
1184+
if e.errno != errno.ENOENT:
1185+
manager.log("Error deleting cache file {}: {}".format(filename, e.strerror))
1186+
1187+
11681188
"""Dependency manager.
11691189
11701190
Design
@@ -1534,11 +1554,12 @@ def mark_as_rechecked(self) -> None:
15341554
"""Marks this module as having been fully re-analyzed by the type-checker."""
15351555
self.manager.rechecked_modules.add(self.id)
15361556

1537-
def mark_interface_stale(self) -> None:
1557+
def mark_interface_stale(self, *, on_errors: bool = False) -> None:
15381558
"""Marks this module as having a stale public interface, and discards the cache data."""
15391559
self.meta = None
15401560
self.externally_same = False
1541-
self.manager.stale_modules.add(self.id)
1561+
if not on_errors:
1562+
self.manager.stale_modules.add(self.id)
15421563

15431564
def check_blockers(self) -> None:
15441565
"""Raise CompileError if a blocking error is detected."""
@@ -1813,6 +1834,8 @@ def write_cache(self) -> None:
18131834
else:
18141835
is_errors = self.manager.errors.is_errors()
18151836
if is_errors:
1837+
delete_cache(self.id, self.path, self.manager)
1838+
self.mark_interface_stale(on_errors=True)
18161839
return
18171840
dep_prios = [self.priorities.get(dep, PRI_HIGH) for dep in self.dependencies]
18181841
new_interface_hash = write_cache(

test-data/unit/check-incremental.test

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
-- Any files that we expect to be rechecked should be annotated in the [rechecked]
2828
-- annotation, and any files expect to be stale (aka have a modified interface)
2929
-- should be annotated in the [stale] annotation. Note that a file that ends up
30-
-- producing an error does not create a new cache file and so is not considered stale.
30+
-- producing an error has its caches deleted and is marked stale automatically.
31+
-- Such files don't need to be included in [stale ...] list.
3132
--
3233
-- The test suite will automatically assume that __main__ is stale and rechecked in
3334
-- all cases so we can avoid constantly having to annotate it. The list of
@@ -200,7 +201,7 @@ def foo() -> int:
200201
return "foo"
201202
return inner2()
202203

203-
[rechecked mod2]
204+
[rechecked mod1, mod2]
204205
[stale]
205206
[out2]
206207
tmp/mod2.py:4: error: Incompatible return value type (got "str", expected "int")
@@ -2800,6 +2801,87 @@ b.x.y
28002801
tmp/c.py:2: error: Revealed type is '<stale cache: consider running mypy without --quick>'
28012802
tmp/c.py:5: error: "<stale cache: consider running mypy without --quick>" has no attribute "y"
28022803

2804+
[case testCacheDeletedAfterErrorsFound]
2805+
import a
2806+
[file a.py]
2807+
from b import x
2808+
[file b.py]
2809+
from c import x
2810+
[file c.py]
2811+
x = 1
2812+
[file c.py.2]
2813+
1 + 1
2814+
[file a.py.3]
2815+
from b import x
2816+
1 + 1
2817+
[out]
2818+
[out2]
2819+
tmp/b.py:1: error: Module 'c' has no attribute 'x'
2820+
tmp/a.py:1: error: Module 'b' has no attribute 'x'
2821+
[out3]
2822+
tmp/b.py:1: error: Module 'c' has no attribute 'x'
2823+
tmp/a.py:1: error: Module 'b' has no attribute 'x'
2824+
2825+
[case testCacheDeletedAfterErrorsFound2]
2826+
import a
2827+
[file a.py]
2828+
from b import x
2829+
[file b.py]
2830+
from c import C
2831+
x: C
2832+
[file c.py]
2833+
class C: pass
2834+
[file c.py.2]
2835+
def C(): pass
2836+
[file a.py.3]
2837+
from b import x
2838+
1 + 1
2839+
[out]
2840+
[out2]
2841+
tmp/b.py:2: error: Invalid type "c.C"
2842+
[out3]
2843+
tmp/b.py:2: error: Invalid type "c.C"
2844+
2845+
[case testCacheDeletedAfterErrorsFound3]
2846+
import a
2847+
[file a.py]
2848+
import b
2849+
b.f()
2850+
[file b.py]
2851+
def f() -> None: pass
2852+
[file b.py.2]
2853+
def f(x) -> None: pass
2854+
[out]
2855+
[out2]
2856+
tmp/a.py:2: error: Too few arguments for "f"
2857+
[out3]
2858+
tmp/a.py:2: error: Too few arguments for "f"
2859+
2860+
[case testCacheDeletedAfterErrorsFound4]
2861+
import a
2862+
[file a.py]
2863+
from b import x
2864+
[file b.py]
2865+
from c import x
2866+
[file c.py]
2867+
from d import x
2868+
[file d.py]
2869+
x = 1
2870+
[file d.py.2]
2871+
1 + 1
2872+
[file a.py.3]
2873+
from b import x
2874+
1 + 1
2875+
[out]
2876+
[out2]
2877+
tmp/c.py:1: error: Module 'd' has no attribute 'x'
2878+
tmp/b.py:1: error: Module 'c' has no attribute 'x'
2879+
tmp/a.py:1: error: Module 'b' has no attribute 'x'
2880+
[out3]
2881+
tmp/c.py:1: error: Module 'd' has no attribute 'x'
2882+
tmp/b.py:1: error: Module 'c' has no attribute 'x'
2883+
tmp/a.py:1: error: Module 'b' has no attribute 'x'
2884+
28032885
[case testNoCrashOnDoubleImportAliasQuick]
28042886
# cmd: mypy -m e
28052887
# cmd2: mypy -m c

0 commit comments

Comments
 (0)