|
1 """Core control stuff for Coverage.""" |
|
2 |
|
3 import os, socket |
|
4 |
|
5 from .annotate import AnnotateReporter |
|
6 from .codeunit import code_unit_factory |
|
7 from .collector import Collector |
|
8 from .data import CoverageData |
|
9 from .files import FileLocator |
|
10 from .html import HtmlReporter |
|
11 from .misc import format_lines, CoverageException |
|
12 from .summary import SummaryReporter |
|
13 |
|
14 class coverage: |
|
15 """Programmatic access to Coverage. |
|
16 |
|
17 To use:: |
|
18 |
|
19 from coverage import coverage |
|
20 |
|
21 cov = coverage() |
|
22 cov.start() |
|
23 #.. blah blah (run your code) blah blah |
|
24 cov.stop() |
|
25 cov.html_report(directory='covhtml') |
|
26 |
|
27 """ |
|
28 def __init__(self, data_file=None, data_suffix=False, cover_pylib=False, |
|
29 auto_data=False): |
|
30 """Create a new coverage measurement context. |
|
31 |
|
32 `data_file` is the base name of the data file to use, defaulting to |
|
33 ".coverage". `data_suffix` is appended to `data_file` to create the |
|
34 final file name. If `data_suffix` is simply True, then a suffix is |
|
35 created with the machine and process identity included. |
|
36 |
|
37 `cover_pylib` is a boolean determining whether Python code installed |
|
38 with the Python interpreter is measured. This includes the Python |
|
39 standard library and any packages installed with the interpreter. |
|
40 |
|
41 If `auto_data` is true, then any existing data file will be read when |
|
42 coverage measurement starts, and data will be saved automatically when |
|
43 measurement stops. |
|
44 |
|
45 """ |
|
46 from coverage import __version__ |
|
47 |
|
48 self.cover_pylib = cover_pylib |
|
49 self.auto_data = auto_data |
|
50 |
|
51 self.exclude_re = "" |
|
52 self.exclude_list = [] |
|
53 |
|
54 self.file_locator = FileLocator() |
|
55 |
|
56 self.collector = Collector(self._should_trace) |
|
57 |
|
58 # Create the data file. |
|
59 if data_suffix: |
|
60 if not isinstance(data_suffix, str): |
|
61 # if data_suffix=True, use .machinename.pid |
|
62 data_suffix = ".%s.%s" % (socket.gethostname(), os.getpid()) |
|
63 else: |
|
64 data_suffix = None |
|
65 |
|
66 self.data = CoverageData( |
|
67 basename=data_file, suffix=data_suffix, |
|
68 collector="coverage v%s" % __version__ |
|
69 ) |
|
70 |
|
71 # The default exclude pattern. |
|
72 self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]') |
|
73 |
|
74 # The prefix for files considered "installed with the interpreter". |
|
75 if not self.cover_pylib: |
|
76 os_file = self.file_locator.canonical_filename(os.__file__) |
|
77 self.pylib_prefix = os.path.split(os_file)[0] |
|
78 |
|
79 here = self.file_locator.canonical_filename(__file__) |
|
80 self.cover_prefix = os.path.split(here)[0] |
|
81 |
|
82 def _should_trace(self, filename, frame): |
|
83 """Decide whether to trace execution in `filename` |
|
84 |
|
85 Returns a canonicalized filename if it should be traced, False if it |
|
86 should not. |
|
87 |
|
88 """ |
|
89 if filename == '<string>': |
|
90 # There's no point in ever tracing string executions, we can't do |
|
91 # anything with the data later anyway. |
|
92 return False |
|
93 |
|
94 # Compiled Python files have two filenames: frame.f_code.co_filename is |
|
95 # the filename at the time the .pyc was compiled. The second name |
|
96 # is __file__, which is where the .pyc was actually loaded from. Since |
|
97 # .pyc files can be moved after compilation (for example, by being |
|
98 # installed), we look for __file__ in the frame and prefer it to the |
|
99 # co_filename value. |
|
100 dunder_file = frame.f_globals.get('__file__') |
|
101 if dunder_file: |
|
102 if not dunder_file.endswith(".py"): |
|
103 if dunder_file[-4:-1] == ".py": |
|
104 dunder_file = dunder_file[:-1] |
|
105 filename = dunder_file |
|
106 |
|
107 canonical = self.file_locator.canonical_filename(filename) |
|
108 |
|
109 # If we aren't supposed to trace installed code, then check if this is |
|
110 # near the Python standard library and skip it if so. |
|
111 if not self.cover_pylib: |
|
112 if canonical.startswith(self.pylib_prefix): |
|
113 return False |
|
114 |
|
115 # We exclude the coverage code itself, since a little of it will be |
|
116 # measured otherwise. |
|
117 if canonical.startswith(self.cover_prefix): |
|
118 return False |
|
119 |
|
120 return canonical |
|
121 |
|
122 def use_cache(self, usecache): |
|
123 """Control the use of a data file (incorrectly called a cache). |
|
124 |
|
125 `usecache` is true or false, whether to read and write data on disk. |
|
126 |
|
127 """ |
|
128 self.data.usefile(usecache) |
|
129 |
|
130 def load(self): |
|
131 """Load previously-collected coverage data from the data file.""" |
|
132 self.collector.reset() |
|
133 self.data.read() |
|
134 |
|
135 def start(self): |
|
136 """Start measuring code coverage.""" |
|
137 if self.auto_data: |
|
138 self.load() |
|
139 # Save coverage data when Python exits. |
|
140 import atexit |
|
141 atexit.register(self.save) |
|
142 self.collector.start() |
|
143 |
|
144 def stop(self): |
|
145 """Stop measuring code coverage.""" |
|
146 self.collector.stop() |
|
147 self._harvest_data() |
|
148 |
|
149 def erase(self): |
|
150 """Erase previously-collected coverage data. |
|
151 |
|
152 This removes the in-memory data collected in this session as well as |
|
153 discarding the data file. |
|
154 |
|
155 """ |
|
156 self.collector.reset() |
|
157 self.data.erase() |
|
158 |
|
159 def clear_exclude(self): |
|
160 """Clear the exclude list.""" |
|
161 self.exclude_list = [] |
|
162 self.exclude_re = "" |
|
163 |
|
164 def exclude(self, regex): |
|
165 """Exclude source lines from execution consideration. |
|
166 |
|
167 `regex` is a regular expression. Lines matching this expression are |
|
168 not considered executable when reporting code coverage. A list of |
|
169 regexes is maintained; this function adds a new regex to the list. |
|
170 Matching any of the regexes excludes a source line. |
|
171 |
|
172 """ |
|
173 self.exclude_list.append(regex) |
|
174 self.exclude_re = "(" + ")|(".join(self.exclude_list) + ")" |
|
175 |
|
176 def get_exclude_list(self): |
|
177 """Return the list of excluded regex patterns.""" |
|
178 return self.exclude_list |
|
179 |
|
180 def save(self): |
|
181 """Save the collected coverage data to the data file.""" |
|
182 self._harvest_data() |
|
183 self.data.write() |
|
184 |
|
185 def combine(self): |
|
186 """Combine together a number of similarly-named coverage data files. |
|
187 |
|
188 All coverage data files whose name starts with `data_file` (from the |
|
189 coverage() constructor) will be read, and combined together into the |
|
190 current measurements. |
|
191 |
|
192 """ |
|
193 self.data.combine_parallel_data() |
|
194 |
|
195 def _harvest_data(self): |
|
196 """Get the collected data by filename and reset the collector.""" |
|
197 self.data.add_line_data(self.collector.data_points()) |
|
198 self.collector.reset() |
|
199 |
|
200 # Backward compatibility with version 1. |
|
201 def analysis(self, morf): |
|
202 """Like `analysis2` but doesn't return excluded line numbers.""" |
|
203 f, s, _, m, mf = self.analysis2(morf) |
|
204 return f, s, m, mf |
|
205 |
|
206 def analysis2(self, morf): |
|
207 """Analyze a module. |
|
208 |
|
209 `morf` is a module or a filename. It will be analyzed to determine |
|
210 its coverage statistics. The return value is a 5-tuple: |
|
211 |
|
212 * The filename for the module. |
|
213 * A list of line numbers of executable statements. |
|
214 * A list of line numbers of excluded statements. |
|
215 * A list of line numbers of statements not run (missing from execution). |
|
216 * A readable formatted string of the missing line numbers. |
|
217 |
|
218 The analysis uses the source file itself and the current measured |
|
219 coverage data. |
|
220 |
|
221 """ |
|
222 code_unit = code_unit_factory(morf, self.file_locator)[0] |
|
223 st, ex, m, mf = self._analyze(code_unit) |
|
224 return code_unit.filename, st, ex, m, mf |
|
225 |
|
226 def _analyze(self, code_unit): |
|
227 """Analyze a single code unit. |
|
228 |
|
229 Returns a 4-tuple: (statements, excluded, missing, missing formatted). |
|
230 |
|
231 """ |
|
232 from .parser import CodeParser |
|
233 |
|
234 filename = code_unit.filename |
|
235 ext = os.path.splitext(filename)[1] |
|
236 source = None |
|
237 if ext == '.py': |
|
238 if not os.path.exists(filename): |
|
239 source = self.file_locator.get_zip_data(filename) |
|
240 if not source: |
|
241 raise CoverageException( |
|
242 "No source for code '%s'." % code_unit.filename |
|
243 ) |
|
244 |
|
245 parser = CodeParser() |
|
246 statements, excluded, line_map = parser.parse_source( |
|
247 text=source, filename=filename, exclude=self.exclude_re |
|
248 ) |
|
249 |
|
250 # Identify missing statements. |
|
251 missing = [] |
|
252 execed = self.data.executed_lines(filename) |
|
253 for line in statements: |
|
254 lines = line_map.get(line) |
|
255 if lines: |
|
256 for l in range(lines[0], lines[1]+1): |
|
257 if l in execed: |
|
258 break |
|
259 else: |
|
260 missing.append(line) |
|
261 else: |
|
262 if line not in execed: |
|
263 missing.append(line) |
|
264 |
|
265 return ( |
|
266 statements, excluded, missing, format_lines(statements, missing) |
|
267 ) |
|
268 |
|
269 def report(self, morfs=None, show_missing=True, ignore_errors=False, |
|
270 file=None, omit_prefixes=None): # pylint: disable-msg=W0622 |
|
271 """Write a summary report to `file`. |
|
272 |
|
273 Each module in `morfs` is listed, with counts of statements, executed |
|
274 statements, missing statements, and a list of lines missed. |
|
275 |
|
276 """ |
|
277 reporter = SummaryReporter(self, show_missing, ignore_errors) |
|
278 reporter.report(morfs, outfile=file, omit_prefixes=omit_prefixes) |
|
279 |
|
280 def annotate(self, morfs=None, directory=None, ignore_errors=False, |
|
281 omit_prefixes=None): |
|
282 """Annotate a list of modules. |
|
283 |
|
284 Each module in `morfs` is annotated. The source is written to a new |
|
285 file, named with a ",cover" suffix, with each line prefixed with a |
|
286 marker to indicate the coverage of the line. Covered lines have ">", |
|
287 excluded lines have "-", and missing lines have "!". |
|
288 |
|
289 """ |
|
290 reporter = AnnotateReporter(self, ignore_errors) |
|
291 reporter.report( |
|
292 morfs, directory=directory, omit_prefixes=omit_prefixes) |
|
293 |
|
294 def html_report(self, morfs=None, directory=None, ignore_errors=False, |
|
295 omit_prefixes=None): |
|
296 """Generate an HTML report. |
|
297 |
|
298 """ |
|
299 reporter = HtmlReporter(self, ignore_errors) |
|
300 reporter.report( |
|
301 morfs, directory=directory, omit_prefixes=omit_prefixes) |