diff --git a/mypy/build.py b/mypy/build.py index d9692e6001e6..6ed2385dbacf 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -1835,6 +1835,7 @@ def write_cache( tree: MypyFile, dependencies: list[str], suppressed: list[str], + imports_ignored: dict[int, list[str]], dep_prios: list[int], dep_lines: list[int], old_interface_hash: bytes, @@ -1949,6 +1950,7 @@ def write_cache( data_mtime=data_mtime, data_file=data_file, suppressed=suppressed, + imports_ignored=imports_ignored, options=options_snapshot(id, manager), dep_prios=dep_prios, dep_lines=dep_lines, @@ -2198,6 +2200,9 @@ class State: # we use file size as a proxy for complexity. size_hint: int + # Mapping from line number to type ignore codes on this line (for imports only). + imports_ignored: dict[int, list[str]] + @classmethod def new_state( cls, @@ -2279,6 +2284,7 @@ def new_state( dep_hashes = {k: v for (k, v) in zip(meta.dependencies, meta.dep_hashes)} # Only copy `error_lines` if the module is not silently imported. error_lines = [] if ignore_all else meta.error_lines + imports_ignored = meta.imports_ignored else: dependencies = [] suppressed = [] @@ -2286,6 +2292,7 @@ def new_state( dep_line_map = {} dep_hashes = {} error_lines = [] + imports_ignored = {} state = cls( manager=manager, @@ -2306,6 +2313,7 @@ def new_state( dep_line_map=dep_line_map, dep_hashes=dep_hashes, error_lines=error_lines, + imports_ignored=imports_ignored, ) if meta: @@ -2363,6 +2371,7 @@ def __init__( dep_line_map: dict[str, int], dep_hashes: dict[str, bytes], error_lines: list[SerializedError], + imports_ignored: dict[int, list[str]], size_hint: int = 0, ) -> None: self.manager = manager @@ -2392,6 +2401,7 @@ def __init__( self.early_errors = [] self._type_checker = None self.add_ancestors() + self.imports_ignored = imports_ignored self.size_hint = size_hint def write(self, buf: WriteBuffer) -> None: @@ -2478,6 +2488,7 @@ def read(cls, buf: ReadBuffer, manager: BuildManager) -> State: dep_line_map=dep_line_map, dep_hashes=dep_hashes, error_lines=[], + imports_ignored={}, size_hint=read_int(buf), ) @@ -2805,6 +2816,10 @@ def compute_dependencies(self) -> None: self.add_dependency(id) if id not in self.dep_line_map: self.dep_line_map[id] = line + import_lines = self.dep_line_map.values() + self.imports_ignored = { + line: codes for line, codes in self.tree.ignored_lines.items() if line in import_lines + } # Every module implicitly depends on builtins. if self.id != "builtins": self.add_dependency("builtins") @@ -2991,6 +3006,7 @@ def write_cache(self) -> tuple[CacheMeta, str] | None: self.tree, list(self.dependencies), list(self.suppressed), + self.imports_ignored, dep_prios, dep_lines, self.interface_hash, @@ -3260,6 +3276,11 @@ def module_not_found( save_import_context = errors.import_context() errors.set_import_context(caller_state.import_context) errors.set_file(caller_state.xpath, caller_state.id, caller_state.options) + errors.set_file_ignored_lines( + caller_state.xpath, + caller_state.tree.ignored_lines if caller_state.tree else caller_state.imports_ignored, + caller_state.ignore_all or caller_state.options.ignore_errors, + ) if target == "builtins": errors.report( line, 0, "Cannot find 'builtins' module. Typeshed appears broken!", blocker=True diff --git a/mypy/cache.py b/mypy/cache.py index 81915821e19e..ec3315282525 100644 --- a/mypy/cache.py +++ b/mypy/cache.py @@ -69,7 +69,7 @@ from mypy_extensions import u8 # High-level cache layout format -CACHE_VERSION: Final = 1 +CACHE_VERSION: Final = 2 SerializedError: _TypeAlias = tuple[str | None, int | str, int, int, int, str, str, str | None] @@ -89,6 +89,7 @@ def __init__( data_mtime: int, data_file: str, suppressed: list[str], + imports_ignored: dict[int, list[str]], options: dict[str, object], dep_prios: list[int], dep_lines: list[int], @@ -108,6 +109,7 @@ def __init__( self.data_mtime = data_mtime # mtime of data_file self.data_file = data_file # path of .data.json or .data.ff self.suppressed = suppressed # dependencies that weren't imported + self.imports_ignored = imports_ignored # type ignore codes by line self.options = options # build options snapshot # dep_prios and dep_lines are both aligned with dependencies + suppressed self.dep_prios = dep_prios @@ -130,6 +132,7 @@ def serialize(self) -> dict[str, Any]: "data_mtime": self.data_mtime, "dependencies": self.dependencies, "suppressed": self.suppressed, + "imports_ignored": {str(line): codes for line, codes in self.imports_ignored.items()}, "options": self.options, "dep_prios": self.dep_prios, "dep_lines": self.dep_lines, @@ -154,6 +157,9 @@ def deserialize(cls, meta: dict[str, Any], data_file: str) -> CacheMeta | None: data_mtime=meta["data_mtime"], data_file=data_file, suppressed=meta["suppressed"], + imports_ignored={ + int(line): codes for line, codes in meta["imports_ignored"].items() + }, options=meta["options"], dep_prios=meta["dep_prios"], dep_lines=meta["dep_lines"], @@ -176,6 +182,10 @@ def write(self, data: WriteBuffer) -> None: write_str_list(data, self.dependencies) write_int(data, self.data_mtime) write_str_list(data, self.suppressed) + write_int_bare(data, len(self.imports_ignored)) + for line, codes in self.imports_ignored.items(): + write_int(data, line) + write_str_list(data, codes) write_json(data, self.options) write_int_list(data, self.dep_prios) write_int_list(data, self.dep_lines) @@ -201,6 +211,9 @@ def read(cls, data: ReadBuffer, data_file: str) -> CacheMeta | None: data_mtime=read_int(data), data_file=data_file, suppressed=read_str_list(data), + imports_ignored={ + read_int(data): read_str_list(data) for _ in range(read_int_bare(data)) + }, options=read_json(data), dep_prios=read_int_list(data), dep_lines=read_int_list(data), diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index ce10b88a91e0..4552b86c82a7 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -3243,3 +3243,22 @@ tmp/b.py:3: note: Superclass: tmp/b.py:3: note: def frobnicate(self, x: str, *args: Any, **kwargs: Any) -> Any tmp/b.py:3: note: Subclass: tmp/b.py:3: note: def frobnicate(self, *args: int) -> None + +[case testTypeIgnoredImportsWorkWithCacheIncremental] +import a +[file a.py] +import b # type: ignore[import] +[file b.py] +[delete b.py.2] +[out] +[out2] + +[case testTypeIgnoredImportsWorkWithCacheIncremental2] +# flags: --warn-unused-ignores +import a +[file a.py] +import b # type: ignore[import-not-found] +[file b.py.2] +[out] +[out2] +tmp/a.py:1: error: Unused "type: ignore" comment diff --git a/test-data/unit/pep561.test b/test-data/unit/pep561.test index 71a3a46b4de0..fe791a16d555 100644 --- a/test-data/unit/pep561.test +++ b/test-data/unit/pep561.test @@ -246,8 +246,5 @@ import m1 import typedpkg_ns.b # type: ignore [file m2.py] import typedpkg_ns.a -# TODO: the error on second run is a bug, but it is a separate issue, likely #20105. [out] [out2] -m1.py:1: error: Skipping analyzing "typedpkg_ns": module is installed, but missing library stubs or py.typed marker -m1.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports