Skip to content

Commit 477f85a

Browse files
authored
Store line numbers of imports in the cache metadata (#4533)
This allows incremental modes to generate error messages that are both better and more consistent with non-incremental modes. Storing the data in a parallel array is kind of ugly and was done for consistency with dep_prios. It might be worth doing a refactor, though any other way will make the json bigger, which is presumbably why dep_prios was done with an array.
1 parent fc2c678 commit 477f85a

File tree

5 files changed

+36
-20
lines changed

5 files changed

+36
-20
lines changed

mypy/build.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ def default_lib_path(data_dir: str,
393393
('child_modules', List[str]), # all submodules of the given module
394394
('options', Optional[Dict[str, object]]), # build options
395395
('dep_prios', List[int]),
396+
('dep_lines', List[int]),
396397
('interface_hash', str), # hash representing the public interface
397398
('version_id', str), # mypy version for cache invalidation
398399
('ignore_all', bool), # if errors were ignored
@@ -418,6 +419,7 @@ def cache_meta_from_dict(meta: Dict[str, Any], data_json: str) -> CacheMeta:
418419
meta.get('child_modules', []),
419420
meta.get('options'),
420421
meta.get('dep_prios', []),
422+
meta.get('dep_lines', []),
421423
meta.get('interface_hash', ''),
422424
meta.get('version_id', sentinel),
423425
meta.get('ignore_all', True),
@@ -1040,7 +1042,8 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
10401042
# Ignore cache if generated by an older mypy version.
10411043
if ((m.version_id != manager.version_id and not manager.options.skip_version_check)
10421044
or m.options is None
1043-
or len(m.dependencies) != len(m.dep_prios)):
1045+
or len(m.dependencies) != len(m.dep_prios)
1046+
or len(m.dependencies) != len(m.dep_lines)):
10441047
manager.log('Metadata abandoned for {}: new attributes are missing'.format(id))
10451048
return None
10461049

@@ -1157,6 +1160,7 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
11571160
'options': (manager.options.clone_for_module(id)
11581161
.select_options_affecting_cache()),
11591162
'dep_prios': meta.dep_prios,
1163+
'dep_lines': meta.dep_lines,
11601164
'interface_hash': meta.interface_hash,
11611165
'version_id': manager.version_id,
11621166
'ignore_all': meta.ignore_all,
@@ -1186,7 +1190,7 @@ def compute_hash(text: str) -> str:
11861190
def write_cache(id: str, path: str, tree: MypyFile,
11871191
serialized_fine_grained_deps: Dict[str, List[str]],
11881192
dependencies: List[str], suppressed: List[str],
1189-
child_modules: List[str], dep_prios: List[int],
1193+
child_modules: List[str], dep_prios: List[int], dep_lines: List[int],
11901194
old_interface_hash: str, source_hash: str,
11911195
ignore_all: bool, manager: BuildManager) -> Tuple[str, Optional[CacheMeta]]:
11921196
"""Write cache files for a module.
@@ -1203,6 +1207,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
12031207
suppressed: module IDs which were suppressed as dependencies
12041208
child_modules: module IDs which are this package's direct submodules
12051209
dep_prios: priorities (parallel array to dependencies)
1210+
dep_lines: import line locations (parallel array to dependencies)
12061211
old_interface_hash: the hash from the previous version of the data cache file
12071212
source_hash: the hash of the source code
12081213
ignore_all: the ignore_all flag for this module
@@ -1286,6 +1291,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
12861291
'child_modules': child_modules,
12871292
'options': options.select_options_affecting_cache(),
12881293
'dep_prios': dep_prios,
1294+
'dep_lines': dep_lines,
12891295
'interface_hash': interface_hash,
12901296
'version_id': manager.version_id,
12911297
'ignore_all': ignore_all,
@@ -1633,8 +1639,10 @@ def __init__(self,
16331639
assert len(self.meta.dependencies) == len(self.meta.dep_prios)
16341640
self.priorities = {id: pri
16351641
for id, pri in zip(self.meta.dependencies, self.meta.dep_prios)}
1642+
assert len(self.meta.dependencies) == len(self.meta.dep_lines)
1643+
self.dep_line_map = {id: line
1644+
for id, line in zip(self.meta.dependencies, self.meta.dep_lines)}
16361645
self.child_modules = set(self.meta.child_modules)
1637-
self.dep_line_map = {}
16381646
else:
16391647
# Parse the file (and then some) to get the dependencies.
16401648
self.parse_file()
@@ -2023,11 +2031,12 @@ def write_cache(self) -> None:
20232031
self.mark_interface_stale(on_errors=True)
20242032
return
20252033
dep_prios = self.dependency_priorities()
2034+
dep_lines = self.dependency_lines()
20262035
new_interface_hash, self.meta = write_cache(
20272036
self.id, self.path, self.tree,
20282037
{k: list(v) for k, v in self.fine_grained_deps.items()},
20292038
list(self.dependencies), list(self.suppressed), list(self.child_modules),
2030-
dep_prios, self.interface_hash, self.source_hash, self.ignore_all,
2039+
dep_prios, dep_lines, self.interface_hash, self.source_hash, self.ignore_all,
20312040
self.manager)
20322041
if new_interface_hash == self.interface_hash:
20332042
self.manager.log("Cached module {} has same interface".format(self.id))
@@ -2039,6 +2048,9 @@ def write_cache(self) -> None:
20392048
def dependency_priorities(self) -> List[int]:
20402049
return [self.priorities.get(dep, PRI_HIGH) for dep in self.dependencies]
20412050

2051+
def dependency_lines(self) -> List[int]:
2052+
return [self.dep_line_map.get(dep, 1) for dep in self.dependencies]
2053+
20422054
def generate_unused_ignore_notes(self) -> None:
20432055
if self.options.warn_unused_ignores:
20442056
self.manager.errors.generate_unused_ignore_notes(self.xpath)

mypy/server/update.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,13 +562,15 @@ def preserve_full_cache(graph: Graph, manager: BuildManager) -> SavedCache:
562562
# There is no corresponding JSON so create partial "memory-only" metadata.
563563
assert state.path
564564
dep_prios = state.dependency_priorities()
565+
dep_lines = state.dependency_lines()
565566
meta = memory_only_cache_meta(
566567
id,
567568
state.path,
568569
state.dependencies,
569570
state.suppressed,
570571
list(state.child_modules),
571572
dep_prios,
573+
dep_lines,
572574
state.source_hash,
573575
state.ignore_all,
574576
manager)
@@ -584,6 +586,7 @@ def memory_only_cache_meta(id: str,
584586
suppressed: List[str],
585587
child_modules: List[str],
586588
dep_prios: List[int],
589+
dep_lines: List[int],
587590
source_hash: str,
588591
ignore_all: bool,
589592
manager: BuildManager) -> CacheMeta:
@@ -603,6 +606,7 @@ def memory_only_cache_meta(id: str,
603606
'child_modules': child_modules,
604607
'options': options.select_options_affecting_cache(),
605608
'dep_prios': dep_prios,
609+
'dep_lines': dep_lines,
606610
'interface_hash': '',
607611
'version_id': manager.version_id,
608612
'ignore_all': ignore_all,

test-data/unit/check-incremental.test

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3466,3 +3466,14 @@ tmp/main.py:2: error: Expression has type "Any"
34663466

34673467
[out2]
34683468
tmp/main.py:2: error: Expression has type "Any"
3469+
3470+
[case testDeletedDepLineNumber]
3471+
# The import is not on line 1 and that data should be preserved
3472+
import a
3473+
[file a.py]
3474+
[delete a.py.2]
3475+
[out1]
3476+
3477+
[out2]
3478+
main:2: error: Cannot find module named 'a'
3479+
main:2: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,6 @@ def g() -> None: pass
485485
==
486486
main:1: error: Cannot find module named 'a'
487487
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
488-
-- TODO: Remove redundant error message
489-
main:1: error: Cannot find module named 'b'
490488
main:2: error: Cannot find module named 'b'
491489

492490
[case testDeleteTwoFilesNoErrors]
@@ -528,8 +526,6 @@ a.py:3: error: Too many arguments for "g"
528526
==
529527
main:1: error: Cannot find module named 'a'
530528
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
531-
-- TODO: Remove redundant error message
532-
main:1: error: Cannot find module named 'b'
533529
main:2: error: Cannot find module named 'b'
534530

535531
[case testAddFileWhichImportsLibModule]

test-data/unit/fine-grained.test

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,9 +1081,8 @@ def f() -> Iterator[None]:
10811081
[out]
10821082
main:2: error: Revealed type is 'contextlib.GeneratorContextManager[builtins.None]'
10831083
==
1084-
a.py:1: error: Cannot find module named 'b'
1085-
a.py:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
10861084
a.py:3: error: Cannot find module named 'b'
1085+
a.py:3: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
10871086
main:2: error: Revealed type is 'contextlib.GeneratorContextManager[builtins.None]'
10881087
==
10891088
main:2: error: Revealed type is 'contextlib.GeneratorContextManager[builtins.None]'
@@ -1129,9 +1128,8 @@ def g() -> None:
11291128
[out]
11301129
a.py:11: error: Too many arguments for "h"
11311130
==
1132-
a.py:1: error: Cannot find module named 'b'
1133-
a.py:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
11341131
a.py:10: error: Cannot find module named 'b'
1132+
a.py:10: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
11351133
==
11361134
a.py:11: error: Too many arguments for "h"
11371135
==
@@ -1164,9 +1162,8 @@ def f(x: List[int]) -> Iterator[None]:
11641162
[builtins fixtures/list.pyi]
11651163
[out]
11661164
==
1167-
a.py:1: error: Cannot find module named 'b'
1168-
a.py:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
11691165
a.py:3: error: Cannot find module named 'b'
1166+
a.py:3: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
11701167
==
11711168
==
11721169

@@ -1223,10 +1220,8 @@ def g() -> None: pass
12231220
[delete n.py.2]
12241221
[out]
12251222
==
1226-
main:1: error: Cannot find module named 'm'
1227-
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
1228-
main:1: error: Cannot find module named 'n'
12291223
main:2: error: Cannot find module named 'm'
1224+
main:2: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
12301225
main:9: error: Cannot find module named 'n'
12311226

12321227
[case testOverloadSpecialCase]
@@ -1253,10 +1248,8 @@ def g() -> None: pass
12531248
[builtins fixtures/ops.pyi]
12541249
[out]
12551250
==
1256-
main:1: error: Cannot find module named 'm'
1257-
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
1258-
main:1: error: Cannot find module named 'n'
12591251
main:2: error: Cannot find module named 'm'
1252+
main:2: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
12601253
main:14: error: Cannot find module named 'n'
12611254

12621255
[case testRefreshGenericClass]

0 commit comments

Comments
 (0)