@@ -62,7 +62,7 @@ def get_cython_build_rules():
6262
6363
6464@cache
65- def parse_all_cfile_lines ():
65+ def parse_all_cfile_lines (excluded_line_patterns = () ):
6666 """Parse all generated C files from the build directory."""
6767 #
6868 # Each .c file can include code generated from multiple Cython files (e.g.
@@ -77,6 +77,14 @@ def parse_all_cfile_lines():
7777 # expensive.
7878 #
7979 all_code_lines = {}
80+ all_excluded_lines = defaultdict (set )
81+ if excluded_line_patterns :
82+ pattern = re .compile ("|" .join ([f"(?:{ regex } )" for regex in excluded_line_patterns ]))
83+ line_is_excluded = pattern .search
84+ else :
85+ line_is_excluded = lambda line : False
86+
87+ source_cache = {}
8088
8189 for c_file , _ in get_cython_build_rules ():
8290
@@ -85,13 +93,33 @@ def parse_all_cfile_lines():
8593 for cython_file , line_map in cfile_lines .items ():
8694 if cython_file == '(tree fragment)' :
8795 continue
88- elif cython_file in all_code_lines :
96+ src_lines = source_cache .get (cython_file )
97+ if src_lines is None :
98+ src_path = src_dir / cython_file
99+ if src_path .exists ():
100+ with open (src_path , encoding = "utf8" ) as src :
101+ src_lines = src .read ().splitlines ()
102+ else :
103+ src_lines = []
104+ source_cache [cython_file ] = src_lines
105+
106+ excluded = set ()
107+ filtered_line_map = {}
108+ for lineno , line in line_map .items ():
109+ source_line = src_lines [lineno - 1 ] if 0 < lineno <= len (src_lines ) else ""
110+ if line_is_excluded (source_line ) or line_is_excluded (line ):
111+ excluded .add (lineno )
112+ else :
113+ filtered_line_map [lineno ] = line
114+ if cython_file in all_code_lines :
89115 # Possibly need to merge the lines?
90- assert all_code_lines [cython_file ] == line_map
116+ assert all_code_lines [cython_file ] == filtered_line_map
117+ all_excluded_lines [cython_file ].update (excluded )
91118 else :
92- all_code_lines [cython_file ] = line_map
119+ all_code_lines [cython_file ] = filtered_line_map
120+ all_excluded_lines [cython_file ] = excluded
93121
94- return all_code_lines
122+ return all_code_lines , all_excluded_lines
95123
96124
97125def parse_cfile_lines (c_file ):
@@ -102,6 +130,11 @@ def parse_cfile_lines(c_file):
102130
103131class Plugin (CoveragePlugin ):
104132 """A coverage plugin for a spin/meson project with Cython code."""
133+ _excluded_line_patterns = ()
134+
135+ def configure (self , config ):
136+ # Match Cython's plugin behavior and respect coverage's exclusion regexes.
137+ self ._excluded_line_patterns = tuple (config .get_option ("report:exclude_lines" ))
105138
106139 def file_tracer (self , filename ):
107140 """Find a tracer for filename to handle trace events."""
@@ -121,7 +154,7 @@ def file_tracer(self, filename):
121154 def file_reporter (self , filename ):
122155 """Return a file reporter for filename."""
123156 srcfile = Path (filename ).relative_to (src_dir )
124- return CyFileReporter (srcfile )
157+ return CyFileReporter (srcfile , self . _excluded_line_patterns )
125158
126159
127160class CyFileTracer (FileTracer ):
@@ -157,14 +190,15 @@ def get_source_filename(filename):
157190class CyFileReporter (FileReporter ):
158191 """File reporter for Cython or Python files (.pyx,.pxd,.py)."""
159192
160- def __init__ (self , srcpath ):
193+ def __init__ (self , srcpath , excluded_line_patterns ):
161194 abspath = (src_dir / srcpath )
162195 assert abspath .exists ()
163196
164197 # filepath here needs to match dynamic_source_filename
165198 super ().__init__ (str (abspath ))
166199
167200 self .srcpath = srcpath
201+ self .excluded_line_patterns = excluded_line_patterns
168202
169203 def relative_filename (self ):
170204 """Path displayed in the coverage reports."""
@@ -173,10 +207,15 @@ def relative_filename(self):
173207 def lines (self ):
174208 """Set of line numbers for possibly traceable lines."""
175209 srcpath = str (self .srcpath )
176- all_line_maps = parse_all_cfile_lines ()
210+ all_line_maps , _ = parse_all_cfile_lines (self . excluded_line_patterns )
177211 line_map = all_line_maps [srcpath ]
178212 return set (line_map )
179213
214+ def excluded_lines (self ):
215+ srcpath = str (self .srcpath )
216+ _ , all_excluded_lines = parse_all_cfile_lines (self .excluded_line_patterns )
217+ return set (all_excluded_lines .get (srcpath , ()))
218+
180219
181220def coverage_init (reg , options ):
182221 plugin = Plugin ()
0 commit comments