99import csv
1010from collections import defaultdict
1111from collections .abc import Iterable
12+ from dataclasses import asdict , dataclass
1213from pathlib import Path
13- from typing import TypedDict
1414
1515
16- class CoverageStats (TypedDict ):
17- """Type definition for coverage statistics."""
16+ @dataclass (frozen = True , kw_only = True )
17+ class CoverageStats :
18+ """Line and function coverage of a module or the whole repo, with percentages."""
1819
1920 lines_coverage_percent : float
2021 functions_coverage_percent : float
@@ -24,25 +25,24 @@ class CoverageStats(TypedDict):
2425 total_functions : int
2526
2627
27- class RawStats (TypedDict ):
28- """Type definition for raw statistics from LCOV parsing."""
28+ @dataclass (kw_only = True )
29+ class RawStats :
30+ """Covered and total line/function counts accumulated from an LCOV tracefile."""
2931
30- lines : int
31- lines_covered : int
32- functions : int
33- functions_covered : int
32+ lines : int = 0
33+ lines_covered : int = 0
34+ functions : int = 0
35+ functions_covered : int = 0
3436
37+ def record_line (self , * , hits : int ) -> None :
38+ """Count one more line, covered when ``hits > 0``."""
39+ self .lines += 1
40+ self .lines_covered += int (hits > 0 )
3541
36- def _record_line (stats : RawStats , * , hits : int ) -> None :
37- stats ["lines" ] += 1
38- if hits > 0 :
39- stats ["lines_covered" ] += 1
40-
41-
42- def _record_function (stats : RawStats , * , hits : int ) -> None :
43- stats ["functions" ] += 1
44- if hits > 0 :
45- stats ["functions_covered" ] += 1
42+ def record_function (self , * , hits : int ) -> None :
43+ """Count one more function, covered when ``hits > 0``."""
44+ self .functions += 1
45+ self .functions_covered += int (hits > 0 )
4646
4747
4848def parse_lcov (lines : Iterable [str ]) -> dict [str , RawStats ]:
@@ -54,9 +54,7 @@ def parse_lcov(lines: Iterable[str]) -> dict[str, RawStats]:
5454 no hit count and are ignored); line coverage from ``DA:<line>,<hits>``
5555 records.
5656 """
57- file_data : dict [str , RawStats ] = defaultdict (
58- lambda : RawStats (lines = 0 , lines_covered = 0 , functions = 0 , functions_covered = 0 )
59- )
57+ file_data : dict [str , RawStats ] = defaultdict (RawStats )
6058
6159 current_file : str | None = None
6260 for raw_line in lines :
@@ -66,52 +64,43 @@ def parse_lcov(lines: Iterable[str]) -> dict[str, RawStats]:
6664 elif current_file is None :
6765 continue
6866 elif line .startswith ("FNA:" ):
69- _record_function ( file_data [current_file ], hits = int (line [4 :].split ("," , 2 )[1 ]))
67+ file_data [current_file ]. record_function ( hits = int (line [4 :].split ("," , 2 )[1 ]))
7068 elif line .startswith ("DA:" ):
71- _record_line ( file_data [current_file ], hits = int (line [3 :].split ("," )[1 ]))
69+ file_data [current_file ]. record_line ( hits = int (line [3 :].split ("," )[1 ]))
7270 return file_data
7371
7472
7573def calculate_coverage_stats (stats : RawStats ) -> CoverageStats :
7674 """Calculate coverage percentages for given stats."""
77- total_lines = stats ["lines" ]
78- covered_lines = stats ["lines_covered" ]
79- line_cov_pct = 100.0 * covered_lines / total_lines if total_lines else 0
80-
81- total_funcs = stats ["functions" ]
82- covered_funcs = stats ["functions_covered" ]
83- func_cov_pct = 100.0 * covered_funcs / total_funcs if total_funcs else 0
75+ line_cov_pct = 100.0 * stats .lines_covered / stats .lines if stats .lines else 0
76+ func_cov_pct = 100.0 * stats .functions_covered / stats .functions if stats .functions else 0
8477
8578 return CoverageStats (
8679 lines_coverage_percent = round (line_cov_pct , 2 ),
8780 functions_coverage_percent = round (func_cov_pct , 2 ),
88- covered_lines = covered_lines ,
89- total_lines = total_lines ,
90- covered_functions = covered_funcs ,
91- total_functions = total_funcs ,
81+ covered_lines = stats . lines_covered ,
82+ total_lines = stats . lines ,
83+ covered_functions = stats . functions_covered ,
84+ total_functions = stats . functions ,
9285 )
9386
9487
9588def calculate_total_coverage (file_data : dict [str , RawStats ]) -> CoverageStats :
9689 """Calculate total project coverage statistics."""
97- totals = RawStats (lines = 0 , lines_covered = 0 , functions = 0 , functions_covered = 0 )
98- for stats in file_data .values ():
99- totals ["lines" ] += stats ["lines" ]
100- totals ["lines_covered" ] += stats ["lines_covered" ]
101- totals ["functions" ] += stats ["functions" ]
102- totals ["functions_covered" ] += stats ["functions_covered" ]
103-
104- return calculate_coverage_stats (totals )
90+ stats = file_data .values ()
91+ return calculate_coverage_stats (
92+ RawStats (
93+ lines = sum (s .lines for s in stats ),
94+ lines_covered = sum (s .lines_covered for s in stats ),
95+ functions = sum (s .functions for s in stats ),
96+ functions_covered = sum (s .functions_covered for s in stats ),
97+ )
98+ )
10599
106100
107101def write_csv_row (writer : csv .DictWriter [str ], file_path : str , stats : CoverageStats ) -> None :
108102 """Write a single coverage row to CSV."""
109- writer .writerow (
110- {
111- "file_path" : file_path ,
112- ** stats ,
113- }
114- )
103+ writer .writerow ({"file_path" : file_path , ** asdict (stats )})
115104
116105
117106def write_csv (
0 commit comments