blob: 177ddf13cfbef1b33485f0d8661d3157e12e364e [file] [log] [blame]
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
"""
$ python warn_executed.py <coverage_data_file> <config_file>
Find lines that were excluded by "warn-executed" regex patterns
but were actually executed according to coverage data.
The config_file is a TOML file with "warn-executed" and "warn-not-partial"
patterns like:
warn-executed = [
"pragma: no cover",
"# debug",
"raise NotImplemented",
]
warn-not-partial = [
"if TYPE_CHECKING:",
]
These should be patterns that you excluded as lines or partial branches.
Warning: this program uses internal undocumented private classes from
coverage.py. This is an unsupported proof-of-concept.
I wrote a blog post about this:
https://nedbatchelder.com/blog/202508/finding_unneeded_pragmas.html
"""
import linecache
import os
import sys
import tomllib
from coverage.parser import PythonParser
from coverage.sqldata import CoverageData
from coverage.results import Analysis
def read_warn_patterns(config_file: str) -> tuple[list[str], list[str]]:
"""Read "warn-executed" and "warn-not-partial" patterns from a TOML config file."""
with open(config_file, "rb") as f:
config = tomllib.load(f)
warn_executed = []
warn_not_partial = []
if "warn-executed" in config:
warn_executed.extend(config["warn-executed"])
if "warn-not-partial" in config:
warn_not_partial.extend(config["warn-not-partial"])
return warn_executed, warn_not_partial
def find_executed_excluded_lines(
source_file: str,
coverage_data: CoverageData,
warn_patterns: list[str],
) -> set[int]:
"""
Find lines that match warn-executed patterns but were actually executed.
Args:
source_file: Path to the Python source file to analyze
coverage_data: The coverage data object
warn_patterns: List of regex patterns that should warn if executed
Returns:
Set of executed line numbers that matched any pattern
"""
executed_lines = coverage_data.lines(source_file)
if executed_lines is None:
return set()
executed_lines = set(executed_lines)
try:
with open(source_file, "r", encoding="utf-8") as f:
source_text = f.read()
except Exception:
return set()
parser = PythonParser(text=source_text, filename=source_file)
parser.parse_source()
all_executed_excluded = set()
for pattern in warn_patterns:
matched_lines = parser.lines_matching(pattern)
all_executed_excluded.update(matched_lines & executed_lines)
return all_executed_excluded
def find_not_partial_lines(
source_file: str,
coverage_data: CoverageData,
warn_patterns: list[str],
) -> set[int]:
"""
Find lines that match warn-not-partial patterns but had both code paths executed.
Args:
source_file: Path to the Python source file to analyze
coverage_data: The coverage data object
warn_patterns: List of regex patterns for lines expected to be partial
Returns:
Set of line numbers that matched patterns but weren't partial
"""
if not coverage_data.has_arcs():
return set()
all_arcs = coverage_data.arcs(source_file)
if all_arcs is None:
return set()
try:
with open(source_file, "r", encoding="utf-8") as f:
source_text = f.read()
except Exception:
return set()
parser = PythonParser(text=source_text, filename=source_file)
parser.parse_source()
all_possible_arcs = set(parser.arcs())
executed_arcs = set(all_arcs)
# Lines with some missing arcs are partial branches
partial_lines = set()
for start_line in {arc[0] for arc in all_possible_arcs if arc[0] > 0}:
possible_from_line = {arc for arc in all_possible_arcs if arc[0] == start_line}
executed_from_line = {arc for arc in executed_arcs if arc[0] == start_line}
if executed_from_line and possible_from_line != executed_from_line:
partial_lines.add(start_line)
all_not_partial = set()
for pattern in warn_patterns:
matched_lines = parser.lines_matching(pattern)
not_partial = matched_lines - partial_lines
all_not_partial.update(not_partial)
return all_not_partial
def analyze_warnings(coverage_file: str, config_file: str) -> dict[str, set[int]]:
"""
Find lines that match warn-executed or warn-not-partial patterns.
Args:
coverage_file: Path to the coverage data file (.coverage)
config_file: Path to TOML config file with warning patterns
Returns:
Dictionary mapping filenames to sets of problematic line numbers
"""
warn_executed_patterns, warn_not_partial_patterns = read_warn_patterns(config_file)
if not warn_executed_patterns and not warn_not_partial_patterns:
return {}
coverage_data = CoverageData(coverage_file)
coverage_data.read()
measured_files = sorted(coverage_data.measured_files())
all_results = {}
for source_file in measured_files:
problem_lines = set()
if warn_executed_patterns:
executed_excluded = find_executed_excluded_lines(
source_file,
coverage_data,
warn_executed_patterns,
)
problem_lines.update(executed_excluded)
if warn_not_partial_patterns:
not_partial = find_not_partial_lines(
source_file,
coverage_data,
warn_not_partial_patterns,
)
problem_lines.update(not_partial)
if problem_lines:
all_results[source_file] = problem_lines
return all_results
def main():
if len(sys.argv) != 3:
print(__doc__.rstrip())
return 1
coverage_file, config_file = sys.argv[1:]
results = analyze_warnings(coverage_file, config_file)
for source_file in sorted(results.keys()):
problem_lines = results[source_file]
for line_num in sorted(problem_lines):
line_text = linecache.getline(source_file, line_num).rstrip()
print(f"{source_file}:{line_num}: {line_text}")
if __name__ == "__main__":
sys.exit(main())