Skip to content

Commit 706e19e

Browse files
committed
Store line numbers of imports in the cache metadata
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 706e19e

File tree

5 files changed

+38
-21
lines changed

5 files changed

+38
-21
lines changed

mypy/build.py

Lines changed: 17 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,11 @@ 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)}
1636-
self.child_modules = set(self.meta.child_modules)
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)}
16371645
self.dep_line_map = {}
1646+
self.child_modules = set(self.meta.child_modules)
16381647
else:
16391648
# Parse the file (and then some) to get the dependencies.
16401649
self.parse_file()
@@ -2023,11 +2032,12 @@ def write_cache(self) -> None:
20232032
self.mark_interface_stale(on_errors=True)
20242033
return
20252034
dep_prios = self.dependency_priorities()
2035+
dep_lines = self.dependency_lines()
20262036
new_interface_hash, self.meta = write_cache(
20272037
self.id, self.path, self.tree,
20282038
{k: list(v) for k, v in self.fine_grained_deps.items()},
20292039
list(self.dependencies), list(self.suppressed), list(self.child_modules),
2030-
dep_prios, self.interface_hash, self.source_hash, self.ignore_all,
2040+
dep_prios, dep_lines, self.interface_hash, self.source_hash, self.ignore_all,
20312041
self.manager)
20322042
if new_interface_hash == self.interface_hash:
20332043
self.manager.log("Cached module {} has same interface".format(self.id))
@@ -2039,6 +2049,9 @@ def write_cache(self) -> None:
20392049
def dependency_priorities(self) -> List[int]:
20402050
return [self.priorities.get(dep, PRI_HIGH) for dep in self.dependencies]
20412051

2052+
def dependency_lines(self) -> List[int]:
2053+
return [self.dep_line_map.get(dep, 1) for dep in self.dependencies]
2054+
20422055
def generate_unused_ignore_notes(self) -> None:
20432056
if self.options.warn_unused_ignores:
20442057
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: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,8 @@ from p import q
242242
[file p/q.py.3]
243243
[out]
244244
==
245-
main:1: error: Cannot find module named 'p.q'
246245
-- TODO: The following messages are different compared to non-incremental mode
246+
main:1: error: Cannot find module named 'p.q'
247247
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
248248
main:1: error: Module 'p' has no attribute 'q'
249249
==
@@ -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)