Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2279,13 +2284,15 @@ 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 = []
priorities = {}
dep_line_map = {}
dep_hashes = {}
error_lines = []
imports_ignored = {}

state = cls(
manager=manager,
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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),
)

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion mypy/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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],
Expand All @@ -108,6 +109,7 @@ def __init__(
self.data_mtime = data_mtime # mtime of data_file
self.data_file = data_file # path of <id>.data.json or <id>.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
Expand All @@ -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,
Expand All @@ -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"],
Expand All @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this becomes a bottleneck, we could use an integer id to represent some of most common error codes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we could potentially filter common error codes that we know won't be generated for import related errors, but this is a bit dangerous, as we'd need to ensure these ignored lines are used for cases where they work. We could rename the variable though to reduce confusion.

In any case, maye this won't be an issue -- we should probably only optimized if this appears to have some impact in real-world projects.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I will probably just do some easy optimization(s) for now (and will probably rename it to something like import_ignore_lines).

write_json(data, self.options)
write_int_list(data, self.dep_prios)
write_int_list(data, self.dep_lines)
Expand All @@ -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),
Expand Down
19 changes: 19 additions & 0 deletions test-data/unit/check-modules.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 0 additions & 3 deletions test-data/unit/pep561.test
Original file line number Diff line number Diff line change
Expand Up @@ -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