Skip to content

Instantly share code, notes, and snippets.

@alhoo
Created May 10, 2025 05:33
Show Gist options
  • Save alhoo/13d17be49645f32bbb73787d52c15c06 to your computer and use it in GitHub Desktop.
Save alhoo/13d17be49645f32bbb73787d52c15c06 to your computer and use it in GitHub Desktop.
coverage.json analyzer
import json
import ast
import os
def get_functions_from_source(filepath):
"""
Parses a Python file and returns a list of functions with their line numbers.
Each item is a tuple: (function_name, start_line, end_line).
"""
functions = []
try:
with open(filepath, 'r', encoding='utf-8') as f:
source_code = f.read()
tree = ast.parse(source_code, filename=filepath)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
start_line = node.lineno
# Determine end_line more robustly
if hasattr(node, 'end_lineno') and node.end_lineno is not None:
end_line = node.end_lineno
else:
# Fallback: find max line number of direct children or the function def line itself
if node.body:
# Get the last line of the last statement in the body
# This can be complex for multi-line statements.
# A simpler heuristic: last line of any node within the function body.
max_child_line = start_line
for child_node in ast.walk(node): # Walk within the function node
if child_node is not node and hasattr(child_node, 'lineno'): # Exclude the func def itself
current_line = getattr(child_node, 'end_lineno', getattr(child_node, 'lineno'))
if current_line is not None:
max_child_line = max(max_child_line, current_line)
end_line = max_child_line
if not node.body or end_line < start_line : # if only pass or docstring
end_line = start_line # Simplistic fallback for really empty functions
else: # Empty function body
end_line = start_line
functions.append((node.name, start_line, end_line))
except Exception as e:
print(f"Could not parse {filepath}: {e}")
return functions
def find_partially_covered_functions(
coverage_json_path="coverage.json",
project_root=".",
coverage_threshold_percent=20.0 # Default threshold: 20%
):
"""
Identifies functions where the percentage of executed lines within the function
is below the specified threshold.
"""
under_covered_functions_map = {}
try:
with open(coverage_json_path, 'r') as f:
coverage_data = json.load(f)
except FileNotFoundError:
print(f"Error: {coverage_json_path} not found. Run pytest with --cov-report=json first.")
return {}
except json.JSONDecodeError:
print(f"Error: Could not decode {coverage_json_path}. Is it a valid JSON file?")
return {}
if 'files' not in coverage_data:
print("Error: 'files' key not found in coverage.json. The format might be unexpected.")
return {}
for filepath_rel, file_data in coverage_data.get('files', {}).items():
filepath_abs = os.path.abspath(os.path.join(project_root, filepath_rel))
if not os.path.exists(filepath_abs):
print(f"Warning: Source file {filepath_abs} (from {filepath_rel}) not found. Skipping.")
continue
if not filepath_abs.endswith(".py") or "test" in filepath_abs.lower():
continue
source_functions = get_functions_from_source(filepath_abs)
executed_lines_in_file = set(file_data.get('executed_lines', []))
missing_lines_in_file = set(file_data.get('missing_lines', [])) # Lines that could be executed
file_under_covered_functions = []
for func_name, start_line, end_line in source_functions:
func_executed_count = 0
func_executable_count = 0 # Lines in function that are either executed or missing
# Iterate over the lines within the function's span (inclusive)
for line_num in range(start_line, end_line + 1):
if line_num in executed_lines_in_file:
func_executed_count += 1
func_executable_count += 1
elif line_num in missing_lines_in_file:
# This line is part of the function, could have been executed, but wasn't.
# It contributes to the total number of executable lines in the function.
func_executable_count += 1
# Lines not in executed_lines nor missing_lines are comments, blank,
# or excluded by pragmas (which coverage.py already handles by not listing them
# as missing or executed if they are truly excluded).
if func_executable_count > 0: # Avoid division by zero for empty/non-executable functions
percentage_covered = (func_executed_count / func_executable_count) * 100
if percentage_covered < coverage_threshold_percent:
file_under_covered_functions.append(
f"{func_name} (lines {start_line}-{end_line}, {percentage_covered:.1f}% covered, {func_executed_count}/{func_executable_count} lines)"
)
elif func_executable_count == 0 and start_line <= end_line :
# This function has no lines that coverage.py considers executable
# (e.g., only pass, docstrings, or all lines #pragma: no cover)
# You might decide to always list these, or consider them "N/A" or "100% covered"
# For this script, if threshold is > 0, it will appear if not 100% (0/0 is tricky)
# Let's assume if threshold is 0, it means "any coverage".
# If threshold is > 0, and it has 0 executable lines, it won't meet the threshold
# unless the threshold is 0. If threshold is 0, it means "any coverage is fine",
# so a func with 0 executable lines is fine.
# If the threshold is, say, 20%, and a function has 0 executable lines,
# 0% < 20% is true, so it would be listed.
# This feels right: if it has no executable code, it can't meet a positive threshold.
if coverage_threshold_percent > 0 : # only list if we expect some coverage
file_under_covered_functions.append(
f"{func_name} (lines {start_line}-{end_line}, 0.0% covered, 0/0 executable lines)"
)
if file_under_covered_functions:
under_covered_functions_map[filepath_rel] = file_under_covered_functions
return under_covered_functions_map
if __name__ == "__main__":
# --- Configuration ---
json_file = "coverage.json"
# Set this to the root of your source code if coverage.json paths are relative to it
# Or if the script is not in the same dir as coverage.json
source_project_root = "."
# Minimum percentage of lines within a function that must be covered
min_coverage_percent_for_function = 20.0
# --- End Configuration ---
print(f"Looking for functions with less than {min_coverage_percent_for_function}% line coverage...\n")
uncovered = find_partially_covered_functions(
coverage_json_path=json_file,
project_root=source_project_root,
coverage_threshold_percent=min_coverage_percent_for_function
)
if not uncovered:
print(f"All identified functions meet or exceed the {min_coverage_percent_for_function}% coverage threshold (or no Python files found/parsed)!")
else:
print(f"Functions with less than {min_coverage_percent_for_function}% coverage:")
for filepath, funcs in uncovered.items():
print(f" File: {filepath}")
for func_info in funcs:
print(f" - {func_info}")
print("\nNote:")
print("- 'Executable lines' are lines coverage.py identified as either executed or missing (could be run).")
print("- Function line ranges are determined by AST parsing.")
print("- Exclude test files or virtual envs from your --cov target for cleaner results.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment