Skip to content

Commit a4801f9

Browse files
meshyseddonymhauntsaninjailevkivskyipre-commit-ci[bot]
authored andcommitted
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 c3bfa0d commit a4801f9

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
@@ -786,6 +786,8 @@ def generate_unused_ignore_errors(self, file: str) -> None:
786786
code=codes.UNUSED_IGNORE,
787787
blocker=False,
788788
only_once=False,
789+
origin=(self.file, [line]),
790+
target=self.target_module,
789791
)
790792
self._add_error_info(file, info)
791793

@@ -837,6 +839,8 @@ def generate_ignore_without_code_errors(
837839
code=codes.IGNORE_WITHOUT_CODE,
838840
blocker=False,
839841
only_once=False,
842+
origin=(self.file, [line]),
843+
target=self.target_module,
840844
)
841845
self._add_error_info(file, info)
842846

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy