Skip to content
Open
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
181 changes: 82 additions & 99 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import types
from collections.abc import Callable, Iterator, Mapping, Sequence, Set as AbstractSet
from heapq import heappop, heappush
from textwrap import dedent
from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -61,9 +62,9 @@
LIST_GEN,
LITERAL_NONE,
CacheMeta,
ErrorTuple,
JsonValue,
ReadBuffer,
SerializedError,
Tag,
WriteBuffer,
read_bytes,
Expand All @@ -90,7 +91,8 @@
WORKER_START_TIMEOUT,
)
from mypy.error_formatter import OUTPUT_CHOICES, ErrorFormatter
from mypy.errors import CompileError, ErrorInfo, Errors, ErrorTuple, report_internal_error
from mypy.errorcodes import ErrorCode
from mypy.errors import CompileError, ErrorInfo, Errors, report_internal_error
from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort
from mypy.indirection import TypeIndirectionVisitor
from mypy.ipc import (
Expand Down Expand Up @@ -177,14 +179,18 @@
}

# We are careful now, we can increase this in future if safe/useful.
MAX_GC_FREEZE_CYCLES = 1
MAX_GC_FREEZE_CYCLES: Final = 1

# We store status of initial GC freeze as a global variable to avoid memory
# leaks in tests, where we keep creating new BuildManagers in the same process.
initial_gc_freeze_done = False

Graph: _TypeAlias = dict[str, "State"]

MODULE_RESOLUTION_URL: Final = (
"https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-file-paths-to-modules"
)


class SCC:
"""A simple class that represents a strongly connected component (import cycle)."""
Expand Down Expand Up @@ -953,8 +959,8 @@ def correct_rel_imp(self, file: MypyFile, imp: ImportFrom | ImportAll) -> str:

if not new_id:
self.errors.set_file(file.path, file.name, self.options)
self.errors.report(
imp.line, 0, "No parent module -- cannot perform relative import", blocker=True
self.error(
imp.line, "No parent module -- cannot perform relative import", blocker=True
)

return new_id
Expand Down Expand Up @@ -1183,6 +1189,36 @@ def is_transitive_scc_dep(self, from_scc_id: int, to_scc_id: int) -> bool:
self.transitive_deps_cache[(dep, to_scc_id)] = False
return False

def error(
self,
line: int | None,
msg: str,
code: ErrorCode | None = None,
*,
blocker: bool = False,
only_once: bool = False,
) -> None:
if line is None:
line = column = -1
else:
column = 0
self.errors.report(line, column, msg, code, blocker=blocker, only_once=only_once)

def note(
self, line: int | None, msg: str, code: ErrorCode | None = None, *, only_once: bool = False
) -> None:
if line is None:
line = column = -1
else:
column = 0
self.errors.report(line, column, msg, code, severity="note", only_once=only_once)

def note_multiline(
self, line: int | None, msg: str, code: ErrorCode | None = None, *, only_once: bool = False
) -> None:
for msg_line in dedent(msg.lstrip("\n")).splitlines():
self.note(line, msg_line, code, only_once=only_once)


def deps_to_json(x: dict[str, set[str]]) -> bytes:
return json_dumps({k: list(v) for k, v in x.items()})
Expand Down Expand Up @@ -1261,7 +1297,7 @@ def write_deps_cache(

if error:
manager.errors.set_file(_cache_dir_prefix(manager.options), None, manager.options)
manager.errors.report(0, 0, "Error writing fine-grained dependencies cache", blocker=True)
manager.error(None, "Error writing fine-grained dependencies cache", blocker=True)


def invert_deps(deps: dict[str, set[str]], graph: Graph) -> dict[str, dict[str, set[str]]]:
Expand Down Expand Up @@ -1329,7 +1365,7 @@ def write_plugins_snapshot(manager: BuildManager) -> None:
and manager.options.cache_dir != os.devnull
):
manager.errors.set_file(_cache_dir_prefix(manager.options), None, manager.options)
manager.errors.report(0, 0, "Error writing plugins snapshot", blocker=True)
manager.error(None, "Error writing plugins snapshot", blocker=True)


def read_plugins_snapshot(manager: BuildManager) -> dict[str, str] | None:
Expand Down Expand Up @@ -1444,9 +1480,8 @@ def _load_json_file(
manager.add_stats(data_file_load_time=time.time() - t1)
except json.JSONDecodeError:
manager.errors.set_file(file, None, manager.options)
manager.errors.report(
-1,
-1,
manager.error(
None,
"Error reading JSON file;"
" you likely have a bad cache.\n"
"Try removing the {cache_dir} directory"
Expand Down Expand Up @@ -2151,7 +2186,7 @@ class State:
dep_hashes: dict[str, bytes]

# List of errors reported for this file last time.
error_lines: list[SerializedError]
error_lines: list[ErrorTuple]

# Parent package, its parent, etc.
ancestors: list[str] | None = None
Expand Down Expand Up @@ -2380,7 +2415,7 @@ def __init__(
priorities: dict[str, int],
dep_line_map: dict[str, int],
dep_hashes: dict[str, bytes],
error_lines: list[SerializedError],
error_lines: list[ErrorTuple],
imports_ignored: dict[int, list[str]],
size_hint: int = 0,
) -> None:
Expand Down Expand Up @@ -2748,13 +2783,13 @@ def parse_inline_configuration(self, source: str) -> None:
self.options = self.options.apply_changes(changes)
self.manager.errors.set_file(self.xpath, self.id, self.options)
for lineno, error in config_errors:
self.manager.errors.report(lineno, 0, error)
self.manager.error(lineno, error)

def check_for_invalid_options(self) -> None:
if self.options.mypyc and not self.options.strict_bytes:
self.manager.errors.set_file(self.xpath, self.id, options=self.options)
self.manager.errors.report(
1, 0, "Option --strict-bytes cannot be disabled when using mypyc", blocker=True
self.manager.error(
None, "Option --strict-bytes cannot be disabled when using mypyc", blocker=True
)

def semantic_analysis_pass1(self) -> None:
Expand Down Expand Up @@ -3346,8 +3381,8 @@ def module_not_found(
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
manager.error(
line, "Cannot find 'builtins' module. Typeshed appears broken!", blocker=True
)
errors.raise_error()
else:
Expand All @@ -3362,14 +3397,14 @@ def module_not_found(
code = codes.IMPORT_UNTYPED
else:
code = codes.IMPORT
errors.report(line, 0, msg.format(module=target), code=code)
manager.error(line, msg.format(module=target), code=code)

dist = stub_distribution_name(target)
for note in notes:
if "{stub_dist}" in note:
assert dist is not None
note = note.format(stub_dist=dist)
errors.report(line, 0, note, severity="note", only_once=True, code=code)
manager.note(line, note, only_once=True, code=code)
if reason is ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED:
assert dist is not None
manager.missing_stub_packages.add(dist)
Expand All @@ -3384,13 +3419,9 @@ def skipping_module(
save_import_context = manager.errors.import_context()
manager.errors.set_import_context(caller_state.import_context)
manager.errors.set_file(caller_state.xpath, caller_state.id, manager.options)
manager.errors.report(line, 0, f'Import of "{id}" ignored', severity="error")
manager.errors.report(
line,
0,
"(Using --follow-imports=error, module not passed on command line)",
severity="note",
only_once=True,
manager.error(line, f'Import of "{id}" ignored')
manager.note(
line, "(Using --follow-imports=error, module not passed on command line)", only_once=True
)
manager.errors.set_import_context(save_import_context)

Expand All @@ -3403,15 +3434,9 @@ def skipping_ancestor(manager: BuildManager, id: str, path: str, ancestor_for: S
# so we'd need to cache the decision.
manager.errors.set_import_context([])
manager.errors.set_file(ancestor_for.xpath, ancestor_for.id, manager.options)
manager.errors.report(
-1, -1, f'Ancestor package "{id}" ignored', severity="error", only_once=True
)
manager.errors.report(
-1,
-1,
"(Using --follow-imports=error, submodule passed on command line)",
severity="note",
only_once=True,
manager.error(None, f'Ancestor package "{id}" ignored', only_once=True)
manager.note(
None, "(Using --follow-imports=error, submodule passed on command line)", only_once=True
)


Expand Down Expand Up @@ -3680,28 +3705,19 @@ def load_graph(
continue
if st.id in graph:
manager.errors.set_file(st.xpath, st.id, manager.options)
manager.errors.report(
-1,
-1,
manager.error(
None,
f'Duplicate module named "{st.id}" (also at "{graph[st.id].xpath}")',
blocker=True,
)
manager.errors.report(
-1,
-1,
"See https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-file-paths-to-modules "
"for more info",
severity="note",
)
manager.errors.report(
-1,
-1,
"Common resolutions include: a) using `--exclude` to avoid checking one of them, "
"b) adding `__init__.py` somewhere, c) using `--explicit-package-bases` or "
"adjusting MYPYPATH",
severity="note",
)

resolution_note = f"""
See {MODULE_RESOLUTION_URL} for more info
Common resolutions include:
a) using `--exclude` to avoid checking one of them,
b) adding `__init__.py` somewhere,
c) using `--explicit-package-bases` or adjusting `MYPYPATH`
"""
manager.note_multiline(None, resolution_note)
manager.errors.raise_error()
graph[st.id] = st
new.append(st)
Expand Down Expand Up @@ -3769,27 +3785,19 @@ def load_graph(
newst_path = newst.abspath

if newst_path in seen_files:
manager.errors.report(
-1,
0,
manager.error(
None,
"Source file found twice under different module names: "
'"{}" and "{}"'.format(seen_files[newst_path].id, newst.id),
f'"{seen_files[newst_path].id}" and "{newst.id}"',
blocker=True,
)
manager.errors.report(
-1,
0,
"See https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-file-paths-to-modules "
"for more info",
severity="note",
)
manager.errors.report(
-1,
0,
"Common resolutions include: a) adding `__init__.py` somewhere, "
"b) using `--explicit-package-bases` or adjusting MYPYPATH",
severity="note",
)
resolution_note = f"""
See {MODULE_RESOLUTION_URL} for more info
Common resolutions include:
a) adding `__init__.py` somewhere,
b) using `--explicit-package-bases` or adjusting `MYPYPATH`
"""
manager.note_multiline(None, resolution_note)
manager.errors.raise_error()

seen_files[newst_path] = newst
Expand Down Expand Up @@ -3923,9 +3931,7 @@ def find_stale_sccs(
if graph[id].error_lines:
path = manager.errors.simplify_path(graph[id].xpath)
formatted = manager.errors.format_messages(
path,
deserialize_codes(graph[id].error_lines),
formatter=manager.error_formatter,
path, graph[id].error_lines, formatter=manager.error_formatter
)
manager.flush_errors(path, formatted, False)
fresh_sccs.append(ascc)
Expand Down Expand Up @@ -4206,7 +4212,7 @@ def process_stale_scc(
continue
meta, meta_file = meta_tuple
meta.dep_hashes = [graph[dep].interface_hash for dep in graph[id].dependencies]
meta.error_lines = serialize_codes(errors_by_id.get(id, []))
meta.error_lines = errors_by_id.get(id, [])
write_cache_meta(meta, manager, meta_file)
manager.done_sccs.add(ascc.id)
manager.add_stats(
Expand Down Expand Up @@ -4376,29 +4382,6 @@ def write_undocumented_ref_info(
metastore.write(ref_info_file, json_dumps(deps_json))


def serialize_codes(errs: list[ErrorTuple]) -> list[SerializedError]:
return [
(path, line, column, end_line, end_column, severity, message, code.code if code else None)
for path, line, column, end_line, end_column, severity, message, code in errs
]


def deserialize_codes(errs: list[SerializedError]) -> list[ErrorTuple]:
return [
(
path,
line,
column,
end_line,
end_column,
severity,
message,
codes.error_codes.get(code) if code else None,
)
for path, line, column, end_line, end_column, severity, message, code in errs
]


# The IPC message classes and tags for communication with build workers are
# in this file to avoid import cycles.
# Note that we use a more compact fixed serialization format than in cache.py.
Expand Down
10 changes: 6 additions & 4 deletions mypy/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@
# High-level cache layout format
CACHE_VERSION: Final = 5

SerializedError: _TypeAlias = tuple[str | None, int, int, int, int, str, str, str | None]
# Type used internally to represent errors:
# (path, line, column, end_line, end_column, severity, message, code)
ErrorTuple: _TypeAlias = tuple[str | None, int, int, int, int, str, str, str | None]


class CacheMeta:
Expand All @@ -97,7 +99,7 @@ def __init__(
dep_hashes: list[bytes],
interface_hash: bytes,
trans_dep_hash: bytes,
error_lines: list[SerializedError],
error_lines: list[ErrorTuple],
version_id: str,
ignore_all: bool,
plugin_data: Any,
Expand Down Expand Up @@ -498,7 +500,7 @@ def write_json(data: WriteBuffer, value: dict[str, Any]) -> None:
write_json_value(data, value[key])


def write_errors(data: WriteBuffer, errs: list[SerializedError]) -> None:
def write_errors(data: WriteBuffer, errs: list[ErrorTuple]) -> None:
write_tag(data, LIST_GEN)
write_int_bare(data, len(errs))
for path, line, column, end_line, end_column, severity, message, code in errs:
Expand All @@ -513,7 +515,7 @@ def write_errors(data: WriteBuffer, errs: list[SerializedError]) -> None:
write_str_opt(data, code)


def read_errors(data: ReadBuffer) -> list[SerializedError]:
def read_errors(data: ReadBuffer) -> list[ErrorTuple]:
assert read_tag(data) == LIST_GEN
result = []
for _ in range(read_int_bare(data)):
Expand Down
2 changes: 1 addition & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2601,7 +2601,7 @@ def erase_override(t: Type) -> Type:
original_arg_type,
supertype,
context,
secondary_context=node,
origin_context=node,
)
emitted_msg = True

Expand Down
Loading