Skip to content
Closed
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
58 changes: 35 additions & 23 deletions mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,33 @@ def add_error_info(self, info: ErrorInfo) -> None:
# Check each line in this context for "type: ignore" comments.
# line == end_line for most nodes, so we only loop once.
for scope_line in lines:
if self.is_ignored_error(scope_line, info, self.ignored_lines[file]):
ignores = self.ignored_lines[file]
if info.code and not self.is_error_code_enabled(info.code):
is_ignored_error = True
record_ignored_line = False
elif scope_line not in ignores:
is_ignored_error = False
record_ignored_line = False
elif not ignores[scope_line]:
# Empty list means that we ignore all errors
is_ignored_error = True
record_ignored_line = True
elif info.code and self.is_error_code_enabled(info.code):
is_ignored_error = (
info.code.code in ignores[scope_line]
or info.code.sub_code_of is not None
and info.code.sub_code_of.code in ignores[scope_line]
)
record_ignored_line = is_ignored_error
else:
is_ignored_error = False
record_ignored_line = False

if record_ignored_line and file not in self.ignored_files:
info.hidden = True
self._add_error_info(file, info)

if is_ignored_error:
# Annotation requests us to ignore all errors on this line.
self.used_ignored_lines[file][scope_line].append(
(info.code or codes.MISC).code
Expand Down Expand Up @@ -627,25 +653,6 @@ def report_hidden_errors(self, info: ErrorInfo) -> None:
)
self._add_error_info(info.origin[0], new_info)

def is_ignored_error(self, line: int, info: ErrorInfo, ignores: dict[int, list[str]]) -> bool:
if info.blocker:
# Blocking errors can never be ignored
return False
if info.code and not self.is_error_code_enabled(info.code):
return True
if line not in ignores:
return False
if not ignores[line]:
# Empty list means that we ignore all errors
return True
if info.code and self.is_error_code_enabled(info.code):
return (
info.code.code in ignores[line]
or info.code.sub_code_of is not None
and info.code.sub_code_of.code in ignores[line]
)
return False

def is_error_code_enabled(self, error_code: ErrorCode) -> bool:
if self.options:
current_mod_disabled = self.options.disabled_error_codes
Expand All @@ -672,7 +679,7 @@ def clear_errors_in_targets(self, path: str, targets: set[str]) -> None:
if info.target not in targets:
new_errors.append(info)
has_blocker |= info.blocker
elif info.only_once:
elif info.only_once and not info.hidden:
self.only_once_messages.remove(info.message)
self.error_info_map[path] = new_errors
if not has_blocker and path in self.has_blockers:
Expand Down Expand Up @@ -725,6 +732,8 @@ def generate_unused_ignore_errors(self, file: str) -> None:
blocker=False,
only_once=False,
allow_dups=False,
origin=(self.file, [line]),
target=self.target_module,
)
self._add_error_info(file, info)

Expand Down Expand Up @@ -777,6 +786,8 @@ def generate_ignore_without_code_errors(
blocker=False,
only_once=False,
allow_dups=False,
origin=(self.file, [line]),
target=self.target_module,
)
self._add_error_info(file, info)

Expand All @@ -801,8 +812,9 @@ def blocker_module(self) -> str | None:
return None

def is_errors_for_file(self, file: str) -> bool:
"""Are there any errors for the given file?"""
return file in self.error_info_map
"""Are there any visible errors for the given file?"""
errors = self.error_info_map.get(file, ())
return any(error.hidden is False for error in errors)

def prefer_simple_messages(self) -> bool:
"""Should we generate simple/fast error messages?
Expand Down
6 changes: 6 additions & 0 deletions mypy/server/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,8 @@ def restore(ids: list[str]) -> None:
state.type_check_first_pass()
state.type_check_second_pass()
state.detect_possibly_undefined_vars()
state.generate_unused_ignore_notes()
state.generate_ignore_without_code_notes()
t2 = time.time()
state.finish_passes()
t3 = time.time()
Expand Down Expand Up @@ -1028,6 +1030,10 @@ def key(node: FineGrainedDeferredNode) -> int:
if graph[module_id].type_checker().check_second_pass():
more = True

graph[module_id].detect_possibly_undefined_vars()
graph[module_id].generate_unused_ignore_notes()
graph[module_id].generate_ignore_without_code_notes()

if manager.options.export_types:
manager.all_types.update(graph[module_id].type_map())

Expand Down
74 changes: 74 additions & 0 deletions test-data/unit/daemon.test
Original file line number Diff line number Diff line change
Expand Up @@ -615,3 +615,77 @@ b: str
from demo.test import a
[file demo/test.py]
a: int

[case testUnusedTypeIgnorePreservedOnRerun]
-- Regression test for https://github.com/python/mypy/issues/9655
$ dmypy start -- --warn-unused-ignores --no-error-summary --hide-error-codes
Daemon started
$ dmypy check -- bar.py
bar.py:2: error: Unused "type: ignore" comment
== Return code: 1
$ dmypy check -- bar.py
bar.py:2: error: Unused "type: ignore" comment
== Return code: 1

[file foo/__init__.py]
[file foo/empty.py]
[file bar.py]
from foo.empty import *
a = 1 # type: ignore

[case testTypeIgnoreWithoutCodePreservedOnRerun]
-- Regression test for https://github.com/python/mypy/issues/9655
$ dmypy start -- --enable-error-code ignore-without-code --no-error-summary
Daemon started
$ dmypy check -- bar.py
bar.py:2: error: "type: ignore" comment without error code [ignore-without-code]
== Return code: 1
$ dmypy check -- bar.py
bar.py:2: error: "type: ignore" comment without error code [ignore-without-code]
== Return code: 1

[file foo/__init__.py]
[file foo/empty.py]
[file bar.py]
from foo.empty import *
a = 1 # type: ignore

[case testPossiblyUndefinedVarsPreservedAfterUpdate]
-- Regression test for https://github.com/python/mypy/issues/9655
$ dmypy start -- --enable-error-code possibly-undefined --no-error-summary
Daemon started
$ dmypy check -- bar.py
bar.py:4: error: Name "a" may be undefined [possibly-undefined]
== Return code: 1
$ dmypy check -- bar.py
bar.py:4: error: Name "a" may be undefined [possibly-undefined]
== Return code: 1

[file foo/__init__.py]
[file foo/empty.py]
[file bar.py]
from foo.empty import *
if False:
a = 1
a

[case testReturnTypeIgnoreAfterUnknownImport]
-- Return type ignores after unknown imports and unused modules are respected on the second pass.
$ dmypy start -- --warn-unused-ignores --no-error-summary
Daemon started
$ dmypy check -- foo.py
foo.py:2: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found]
foo.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
== Return code: 1
$ dmypy check -- foo.py
foo.py:2: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found]
foo.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
== Return code: 1

[file unused/__init__.py]
[file unused/empty.py]
[file foo.py]
from unused.empty import *
import a_module_which_does_not_exist
def is_foo() -> str:
return True # type: ignore
60 changes: 60 additions & 0 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -10505,3 +10505,63 @@ from pkg.sub import modb

[out]
==

[case testUnusedTypeIgnorePreservedAfterChange]
# flags: --warn-unused-ignores --no-error-summary
[file main.py]
a = 1 # type: ignore
[file main.py.2]
a = 1 # type: ignore
# Comment to trigger reload.
[out]
main.py:1: error: Unused "type: ignore" comment
==
main.py:1: error: Unused "type: ignore" comment

[case testTypeIgnoreWithoutCodePreservedAfterChange]
# flags: --enable-error-code ignore-without-code --no-error-summary
[file main.py]
a = 1 # type: ignore
[file main.py.2]
a = 1 # type: ignore
# Comment to trigger reload.
[out]
main.py:1: error: "type: ignore" comment without error code
==
main.py:1: error: "type: ignore" comment without error code


[case testUnusedTypeIgnorePreservedOnRerunWithIgnoredMissingImports]
# flags: --no-error-summary --ignore-missing-imports --warn-unused-ignores
import foo
[file foo/__init__.py]
from bar import *
[file foo/__init__.py.2]
from bar import *
# Comment to trigger reload.
[file foo/empty.py]
[file bar.py]
from foo import empty
a = 1 # type: ignore
[out]
bar.py:2: error: Unused "type: ignore" comment
==
bar.py:2: error: Unused "type: ignore" comment


[case testModuleDoesNotExistPreservedOnRerun]
# flags: --no-error-summary --ignore-missing-imports
import foo
[file foo/__init__.py]
from bar import *
[file bar.py]
from foo import does_not_exist
from unused.submodule import *
[file bar.py.2]
from foo import does_not_exist
from unused.submodule import *
# Comment to trigger reload.
[out]
bar.py:1: error: Module "foo" has no attribute "does_not_exist" [attr-defined]
==
bar.py:1: error: Module "foo" has no attribute "does_not_exist" [attr-defined]