diff --git a/Lib/test/test_trace.py b/Lib/test/test_trace.py index 19eee19bdea6d5..3b27ba93aa1bf4 100644 --- a/Lib/test/test_trace.py +++ b/Lib/test/test_trace.py @@ -499,6 +499,57 @@ def test_cover_files_written_with_highlight(self): >>>>>> print('unreachable') ''')) + +# gh-142867: Deferred annotations (PEP 649) should not appear as not covered +class TestCoverageAnnotations(unittest.TestCase): + + codefile = 'tmp_annotations.py' + coverfile = 'tmp_annotations.cover' + + def tearDown(self): + unlink(self.codefile) + unlink(self.coverfile) + + def test_multiline_annotation_not_marked_uncovered(self): + # gh-142867: Multiline type annotations should not be marked as + # uncovered since annotation evaluation is deferred (PEP 649) + with open(self.codefile, 'w', encoding='utf-8') as f: + f.write(textwrap.dedent('''\ + def x() -> tuple[ + int, + ]: + return (1,) + + x() + ''')) + argv = '-m trace --count --missing'.split() + [self.codefile] + status, stdout, stderr = assert_python_ok(*argv) + self.assertTrue(os.path.exists(self.coverfile)) + with open(self.coverfile, encoding='utf-8') as f: + content = f.read() + # The multiline annotation (line 2: "int,") should NOT be marked + # as uncovered with ">>>>>>". It should have a blank prefix since + # annotation lines are not executed during normal program flow. + self.assertNotIn('>>>>>> ', content) + + def test_class_annotation_not_marked_uncovered(self): + # Class attribute annotations should not be marked as uncovered + with open(self.codefile, 'w', encoding='utf-8') as f: + f.write(textwrap.dedent('''\ + class X: + a: int + + x = X() + ''')) + argv = '-m trace --count --missing'.split() + [self.codefile] + status, stdout, stderr = assert_python_ok(*argv) + self.assertTrue(os.path.exists(self.coverfile)) + with open(self.coverfile, encoding='utf-8') as f: + content = f.read() + # The annotation line should NOT be marked as uncovered + self.assertNotIn('>>>>>> ', content) + + class TestCommandLine(unittest.TestCase): def test_failures(self): diff --git a/Lib/trace.py b/Lib/trace.py index cd3a6d30661da3..690f9a86f68131 100644 --- a/Lib/trace.py +++ b/Lib/trace.py @@ -346,6 +346,9 @@ def _find_lines(code, strs): # and check the constants for references to other code objects for c in code.co_consts: if inspect.iscode(c): + # skip __annotate__ code objects (PEP 649) + if c.co_name == "__annotate__": + continue # find another code object, so recurse into it linenos.update(_find_lines(c, strs)) return linenos diff --git a/Misc/NEWS.d/next/Library/2025-12-24-23-30-42.gh-issue-142867.xR4Kj9.rst b/Misc/NEWS.d/next/Library/2025-12-24-23-30-42.gh-issue-142867.xR4Kj9.rst new file mode 100644 index 00000000000000..d85f9d75fe679c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-24-23-30-42.gh-issue-142867.xR4Kj9.rst @@ -0,0 +1,2 @@ +Fix :mod:`trace` module incorrectly marking deferred annotations (PEP 649) +as not covered in ``--count --missing`` mode.