Skip to content

Commit 0755a61

Browse files
meshyseddonymhauntsaninjailevkivskyipre-commit-ci[bot]
authored
Type ignore comments erroneously marked as unused by dmypy (#15043)
There is currently a misbehaviour where "type: ignore" comments are erroneously marked as unused in re-runs of dmypy. There are also cases where errors disappear on the re-run. As far as I can tell, this only happens in modules which contain an import that we don't know how to type (such as a module which does not exist), and a submodule which is unused. There was a lot of commenting and investigation on this PR, but I hope that the committed tests and fixes illustrate and address the issue. Related to #9655 --------- Co-authored-by: David Seddon <david@seddonym.me> Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Co-authored-by: Ivan Levkivskyi <levkivskyi@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 0c26253 commit 0755a61

File tree

4 files changed

+167
-0
lines changed

4 files changed

+167
-0
lines changed

mypy/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,8 @@ def generate_unused_ignore_errors(self, file: str) -> None:
848848
code=codes.UNUSED_IGNORE,
849849
blocker=False,
850850
only_once=False,
851+
origin=(self.file, [line]),
852+
target=self.target_module,
851853
)
852854
self._add_error_info(file, info)
853855

@@ -899,6 +901,8 @@ def generate_ignore_without_code_errors(
899901
code=codes.IGNORE_WITHOUT_CODE,
900902
blocker=False,
901903
only_once=False,
904+
origin=(self.file, [line]),
905+
target=self.target_module,
902906
)
903907
self._add_error_info(file, info)
904908

mypy/server/update.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,8 @@ def restore(ids: list[str]) -> None:
668668
state.type_check_first_pass()
669669
state.type_check_second_pass()
670670
state.detect_possibly_undefined_vars()
671+
state.generate_unused_ignore_notes()
672+
state.generate_ignore_without_code_notes()
671673
t2 = time.time()
672674
state.finish_passes()
673675
t3 = time.time()

test-data/unit/daemon.test

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,143 @@ from demo.test import a
648648
[file demo/test.py]
649649
a: int
650650

651+
[case testUnusedTypeIgnorePreservedOnRerun]
652+
-- Regression test for https://github.com/python/mypy/issues/9655
653+
$ dmypy start -- --warn-unused-ignores --no-error-summary --hide-error-codes
654+
Daemon started
655+
$ dmypy check -- bar.py
656+
bar.py:2: error: Unused "type: ignore" comment
657+
== Return code: 1
658+
$ dmypy check -- bar.py
659+
bar.py:2: error: Unused "type: ignore" comment
660+
== Return code: 1
661+
662+
[file foo/__init__.py]
663+
[file foo/empty.py]
664+
[file bar.py]
665+
from foo.empty import *
666+
a = 1 # type: ignore
667+
668+
[case testTypeIgnoreWithoutCodePreservedOnRerun]
669+
-- Regression test for https://github.com/python/mypy/issues/9655
670+
$ dmypy start -- --enable-error-code ignore-without-code --no-error-summary
671+
Daemon started
672+
$ dmypy check -- bar.py
673+
bar.py:2: error: "type: ignore" comment without error code [ignore-without-code]
674+
== Return code: 1
675+
$ dmypy check -- bar.py
676+
bar.py:2: error: "type: ignore" comment without error code [ignore-without-code]
677+
== Return code: 1
678+
679+
[file foo/__init__.py]
680+
[file foo/empty.py]
681+
[file bar.py]
682+
from foo.empty import *
683+
a = 1 # type: ignore
684+
685+
[case testPossiblyUndefinedVarsPreservedAfterRerun]
686+
-- Regression test for https://github.com/python/mypy/issues/9655
687+
$ dmypy start -- --enable-error-code possibly-undefined --no-error-summary
688+
Daemon started
689+
$ dmypy check -- bar.py
690+
bar.py:4: error: Name "a" may be undefined [possibly-undefined]
691+
== Return code: 1
692+
$ dmypy check -- bar.py
693+
bar.py:4: error: Name "a" may be undefined [possibly-undefined]
694+
== Return code: 1
695+
696+
[file foo/__init__.py]
697+
[file foo/empty.py]
698+
[file bar.py]
699+
from foo.empty import *
700+
if False:
701+
a = 1
702+
a
703+
704+
[case testUnusedTypeIgnorePreservedOnRerunWithIgnoredMissingImports]
705+
$ dmypy start -- --no-error-summary --ignore-missing-imports --warn-unused-ignores
706+
Daemon started
707+
$ dmypy check foo
708+
foo/main.py:3: error: Unused "type: ignore" comment [unused-ignore]
709+
== Return code: 1
710+
$ dmypy check foo
711+
foo/main.py:3: error: Unused "type: ignore" comment [unused-ignore]
712+
== Return code: 1
713+
714+
[file unused/__init__.py]
715+
[file unused/submodule.py]
716+
[file foo/empty.py]
717+
[file foo/__init__.py]
718+
from foo.main import *
719+
from unused.submodule import *
720+
[file foo/main.py]
721+
from foo import empty
722+
from foo.does_not_exist import *
723+
a = 1 # type: ignore
724+
725+
[case testModuleDoesNotExistPreservedOnRerun]
726+
$ dmypy start -- --no-error-summary --ignore-missing-imports
727+
Daemon started
728+
$ dmypy check foo
729+
foo/main.py:1: error: Module "foo" has no attribute "does_not_exist" [attr-defined]
730+
== Return code: 1
731+
$ dmypy check foo
732+
foo/main.py:1: error: Module "foo" has no attribute "does_not_exist" [attr-defined]
733+
== Return code: 1
734+
735+
[file unused/__init__.py]
736+
[file unused/submodule.py]
737+
[file foo/__init__.py]
738+
from foo.main import *
739+
[file foo/main.py]
740+
from foo import does_not_exist
741+
from unused.submodule import *
742+
743+
[case testReturnTypeIgnoreAfterUnknownImport]
744+
-- Return type ignores after unknown imports and unused modules are respected on the second pass.
745+
$ dmypy start -- --warn-unused-ignores --no-error-summary
746+
Daemon started
747+
$ dmypy check -- foo.py
748+
foo.py:2: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found]
749+
foo.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
750+
== Return code: 1
751+
$ dmypy check -- foo.py
752+
foo.py:2: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found]
753+
foo.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
754+
== Return code: 1
755+
756+
[file unused/__init__.py]
757+
[file unused/empty.py]
758+
[file foo.py]
759+
from unused.empty import *
760+
import a_module_which_does_not_exist
761+
def is_foo() -> str:
762+
return True # type: ignore
763+
764+
[case testAttrsTypeIgnoreAfterUnknownImport]
765+
$ dmypy start -- --warn-unused-ignores --no-error-summary
766+
Daemon started
767+
$ dmypy check -- foo.py
768+
foo.py:3: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found]
769+
foo.py:3: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
770+
== Return code: 1
771+
$ dmypy check -- foo.py
772+
foo.py:3: error: Cannot find implementation or library stub for module named "a_module_which_does_not_exist" [import-not-found]
773+
foo.py:3: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
774+
== Return code: 1
775+
776+
[file unused/__init__.py]
777+
[file unused/empty.py]
778+
[file foo.py]
779+
import attr
780+
from unused.empty import *
781+
import a_module_which_does_not_exist
782+
783+
@attr.frozen
784+
class A:
785+
def __init__(self) -> None:
786+
self.__attrs_init__() # type: ignore[attr-defined]
787+
651788
[case testDaemonImportAncestors]
652789
$ dmypy run test.py
653790
Daemon started

test-data/unit/fine-grained.test

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10540,6 +10540,30 @@ from pkg.sub import modb
1054010540
[out]
1054110541
==
1054210542

10543+
[case testUnusedTypeIgnorePreservedAfterChange]
10544+
# flags: --warn-unused-ignores --no-error-summary
10545+
[file main.py]
10546+
a = 1 # type: ignore
10547+
[file main.py.2]
10548+
a = 1 # type: ignore
10549+
# Comment to trigger reload.
10550+
[out]
10551+
main.py:1: error: Unused "type: ignore" comment
10552+
==
10553+
main.py:1: error: Unused "type: ignore" comment
10554+
10555+
[case testTypeIgnoreWithoutCodePreservedAfterChange]
10556+
# flags: --enable-error-code ignore-without-code --no-error-summary
10557+
[file main.py]
10558+
a = 1 # type: ignore
10559+
[file main.py.2]
10560+
a = 1 # type: ignore
10561+
# Comment to trigger reload.
10562+
[out]
10563+
main.py:1: error: "type: ignore" comment without error code
10564+
==
10565+
main.py:1: error: "type: ignore" comment without error code
10566+
1054310567
[case testFineGrainedFunctoolsPartial]
1054410568
import m
1054510569

0 commit comments

Comments
 (0)