@@ -58,7 +58,7 @@ def get_cython_build_rules():
5858
5959
6060@cache
61- def parse_all_cfile_lines (exclude_lines ):
61+ def parse_all_cfile_lines ():
6262 """Parse all generated C files from the build directory."""
6363 #
6464 # Each .c file can include code generated from multiple Cython files (e.g.
@@ -76,7 +76,7 @@ def parse_all_cfile_lines(exclude_lines):
7676
7777 for c_file , _ in get_cython_build_rules ():
7878
79- cfile_lines = parse_cfile_lines (c_file , exclude_lines )
79+ cfile_lines = parse_cfile_lines (c_file )
8080
8181 for cython_file , line_map in cfile_lines .items ():
8282 if cython_file == '(tree fragment)' :
@@ -90,157 +90,38 @@ def parse_all_cfile_lines(exclude_lines):
9090 return all_code_lines
9191
9292
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 )
15797
15898
15999class Plugin (CoveragePlugin ):
160100 """
161101 A Cython coverage plugin for coverage.py suitable for a spin/meson project.
162102 """
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-
168103 def file_tracer (self , filename ):
169104 """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."""
205105 path = Path (filename )
206106
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 :
211108 # A .pyx file from the src directory. The path has src
212109 # stripped out and is not a real absolute path but it looks
213110 # like one. Remove the root prefix and then we have a path
214111 # relative to src_dir.
215- return path .relative_to (root_dir )
112+ srcpath = path .relative_to (root_dir )
113+ return CyFileTracer (srcpath )
216114 else :
115+ # All sorts of paths come here and we reject them
217116 return None
218117
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 )
222122
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 ):
244125 """File tracer for Cython or Python files (.pyx,.pxd,.py)."""
245126
246127 def __init__ (self , srcpath ):
@@ -256,23 +137,24 @@ def has_dynamic_source_filename(self):
256137 def dynamic_source_filename (self , filename , frame ):
257138 """Get filename from frame and return abspath to file."""
258139 # 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 )
261142
262143 # This is called for every traced line. Cache it:
263144 @staticmethod
264145 @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 )
270152
271153
272- class MyFileReporter (FileReporter ):
154+ class CyFileReporter (FileReporter ):
273155 """File reporter for Cython or Python files (.pyx,.pxd,.py)."""
274156
275- def __init__ (self , srcpath , * , exclude_lines ):
157+ def __init__ (self , srcpath ):
276158 abspath = (src_dir / srcpath )
277159 assert abspath .exists ()
278160
@@ -282,32 +164,17 @@ def __init__(self, srcpath, *, exclude_lines):
282164
283165 self .srcpath = srcpath
284166 self .abspath = abspath
285- self .exclude_lines = exclude_lines
286167
287168 def relative_filename (self ):
288169 """Path displayed in the coverage reports."""
289170 return str (self .srcpath )
290171
291172 def lines (self ):
292173 """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."""
307174 srcpath = str (self .srcpath )
308- all_line_maps = parse_all_cfile_lines (self . exclude_lines )
175+ all_line_maps = parse_all_cfile_lines ()
309176 line_map = all_line_maps [srcpath ]
310- return line_map
177+ return set ( line_map )
311178
312179
313180def coverage_init (reg , options ):
0 commit comments