DebugClients/Python3/coverage/control.py

changeset 0
de9c2efb9d02
child 29
391dc0bc4ae5
equal deleted inserted replaced
-1:000000000000 0:de9c2efb9d02
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)

eric ide

mercurial