@@ -58,7 +58,7 @@ def get_cython_build_rules():
58
58
59
59
60
60
@cache
61
- def parse_all_cfile_lines (exclude_lines ):
61
+ def parse_all_cfile_lines ():
62
62
"""Parse all generated C files from the build directory."""
63
63
#
64
64
# Each .c file can include code generated from multiple Cython files (e.g.
@@ -76,7 +76,7 @@ def parse_all_cfile_lines(exclude_lines):
76
76
77
77
for c_file , _ in get_cython_build_rules ():
78
78
79
- cfile_lines = parse_cfile_lines (c_file , exclude_lines )
79
+ cfile_lines = parse_cfile_lines (c_file )
80
80
81
81
for cython_file , line_map in cfile_lines .items ():
82
82
if cython_file == '(tree fragment)' :
@@ -90,157 +90,38 @@ def parse_all_cfile_lines(exclude_lines):
90
90
return all_code_lines
91
91
92
92
93
- def parse_cfile_lines (c_file , exclude_lines ):
94
- """Parse a C file and extract all source file lines."""
95
- #
96
- # The C code has comments that refer to the Cython source files. We want to
97
- # parse those comments and match them up with the __Pyx_TraceLine() calls
98
- # in the C code. The __Pyx_TraceLine calls generate the trace events which
99
- # coverage feeds through to our plugin. If we can pair them up back to the
100
- # Cython source files then we measure coverage of the original Cython code.
101
- #
102
- match_source_path_line = re .compile (r' */[*] +"(.*)":([0-9]+)$' ).match
103
- match_current_code_line = re .compile (r' *[*] (.*) # <<<<<<+$' ).match
104
- match_comment_end = re .compile (r' *[*]/$' ).match
105
- match_trace_line = re .compile (r' *__Pyx_TraceLine\(([0-9]+),' ).match
106
- not_executable = re .compile (
107
- r'\s*c(?:type)?def\s+'
108
- r'(?:(?:public|external)\s+)?'
109
- r'(?:struct|union|enum|class)'
110
- r'(\s+[^:]+|)\s*:'
111
- ).match
112
-
113
- # Exclude e.g. # pragma: nocover
114
- exclude_pats = [f"(?:{ regex } )" for regex in exclude_lines ]
115
- line_is_excluded = re .compile ("|" .join (exclude_pats )).search
116
-
117
- code_lines = defaultdict (dict )
118
- executable_lines = defaultdict (set )
119
- current_filename = None
120
-
121
- with open (c_file ) as lines :
122
- lines = iter (lines )
123
- for line in lines :
124
- match = match_source_path_line (line )
125
- if not match :
126
- if '__Pyx_TraceLine(' in line and current_filename is not None :
127
- trace_line = match_trace_line (line )
128
- if trace_line :
129
- executable_lines [current_filename ].add (int (trace_line .group (1 )))
130
- continue
131
- filename , lineno = match .groups ()
132
- current_filename = filename
133
- lineno = int (lineno )
134
- for comment_line in lines :
135
- match = match_current_code_line (comment_line )
136
- if match :
137
- code_line = match .group (1 ).rstrip ()
138
- if not_executable (code_line ):
139
- break
140
- if line_is_excluded (code_line ):
141
- break
142
- code_lines [filename ][lineno ] = code_line
143
- break
144
- elif match_comment_end (comment_line ):
145
- # unexpected comment format - false positive?
146
- break
147
-
148
- exe_code_lines = {}
149
-
150
- for fname in code_lines :
151
- # Remove lines that generated code but are not traceable.
152
- exe_lines = set (executable_lines .get (fname , ()))
153
- line_map = {n : c for n , c in code_lines [fname ].items () if n in exe_lines }
154
- exe_code_lines [fname ] = line_map
155
-
156
- return exe_code_lines
93
+ def parse_cfile_lines (c_file ):
94
+ """Use Cython's coverage plugin to parse the C code."""
95
+ from Cython .Coverage import Plugin
96
+ return Plugin ()._parse_cfile_lines (c_file )
157
97
158
98
159
99
class Plugin (CoveragePlugin ):
160
100
"""
161
101
A Cython coverage plugin for coverage.py suitable for a spin/meson project.
162
102
"""
163
- def configure (self , config ):
164
- """Configure the plugin based on .coveragerc/pyproject.toml."""
165
- # Read the regular expressions from the coverage config
166
- self .exclude_lines = tuple (config .get_option ("report:exclude_lines" ))
167
-
168
103
def file_tracer (self , filename ):
169
104
"""Find a tracer for filename as reported in trace events."""
170
- # All sorts of paths come here and we discard them if they do not begin
171
- # with the path to this directory. Otherwise we return a tracer.
172
- srcfile = self .get_source_file_tracer (filename )
173
-
174
- if srcfile is None :
175
- return None
176
-
177
- return MyFileTracer (srcfile )
178
-
179
- def file_reporter (self , filename ):
180
- """Return a file reporter for filename."""
181
- srcfile = self .get_source_file_reporter (filename )
182
-
183
- return MyFileReporter (srcfile , exclude_lines = self .exclude_lines )
184
-
185
- #
186
- # It is important not to mix up get_source_file_tracer and
187
- # get_source_file_reporter. On the face of it these two functions do the
188
- # same thing i.e. you give a path and they return a path relative to src.
189
- # However the inputs they receive are different. For get_source_file_tracer
190
- # the inputs are semi-garbage paths from coverage. In particular the Cython
191
- # trace events use src-relative paths but coverage merges those with CWD to
192
- # make paths that look absolute but do not really exist. The paths sent to
193
- # get_source_file_reporter come indirectly from
194
- # MyFileTracer.dynamic_source_filename which we control and so those paths
195
- # are real absolute paths to the source files in the src dir.
196
- #
197
- # We make sure that get_source_file_tracer is the only place that needs to
198
- # deal with garbage paths. It also needs to filter out all of the
199
- # irrelevant paths that coverage sends our way. Once that data cleaning is
200
- # done we can work with real paths sanely.
201
- #
202
-
203
- def get_source_file_tracer (self , filename ):
204
- """Map from coverage path to srcpath."""
205
105
path = Path (filename )
206
106
207
- if build_install_dir in path .parents :
208
- # A .py file in the build-install directory.
209
- return self .get_source_file_build_install (path )
210
- elif root_dir in path .parents :
107
+ if path .suffix in ('.pyx' , '.pxd' ) and root_dir in path .parents :
211
108
# A .pyx file from the src directory. The path has src
212
109
# stripped out and is not a real absolute path but it looks
213
110
# like one. Remove the root prefix and then we have a path
214
111
# relative to src_dir.
215
- return path .relative_to (root_dir )
112
+ srcpath = path .relative_to (root_dir )
113
+ return CyFileTracer (srcpath )
216
114
else :
115
+ # All sorts of paths come here and we reject them
217
116
return None
218
117
219
- def get_source_file_reporter (self , filename ):
220
- """Map from coverage path to srcpath."""
221
- path = Path (filename )
118
+ def file_reporter (self , filename ):
119
+ """Return a file reporter for filename."""
120
+ srcfile = Path (filename ).relative_to (src_dir )
121
+ return CyFileReporter (srcfile )
222
122
223
- if build_install_dir in path .parents :
224
- # A .py file in the build-install directory.
225
- return self .get_source_file_build_install (path )
226
- else :
227
- # An absolute path to a file in src dir.
228
- return path .relative_to (src_dir )
229
-
230
- def get_source_file_build_install (self , path ):
231
- """Get src-relative path for file in build-install directory."""
232
- # A .py file in the build-install directory. We want to find its
233
- # relative path from the src directory. One of path.parents is on
234
- # sys.path and the relpath from there is also the relpath from src.
235
- for pkgdir in path .parents :
236
- init = pkgdir / '__init__.py'
237
- if not init .exists ():
238
- sys_path_dir = pkgdir
239
- return path .relative_to (sys_path_dir )
240
- assert False
241
-
242
-
243
- class MyFileTracer (FileTracer ):
123
+
124
+ class CyFileTracer (FileTracer ):
244
125
"""File tracer for Cython or Python files (.pyx,.pxd,.py)."""
245
126
246
127
def __init__ (self , srcpath ):
@@ -256,23 +137,24 @@ def has_dynamic_source_filename(self):
256
137
def dynamic_source_filename (self , filename , frame ):
257
138
"""Get filename from frame and return abspath to file."""
258
139
# What is returned here needs to match MyFileReporter.filename
259
- srcpath = frame .f_code .co_filename
260
- return self .srcpath_to_abs ( srcpath )
140
+ path = frame .f_code .co_filename
141
+ return self .get_source_filename ( path )
261
142
262
143
# This is called for every traced line. Cache it:
263
144
@staticmethod
264
145
@cache
265
- def srcpath_to_abs (srcpath ):
266
- """Get absolute path string from src-relative path."""
267
- abspath = (src_dir / srcpath ).absolute ()
268
- assert abspath .exists ()
269
- return str (abspath )
146
+ def get_source_filename (filename ):
147
+ """Get src-relative path for filename from trace event."""
148
+ path = src_dir / filename
149
+ assert src_dir in path .parents
150
+ assert path .exists ()
151
+ return str (path )
270
152
271
153
272
- class MyFileReporter (FileReporter ):
154
+ class CyFileReporter (FileReporter ):
273
155
"""File reporter for Cython or Python files (.pyx,.pxd,.py)."""
274
156
275
- def __init__ (self , srcpath , * , exclude_lines ):
157
+ def __init__ (self , srcpath ):
276
158
abspath = (src_dir / srcpath )
277
159
assert abspath .exists ()
278
160
@@ -282,32 +164,17 @@ def __init__(self, srcpath, *, exclude_lines):
282
164
283
165
self .srcpath = srcpath
284
166
self .abspath = abspath
285
- self .exclude_lines = exclude_lines
286
167
287
168
def relative_filename (self ):
288
169
"""Path displayed in the coverage reports."""
289
170
return str (self .srcpath )
290
171
291
172
def lines (self ):
292
173
"""Set of line numbers for possibly traceable lines."""
293
- if self .srcpath .suffix == '.py' :
294
- line_map = self .get_pyfile_line_map ()
295
- else :
296
- line_map = self .get_cyfile_line_map ()
297
- return set (line_map )
298
-
299
- def get_pyfile_line_map (self ):
300
- """Return all lines from .py file."""
301
- with open (self .abspath ) as pyfile :
302
- line_map = dict (enumerate (pyfile ))
303
- return line_map
304
-
305
- def get_cyfile_line_map (self ):
306
- """Get all traceable code lines for this file."""
307
174
srcpath = str (self .srcpath )
308
- all_line_maps = parse_all_cfile_lines (self . exclude_lines )
175
+ all_line_maps = parse_all_cfile_lines ()
309
176
line_map = all_line_maps [srcpath ]
310
- return line_map
177
+ return set ( line_map )
311
178
312
179
313
180
def coverage_init (reg , options ):
0 commit comments