Skip to content

Commit 9ea349a

Browse files
committed
lab: warn_executed.py
1 parent 808c9b4 commit 9ea349a

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed

lab/warn_executed.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2+
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
3+
4+
"""
5+
$ python warn_executed.py <coverage_data_file> <config_file>
6+
7+
Find lines that were excluded by "warn-executed" regex patterns
8+
but were actually executed according to coverage data.
9+
10+
The config_file is a TOML file with "warn-executed" and "warn-not-partial"
11+
patterns like:
12+
13+
warn-executed = [
14+
"pragma: no cover",
15+
"# debug",
16+
"raise NotImplemented",
17+
]
18+
19+
warn-not-partial = [
20+
"if TYPE_CHECKING:",
21+
]
22+
23+
These should be patterns that you excluded as lines or partial branches.
24+
25+
Warning: this program uses internal undocumented private classes from
26+
coverage.py. This is an unsupported proof-of-concept.
27+
28+
I wrote a blog post about this:
29+
https://nedbatchelder.com/blog/202508/finding_unneeded_pragmas.html
30+
31+
"""
32+
33+
import linecache
34+
import os
35+
import sys
36+
import tomllib
37+
38+
from coverage.parser import PythonParser
39+
from coverage.sqldata import CoverageData
40+
from coverage.results import Analysis
41+
42+
43+
def read_warn_patterns(config_file: str) -> tuple[list[str], list[str]]:
44+
"""Read "warn-executed" and "warn-not-partial" patterns from a TOML config file."""
45+
with open(config_file, "rb") as f:
46+
config = tomllib.load(f)
47+
48+
warn_executed = []
49+
warn_not_partial = []
50+
51+
if "warn-executed" in config:
52+
warn_executed.extend(config["warn-executed"])
53+
if "warn-not-partial" in config:
54+
warn_not_partial.extend(config["warn-not-partial"])
55+
56+
return warn_executed, warn_not_partial
57+
58+
59+
def find_executed_excluded_lines(
60+
source_file: str,
61+
coverage_data: CoverageData,
62+
warn_patterns: list[str],
63+
) -> set[int]:
64+
"""
65+
Find lines that match warn-executed patterns but were actually executed.
66+
67+
Args:
68+
source_file: Path to the Python source file to analyze
69+
coverage_data: The coverage data object
70+
warn_patterns: List of regex patterns that should warn if executed
71+
72+
Returns:
73+
Set of executed line numbers that matched any pattern
74+
"""
75+
executed_lines = coverage_data.lines(source_file)
76+
if executed_lines is None:
77+
return set()
78+
79+
executed_lines = set(executed_lines)
80+
81+
try:
82+
with open(source_file, "r", encoding="utf-8") as f:
83+
source_text = f.read()
84+
except Exception:
85+
return set()
86+
87+
parser = PythonParser(text=source_text, filename=source_file)
88+
parser.parse_source()
89+
90+
all_executed_excluded = set()
91+
for pattern in warn_patterns:
92+
matched_lines = parser.lines_matching(pattern)
93+
all_executed_excluded.update(matched_lines & executed_lines)
94+
95+
return all_executed_excluded
96+
97+
98+
def find_not_partial_lines(
99+
source_file: str,
100+
coverage_data: CoverageData,
101+
warn_patterns: list[str],
102+
) -> set[int]:
103+
"""
104+
Find lines that match warn-not-partial patterns but had both code paths executed.
105+
106+
Args:
107+
source_file: Path to the Python source file to analyze
108+
coverage_data: The coverage data object
109+
warn_patterns: List of regex patterns for lines expected to be partial
110+
111+
Returns:
112+
Set of line numbers that matched patterns but weren't partial
113+
"""
114+
if not coverage_data.has_arcs():
115+
return set()
116+
117+
all_arcs = coverage_data.arcs(source_file)
118+
if all_arcs is None:
119+
return set()
120+
121+
try:
122+
with open(source_file, "r", encoding="utf-8") as f:
123+
source_text = f.read()
124+
except Exception:
125+
return set()
126+
127+
parser = PythonParser(text=source_text, filename=source_file)
128+
parser.parse_source()
129+
130+
all_possible_arcs = set(parser.arcs())
131+
executed_arcs = set(all_arcs)
132+
133+
# Lines with some missing arcs are partial branches
134+
partial_lines = set()
135+
for start_line in {arc[0] for arc in all_possible_arcs if arc[0] > 0}:
136+
possible_from_line = {arc for arc in all_possible_arcs if arc[0] == start_line}
137+
executed_from_line = {arc for arc in executed_arcs if arc[0] == start_line}
138+
if executed_from_line and possible_from_line != executed_from_line:
139+
partial_lines.add(start_line)
140+
141+
all_not_partial = set()
142+
for pattern in warn_patterns:
143+
matched_lines = parser.lines_matching(pattern)
144+
not_partial = matched_lines - partial_lines
145+
all_not_partial.update(not_partial)
146+
147+
return all_not_partial
148+
149+
150+
def analyze_warnings(coverage_file: str, config_file: str) -> dict[str, set[int]]:
151+
"""
152+
Find lines that match warn-executed or warn-not-partial patterns.
153+
154+
Args:
155+
coverage_file: Path to the coverage data file (.coverage)
156+
config_file: Path to TOML config file with warning patterns
157+
158+
Returns:
159+
Dictionary mapping filenames to sets of problematic line numbers
160+
"""
161+
warn_executed_patterns, warn_not_partial_patterns = read_warn_patterns(config_file)
162+
163+
if not warn_executed_patterns and not warn_not_partial_patterns:
164+
return {}
165+
166+
coverage_data = CoverageData(coverage_file)
167+
coverage_data.read()
168+
169+
measured_files = sorted(coverage_data.measured_files())
170+
171+
all_results = {}
172+
for source_file in measured_files:
173+
problem_lines = set()
174+
175+
if warn_executed_patterns:
176+
executed_excluded = find_executed_excluded_lines(
177+
source_file,
178+
coverage_data,
179+
warn_executed_patterns,
180+
)
181+
problem_lines.update(executed_excluded)
182+
183+
if warn_not_partial_patterns:
184+
not_partial = find_not_partial_lines(
185+
source_file,
186+
coverage_data,
187+
warn_not_partial_patterns,
188+
)
189+
problem_lines.update(not_partial)
190+
191+
if problem_lines:
192+
all_results[source_file] = problem_lines
193+
194+
return all_results
195+
196+
197+
def main():
198+
if len(sys.argv) != 3:
199+
print(__doc__.rstrip())
200+
return 1
201+
202+
coverage_file, config_file = sys.argv[1:]
203+
results = analyze_warnings(coverage_file, config_file)
204+
205+
for source_file in sorted(results.keys()):
206+
problem_lines = results[source_file]
207+
for line_num in sorted(problem_lines):
208+
line_text = linecache.getline(source_file, line_num).rstrip()
209+
print(f"{source_file}:{line_num}: {line_text}")
210+
211+
212+
if __name__ == "__main__":
213+
sys.exit(main())

lab/warn_executed.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
warn-executed = [
2+
"pragma: not covered",
3+
"pragma: not testing",
4+
"raise AssertionError",
5+
"pragma: only failure",
6+
"pragma: cant happen",
7+
"pragma: never called",
8+
"pytest.mark.skipif\\(env.METACOV",
9+
]
10+
11+
warn-not-partial = [
12+
"pragma: partial metacov",
13+
"if env.METACOV:",
14+
"pragma: part covered",
15+
]

0 commit comments

Comments
 (0)