1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
3 |
3 |
4 """Core control stuff for coverage.py.""" |
4 """Core control stuff for coverage.py.""" |
5 |
5 |
|
6 |
6 import atexit |
7 import atexit |
7 import inspect |
8 import inspect |
|
9 import itertools |
8 import os |
10 import os |
9 import platform |
11 import platform |
10 import re |
12 import re |
11 import sys |
13 import sys |
|
14 import time |
12 import traceback |
15 import traceback |
13 |
16 |
14 from coverage import env, files |
17 from coverage import env |
15 from coverage.annotate import AnnotateReporter |
18 from coverage.annotate import AnnotateReporter |
16 from coverage.backward import string_class, iitems |
19 from coverage.backward import string_class, iitems |
17 from coverage.collector import Collector |
20 from coverage.collector import Collector |
18 from coverage.config import CoverageConfig |
21 from coverage.config import read_coverage_config |
19 from coverage.data import CoverageData, CoverageDataFiles |
22 from coverage.data import CoverageData, CoverageDataFiles |
20 from coverage.debug import DebugControl |
23 from coverage.debug import DebugControl, write_formatted_info |
21 from coverage.files import TreeMatcher, FnmatchMatcher |
24 from coverage.files import TreeMatcher, FnmatchMatcher |
22 from coverage.files import PathAliases, find_python_files, prep_patterns |
25 from coverage.files import PathAliases, find_python_files, prep_patterns |
|
26 from coverage.files import canonical_filename, set_relative_directory |
23 from coverage.files import ModuleMatcher, abs_file |
27 from coverage.files import ModuleMatcher, abs_file |
24 from coverage.html import HtmlReporter |
28 from coverage.html import HtmlReporter |
25 from coverage.misc import CoverageException, bool_or_none, join_regex |
29 from coverage.misc import CoverageException, bool_or_none, join_regex |
26 from coverage.misc import file_be_gone, isolate_module |
30 from coverage.misc import file_be_gone, isolate_module |
27 from coverage.monkey import patch_multiprocessing |
|
28 from coverage.plugin import FileReporter |
31 from coverage.plugin import FileReporter |
29 from coverage.plugin_support import Plugins |
32 from coverage.plugin_support import Plugins |
30 from coverage.python import PythonFileReporter |
33 from coverage.python import PythonFileReporter, source_for_file |
31 from coverage.results import Analysis, Numbers |
34 from coverage.results import Analysis, Numbers |
32 from coverage.summary import SummaryReporter |
35 from coverage.summary import SummaryReporter |
33 from coverage.xmlreport import XmlReporter |
36 from coverage.xmlreport import XmlReporter |
34 |
37 |
|
38 try: |
|
39 from coverage.multiproc import patch_multiprocessing |
|
40 except ImportError: # pragma: only jython |
|
41 # Jython has no multiprocessing module. |
|
42 patch_multiprocessing = None |
|
43 |
35 os = isolate_module(os) |
44 os = isolate_module(os) |
36 |
45 |
37 # Pypy has some unusual stuff in the "stdlib". Consider those locations |
46 # Pypy has some unusual stuff in the "stdlib". Consider those locations |
38 # when deciding where the stdlib is. |
47 # when deciding where the stdlib is. These modules are not used for anything, |
39 try: |
48 # they are modules importable from the pypy lib directories, so that we can |
40 import _structseq |
49 # find those directories. |
41 except ImportError: |
50 _structseq = _pypy_irc_topic = None |
42 _structseq = None |
51 if env.PYPY: |
|
52 try: |
|
53 import _structseq |
|
54 except ImportError: |
|
55 pass |
|
56 |
|
57 try: |
|
58 import _pypy_irc_topic |
|
59 except ImportError: |
|
60 pass |
43 |
61 |
44 |
62 |
45 class Coverage(object): |
63 class Coverage(object): |
46 """Programmatic access to coverage.py. |
64 """Programmatic access to coverage.py. |
47 |
65 |
108 `debug` is a list of strings indicating what debugging information is |
126 `debug` is a list of strings indicating what debugging information is |
109 desired. |
127 desired. |
110 |
128 |
111 `concurrency` is a string indicating the concurrency library being used |
129 `concurrency` is a string indicating the concurrency library being used |
112 in the measured code. Without this, coverage.py will get incorrect |
130 in the measured code. Without this, coverage.py will get incorrect |
113 results. Valid strings are "greenlet", "eventlet", "gevent", |
131 results if these libraries are in use. Valid strings are "greenlet", |
114 "multiprocessing", or "thread" (the default). |
132 "eventlet", "gevent", "multiprocessing", or "thread" (the default). |
|
133 This can also be a list of these strings. |
115 |
134 |
116 .. versionadded:: 4.0 |
135 .. versionadded:: 4.0 |
117 The `concurrency` parameter. |
136 The `concurrency` parameter. |
118 |
137 |
119 """ |
138 .. versionadded:: 4.2 |
120 # Build our configuration from a number of sources: |
139 The `concurrency` parameter can now be a list of strings. |
121 # 1: defaults: |
140 |
122 self.config = CoverageConfig() |
141 """ |
123 |
142 # Build our configuration from a number of sources. |
124 # 2: from the rcfile, .coveragerc or setup.cfg file: |
143 self.config_file, self.config = read_coverage_config( |
125 if config_file: |
144 config_file=config_file, |
126 did_read_rc = False |
|
127 # Some API users were specifying ".coveragerc" to mean the same as |
|
128 # True, so make it so. |
|
129 if config_file == ".coveragerc": |
|
130 config_file = True |
|
131 specified_file = (config_file is not True) |
|
132 if not specified_file: |
|
133 config_file = ".coveragerc" |
|
134 |
|
135 did_read_rc = self.config.from_file(config_file) |
|
136 |
|
137 if not did_read_rc: |
|
138 if specified_file: |
|
139 raise CoverageException( |
|
140 "Couldn't read '%s' as a config file" % config_file |
|
141 ) |
|
142 self.config.from_file("setup.cfg", section_prefix="coverage:") |
|
143 |
|
144 # 3: from environment variables: |
|
145 env_data_file = os.environ.get('COVERAGE_FILE') |
|
146 if env_data_file: |
|
147 self.config.data_file = env_data_file |
|
148 debugs = os.environ.get('COVERAGE_DEBUG') |
|
149 if debugs: |
|
150 self.config.debug.extend(debugs.split(",")) |
|
151 |
|
152 # 4: from constructor arguments: |
|
153 self.config.from_args( |
|
154 data_file=data_file, cover_pylib=cover_pylib, timid=timid, |
145 data_file=data_file, cover_pylib=cover_pylib, timid=timid, |
155 branch=branch, parallel=bool_or_none(data_suffix), |
146 branch=branch, parallel=bool_or_none(data_suffix), |
156 source=source, omit=omit, include=include, debug=debug, |
147 source=source, run_omit=omit, run_include=include, debug=debug, |
|
148 report_omit=omit, report_include=include, |
157 concurrency=concurrency, |
149 concurrency=concurrency, |
158 ) |
150 ) |
159 |
151 |
|
152 # This is injectable by tests. |
160 self._debug_file = None |
153 self._debug_file = None |
161 self._auto_data = auto_data |
154 |
|
155 self._auto_load = self._auto_save = auto_data |
162 self._data_suffix = data_suffix |
156 self._data_suffix = data_suffix |
163 |
157 |
164 # The matchers for _should_trace. |
158 # The matchers for _should_trace. |
165 self.source_match = None |
159 self.source_match = None |
166 self.source_pkgs_match = None |
160 self.source_pkgs_match = None |
174 # A record of all the warnings that have been issued. |
168 # A record of all the warnings that have been issued. |
175 self._warnings = [] |
169 self._warnings = [] |
176 |
170 |
177 # Other instance attributes, set later. |
171 # Other instance attributes, set later. |
178 self.omit = self.include = self.source = None |
172 self.omit = self.include = self.source = None |
|
173 self.source_pkgs_unmatched = None |
179 self.source_pkgs = None |
174 self.source_pkgs = None |
180 self.data = self.data_files = self.collector = None |
175 self.data = self.data_files = self.collector = None |
181 self.plugins = None |
176 self.plugins = None |
182 self.pylib_dirs = self.cover_dirs = None |
177 self.pylib_paths = self.cover_paths = None |
183 self.data_suffix = self.run_suffix = None |
178 self.data_suffix = self.run_suffix = None |
184 self._exclude_re = None |
179 self._exclude_re = None |
185 self.debug = None |
180 self.debug = None |
186 |
181 |
187 # State machine variables: |
182 # State machine variables: |
188 # Have we initialized everything? |
183 # Have we initialized everything? |
189 self._inited = False |
184 self._inited = False |
190 # Have we started collecting and not stopped it? |
185 # Have we started collecting and not stopped it? |
191 self._started = False |
186 self._started = False |
192 # Have we measured some data and not harvested it? |
187 |
193 self._measured = False |
188 # If we have sub-process measurement happening automatically, then we |
|
189 # want any explicit creation of a Coverage object to mean, this process |
|
190 # is already coverage-aware, so don't auto-measure it. By now, the |
|
191 # auto-creation of a Coverage object has already happened. But we can |
|
192 # find it and tell it not to save its data. |
|
193 if not env.METACOV: |
|
194 _prevent_sub_process_measurement() |
194 |
195 |
195 def _init(self): |
196 def _init(self): |
196 """Set all the initial state. |
197 """Set all the initial state. |
197 |
198 |
198 This is called by the public methods to initialize state. This lets us |
199 This is called by the public methods to initialize state. This lets us |
212 self._debug_file = open(debug_file_name, "a") |
215 self._debug_file = open(debug_file_name, "a") |
213 else: |
216 else: |
214 self._debug_file = sys.stderr |
217 self._debug_file = sys.stderr |
215 self.debug = DebugControl(self.config.debug, self._debug_file) |
218 self.debug = DebugControl(self.config.debug, self._debug_file) |
216 |
219 |
|
220 # _exclude_re is a dict that maps exclusion list names to compiled regexes. |
|
221 self._exclude_re = {} |
|
222 |
|
223 set_relative_directory() |
|
224 |
217 # Load plugins |
225 # Load plugins |
218 self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug) |
226 self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug) |
219 |
227 |
220 # _exclude_re is a dict that maps exclusion list names to compiled |
228 # Run configuring plugins. |
221 # regexes. |
229 for plugin in self.plugins.configurers: |
222 self._exclude_re = {} |
230 # We need an object with set_option and get_option. Either self or |
223 self._exclude_regex_stale() |
231 # self.config will do. Choosing randomly stops people from doing |
224 |
232 # other things with those objects, against the public API. Yes, |
225 files.set_relative_directory() |
233 # this is a bit childish. :) |
|
234 plugin.configure([self, self.config][int(time.time()) % 2]) |
226 |
235 |
227 # The source argument can be directories or package names. |
236 # The source argument can be directories or package names. |
228 self.source = [] |
237 self.source = [] |
229 self.source_pkgs = [] |
238 self.source_pkgs = [] |
230 for src in self.config.source or []: |
239 for src in self.config.source or []: |
231 if os.path.exists(src): |
240 if os.path.isdir(src): |
232 self.source.append(files.canonical_filename(src)) |
241 self.source.append(canonical_filename(src)) |
233 else: |
242 else: |
234 self.source_pkgs.append(src) |
243 self.source_pkgs.append(src) |
235 |
244 self.source_pkgs_unmatched = self.source_pkgs[:] |
236 self.omit = prep_patterns(self.config.omit) |
245 |
237 self.include = prep_patterns(self.config.include) |
246 self.omit = prep_patterns(self.config.run_omit) |
238 |
247 self.include = prep_patterns(self.config.run_include) |
239 concurrency = self.config.concurrency |
248 |
240 if concurrency == "multiprocessing": |
249 concurrency = self.config.concurrency or [] |
241 patch_multiprocessing() |
250 if "multiprocessing" in concurrency: |
242 concurrency = None |
251 if not patch_multiprocessing: |
|
252 raise CoverageException( # pragma: only jython |
|
253 "multiprocessing is not supported on this Python" |
|
254 ) |
|
255 patch_multiprocessing(rcfile=self.config_file) |
|
256 # Multi-processing uses parallel for the subprocesses, so also use |
|
257 # it for the main process. |
|
258 self.config.parallel = True |
243 |
259 |
244 self.collector = Collector( |
260 self.collector = Collector( |
245 should_trace=self._should_trace, |
261 should_trace=self._should_trace, |
246 check_include=self._check_include_omit_etc, |
262 check_include=self._check_include_omit_etc, |
247 timid=self.config.timid, |
263 timid=self.config.timid, |
279 |
295 |
280 # Create the data file. We do this at construction time so that the |
296 # Create the data file. We do this at construction time so that the |
281 # data file will be written into the directory where the process |
297 # data file will be written into the directory where the process |
282 # started rather than wherever the process eventually chdir'd to. |
298 # started rather than wherever the process eventually chdir'd to. |
283 self.data = CoverageData(debug=self.debug) |
299 self.data = CoverageData(debug=self.debug) |
284 self.data_files = CoverageDataFiles(basename=self.config.data_file, warn=self._warn) |
300 self.data_files = CoverageDataFiles( |
|
301 basename=self.config.data_file, warn=self._warn, debug=self.debug, |
|
302 ) |
285 |
303 |
286 # The directories for files considered "installed with the interpreter". |
304 # The directories for files considered "installed with the interpreter". |
287 self.pylib_dirs = set() |
305 self.pylib_paths = set() |
288 if not self.config.cover_pylib: |
306 if not self.config.cover_pylib: |
289 # Look at where some standard modules are located. That's the |
307 # Look at where some standard modules are located. That's the |
290 # indication for "installed with the interpreter". In some |
308 # indication for "installed with the interpreter". In some |
291 # environments (virtualenv, for example), these modules may be |
309 # environments (virtualenv, for example), these modules may be |
292 # spread across a few locations. Look at all the candidate modules |
310 # spread across a few locations. Look at all the candidate modules |
293 # we've imported, and take all the different ones. |
311 # we've imported, and take all the different ones. |
294 for m in (atexit, inspect, os, platform, re, _structseq, traceback): |
312 for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback): |
295 if m is not None and hasattr(m, "__file__"): |
313 if m is not None and hasattr(m, "__file__"): |
296 self.pylib_dirs.add(self._canonical_dir(m)) |
314 self.pylib_paths.add(self._canonical_path(m, directory=True)) |
|
315 |
297 if _structseq and not hasattr(_structseq, '__file__'): |
316 if _structseq and not hasattr(_structseq, '__file__'): |
298 # PyPy 2.4 has no __file__ in the builtin modules, but the code |
317 # PyPy 2.4 has no __file__ in the builtin modules, but the code |
299 # objects still have the file names. So dig into one to find |
318 # objects still have the file names. So dig into one to find |
300 # the path to exclude. |
319 # the path to exclude. |
301 structseq_new = _structseq.structseq_new |
320 structseq_new = _structseq.structseq_new |
302 try: |
321 try: |
303 structseq_file = structseq_new.func_code.co_filename |
322 structseq_file = structseq_new.func_code.co_filename |
304 except AttributeError: |
323 except AttributeError: |
305 structseq_file = structseq_new.__code__.co_filename |
324 structseq_file = structseq_new.__code__.co_filename |
306 self.pylib_dirs.add(self._canonical_dir(structseq_file)) |
325 self.pylib_paths.add(self._canonical_path(structseq_file)) |
307 |
326 |
308 # To avoid tracing the coverage.py code itself, we skip anything |
327 # To avoid tracing the coverage.py code itself, we skip anything |
309 # located where we are. |
328 # located where we are. |
310 self.cover_dirs = [self._canonical_dir(__file__)] |
329 self.cover_paths = [self._canonical_path(__file__, directory=True)] |
311 if env.TESTING: |
330 if env.TESTING: |
|
331 # Don't include our own test code. |
|
332 self.cover_paths.append(os.path.join(self.cover_paths[0], "tests")) |
|
333 |
312 # When testing, we use PyContracts, which should be considered |
334 # When testing, we use PyContracts, which should be considered |
313 # part of coverage.py, and it uses six. Exclude those directories |
335 # part of coverage.py, and it uses six. Exclude those directories |
314 # just as we exclude ourselves. |
336 # just as we exclude ourselves. |
315 import contracts, six |
337 import contracts |
|
338 import six |
316 for mod in [contracts, six]: |
339 for mod in [contracts, six]: |
317 self.cover_dirs.append(self._canonical_dir(mod)) |
340 self.cover_paths.append(self._canonical_path(mod)) |
318 |
341 |
319 # Set the reporting precision. |
342 # Set the reporting precision. |
320 Numbers.set_precision(self.config.precision) |
343 Numbers.set_precision(self.config.precision) |
321 |
344 |
322 atexit.register(self._atexit) |
345 atexit.register(self._atexit) |
323 |
|
324 self._inited = True |
|
325 |
346 |
326 # Create the matchers we need for _should_trace |
347 # Create the matchers we need for _should_trace |
327 if self.source or self.source_pkgs: |
348 if self.source or self.source_pkgs: |
328 self.source_match = TreeMatcher(self.source) |
349 self.source_match = TreeMatcher(self.source) |
329 self.source_pkgs_match = ModuleMatcher(self.source_pkgs) |
350 self.source_pkgs_match = ModuleMatcher(self.source_pkgs) |
330 else: |
351 else: |
331 if self.cover_dirs: |
352 if self.cover_paths: |
332 self.cover_match = TreeMatcher(self.cover_dirs) |
353 self.cover_match = TreeMatcher(self.cover_paths) |
333 if self.pylib_dirs: |
354 if self.pylib_paths: |
334 self.pylib_match = TreeMatcher(self.pylib_dirs) |
355 self.pylib_match = TreeMatcher(self.pylib_paths) |
335 if self.include: |
356 if self.include: |
336 self.include_match = FnmatchMatcher(self.include) |
357 self.include_match = FnmatchMatcher(self.include) |
337 if self.omit: |
358 if self.omit: |
338 self.omit_match = FnmatchMatcher(self.omit) |
359 self.omit_match = FnmatchMatcher(self.omit) |
339 |
360 |
340 # The user may want to debug things, show info if desired. |
361 # The user may want to debug things, show info if desired. |
|
362 self._write_startup_debug() |
|
363 |
|
364 def _write_startup_debug(self): |
|
365 """Write out debug info at startup if needed.""" |
341 wrote_any = False |
366 wrote_any = False |
342 if self.debug.should('config'): |
367 with self.debug.without_callers(): |
343 config_info = sorted(self.config.__dict__.items()) |
368 if self.debug.should('config'): |
344 self.debug.write_formatted_info("config", config_info) |
369 config_info = sorted(self.config.__dict__.items()) |
345 wrote_any = True |
370 write_formatted_info(self.debug, "config", config_info) |
346 |
371 wrote_any = True |
347 if self.debug.should('sys'): |
372 |
348 self.debug.write_formatted_info("sys", self.sys_info()) |
373 if self.debug.should('sys'): |
349 for plugin in self.plugins: |
374 write_formatted_info(self.debug, "sys", self.sys_info()) |
350 header = "sys: " + plugin._coverage_plugin_name |
375 for plugin in self.plugins: |
351 info = plugin.sys_info() |
376 header = "sys: " + plugin._coverage_plugin_name |
352 self.debug.write_formatted_info(header, info) |
377 info = plugin.sys_info() |
353 wrote_any = True |
378 write_formatted_info(self.debug, header, info) |
|
379 wrote_any = True |
354 |
380 |
355 if wrote_any: |
381 if wrote_any: |
356 self.debug.write_formatted_info("end", ()) |
382 write_formatted_info(self.debug, "end", ()) |
357 |
383 |
358 def _canonical_dir(self, morf): |
384 def _canonical_path(self, morf, directory=False): |
359 """Return the canonical directory of the module or file `morf`.""" |
385 """Return the canonical path of the module or file `morf`. |
360 morf_filename = PythonFileReporter(morf, self).filename |
386 |
361 return os.path.split(morf_filename)[0] |
387 If the module is a package, then return its directory. If it is a |
362 |
388 module, then return its file, unless `directory` is True, in which |
363 def _source_for_file(self, filename): |
389 case return its enclosing directory. |
364 """Return the source file for `filename`. |
390 |
365 |
391 """ |
366 Given a file name being traced, return the best guess as to the source |
392 morf_path = PythonFileReporter(morf, self).filename |
367 file to attribute it to. |
393 if morf_path.endswith("__init__.py") or directory: |
368 |
394 morf_path = os.path.split(morf_path)[0] |
369 """ |
395 return morf_path |
370 if filename.endswith(".py"): |
|
371 # .py files are themselves source files. |
|
372 return filename |
|
373 |
|
374 elif filename.endswith((".pyc", ".pyo")): |
|
375 # Bytecode files probably have source files near them. |
|
376 py_filename = filename[:-1] |
|
377 if os.path.exists(py_filename): |
|
378 # Found a .py file, use that. |
|
379 return py_filename |
|
380 if env.WINDOWS: |
|
381 # On Windows, it could be a .pyw file. |
|
382 pyw_filename = py_filename + "w" |
|
383 if os.path.exists(pyw_filename): |
|
384 return pyw_filename |
|
385 # Didn't find source, but it's probably the .py file we want. |
|
386 return py_filename |
|
387 |
|
388 elif filename.endswith("$py.class"): |
|
389 # Jython is easy to guess. |
|
390 return filename[:-9] + ".py" |
|
391 |
|
392 # No idea, just use the file name as-is. |
|
393 return filename |
|
394 |
396 |
395 def _name_for_module(self, module_globals, filename): |
397 def _name_for_module(self, module_globals, filename): |
396 """Get the name of the module for a set of globals and file name. |
398 """Get the name of the module for a set of globals and file name. |
397 |
399 |
398 For configurability's sake, we allow __main__ modules to be matched by |
400 For configurability's sake, we allow __main__ modules to be matched by |
664 self.data_files.read(self.data) |
677 self.data_files.read(self.data) |
665 |
678 |
666 def start(self): |
679 def start(self): |
667 """Start measuring code coverage. |
680 """Start measuring code coverage. |
668 |
681 |
669 Coverage measurement actually occurs in functions called after |
682 Coverage measurement only occurs in functions called after |
670 :meth:`start` is invoked. Statements in the same scope as |
683 :meth:`start` is invoked. Statements in the same scope as |
671 :meth:`start` won't be measured. |
684 :meth:`start` won't be measured. |
672 |
685 |
673 Once you invoke :meth:`start`, you must also call :meth:`stop` |
686 Once you invoke :meth:`start`, you must also call :meth:`stop` |
674 eventually, or your process might not shut down cleanly. |
687 eventually, or your process might not shut down cleanly. |
675 |
688 |
676 """ |
689 """ |
677 self._init() |
690 self._init() |
|
691 if self.include: |
|
692 if self.source or self.source_pkgs: |
|
693 self._warn("--include is ignored because --source is set", slug="include-ignored") |
678 if self.run_suffix: |
694 if self.run_suffix: |
679 # Calling start() means we're running code, so use the run_suffix |
695 # Calling start() means we're running code, so use the run_suffix |
680 # as the data_suffix when we eventually save the data. |
696 # as the data_suffix when we eventually save the data. |
681 self.data_suffix = self.run_suffix |
697 self.data_suffix = self.run_suffix |
682 if self._auto_data: |
698 if self._auto_load: |
683 self.load() |
699 self.load() |
684 |
700 |
685 self.collector.start() |
701 self.collector.start() |
686 self._started = True |
702 self._started = True |
687 self._measured = True |
|
688 |
703 |
689 def stop(self): |
704 def stop(self): |
690 """Stop measuring code coverage.""" |
705 """Stop measuring code coverage.""" |
691 if self._started: |
706 if self._started: |
692 self.collector.stop() |
707 self.collector.stop() |
693 self._started = False |
708 self._started = False |
694 |
709 |
695 def _atexit(self): |
710 def _atexit(self): |
696 """Clean up on process shutdown.""" |
711 """Clean up on process shutdown.""" |
|
712 if self.debug.should("process"): |
|
713 self.debug.write("atexit: {0!r}".format(self)) |
697 if self._started: |
714 if self._started: |
698 self.stop() |
715 self.stop() |
699 if self._auto_data: |
716 if self._auto_save: |
700 self.save() |
717 self.save() |
701 |
718 |
702 def erase(self): |
719 def erase(self): |
703 """Erase previously-collected coverage data. |
720 """Erase previously-collected coverage data. |
704 |
721 |
789 for paths in self.config.paths.values(): |
812 for paths in self.config.paths.values(): |
790 result = paths[0] |
813 result = paths[0] |
791 for pattern in paths[1:]: |
814 for pattern in paths[1:]: |
792 aliases.add(pattern, result) |
815 aliases.add(pattern, result) |
793 |
816 |
794 self.data_files.combine_parallel_data(self.data, aliases=aliases, data_paths=data_paths) |
817 self.data_files.combine_parallel_data( |
|
818 self.data, aliases=aliases, data_paths=data_paths, strict=strict, |
|
819 ) |
795 |
820 |
796 def get_data(self): |
821 def get_data(self): |
797 """Get the collected data and reset the collector. |
822 """Get the collected data. |
798 |
823 |
799 Also warn about various problems collecting data. |
824 Also warn about various problems collecting data. |
800 |
825 |
801 Returns a :class:`coverage.CoverageData`, the collected coverage data. |
826 Returns a :class:`coverage.CoverageData`, the collected coverage data. |
802 |
827 |
803 .. versionadded:: 4.0 |
828 .. versionadded:: 4.0 |
804 |
829 |
805 """ |
830 """ |
806 self._init() |
831 self._init() |
807 if not self._measured: |
832 |
808 return self.data |
833 if self.collector.save_data(self.data): |
809 |
834 self._post_save_work() |
810 self.collector.save_data(self.data) |
835 |
811 |
836 return self.data |
812 # If there are still entries in the source_pkgs list, then we never |
837 |
813 # encountered those packages. |
838 def _post_save_work(self): |
|
839 """After saving data, look for warnings, post-work, etc. |
|
840 |
|
841 Warn about things that should have happened but didn't. |
|
842 Look for unexecuted files. |
|
843 |
|
844 """ |
|
845 # If there are still entries in the source_pkgs_unmatched list, |
|
846 # then we never encountered those packages. |
814 if self._warn_unimported_source: |
847 if self._warn_unimported_source: |
815 for pkg in self.source_pkgs: |
848 for pkg in self.source_pkgs_unmatched: |
816 if pkg not in sys.modules: |
849 self._warn_about_unmeasured_code(pkg) |
817 self._warn("Module %s was never imported." % pkg) |
|
818 elif not ( |
|
819 hasattr(sys.modules[pkg], '__file__') and |
|
820 os.path.exists(sys.modules[pkg].__file__) |
|
821 ): |
|
822 self._warn("Module %s has no Python source." % pkg) |
|
823 else: |
|
824 self._warn("Module %s was previously imported, but not measured." % pkg) |
|
825 |
850 |
826 # Find out if we got any data. |
851 # Find out if we got any data. |
827 if not self.data and self._warn_no_data: |
852 if not self.data and self._warn_no_data: |
828 self._warn("No data was collected.") |
853 self._warn("No data was collected.", slug="no-data-collected") |
829 |
854 |
830 # Find files that were never executed at all. |
855 # Find files that were never executed at all. |
|
856 for pkg in self.source_pkgs: |
|
857 if (not pkg in sys.modules or |
|
858 not hasattr(sys.modules[pkg], '__file__') or |
|
859 not os.path.exists(sys.modules[pkg].__file__)): |
|
860 continue |
|
861 pkg_file = source_for_file(sys.modules[pkg].__file__) |
|
862 self._find_unexecuted_files(self._canonical_path(pkg_file)) |
|
863 |
831 for src in self.source: |
864 for src in self.source: |
832 for py_file in find_python_files(src): |
865 self._find_unexecuted_files(src) |
833 py_file = files.canonical_filename(py_file) |
|
834 |
|
835 if self.omit_match and self.omit_match.match(py_file): |
|
836 # Turns out this file was omitted, so don't pull it back |
|
837 # in as unexecuted. |
|
838 continue |
|
839 |
|
840 self.data.touch_file(py_file) |
|
841 |
866 |
842 if self.config.note: |
867 if self.config.note: |
843 self.data.add_run_info(note=self.config.note) |
868 self.data.add_run_info(note=self.config.note) |
844 |
869 |
845 self._measured = False |
870 def _warn_about_unmeasured_code(self, pkg): |
846 return self.data |
871 """Warn about a package or module that we never traced. |
|
872 |
|
873 `pkg` is a string, the name of the package or module. |
|
874 |
|
875 """ |
|
876 mod = sys.modules.get(pkg) |
|
877 if mod is None: |
|
878 self._warn("Module %s was never imported." % pkg, slug="module-not-imported") |
|
879 return |
|
880 |
|
881 is_namespace = hasattr(mod, '__path__') and not hasattr(mod, '__file__') |
|
882 has_file = hasattr(mod, '__file__') and os.path.exists(mod.__file__) |
|
883 |
|
884 if is_namespace: |
|
885 # A namespace package. It's OK for this not to have been traced, |
|
886 # since there is no code directly in it. |
|
887 return |
|
888 |
|
889 if not has_file: |
|
890 self._warn("Module %s has no Python source." % pkg, slug="module-not-python") |
|
891 return |
|
892 |
|
893 # The module was in sys.modules, and seems like a module with code, but |
|
894 # we never measured it. I guess that means it was imported before |
|
895 # coverage even started. |
|
896 self._warn( |
|
897 "Module %s was previously imported, but not measured" % pkg, |
|
898 slug="module-not-measured", |
|
899 ) |
|
900 |
|
901 def _find_plugin_files(self, src_dir): |
|
902 """Get executable files from the plugins.""" |
|
903 for plugin in self.plugins.file_tracers: |
|
904 for x_file in plugin.find_executable_files(src_dir): |
|
905 yield x_file, plugin._coverage_plugin_name |
|
906 |
|
907 def _find_unexecuted_files(self, src_dir): |
|
908 """Find unexecuted files in `src_dir`. |
|
909 |
|
910 Search for files in `src_dir` that are probably importable, |
|
911 and add them as unexecuted files in `self.data`. |
|
912 |
|
913 """ |
|
914 py_files = ((py_file, None) for py_file in find_python_files(src_dir)) |
|
915 plugin_files = self._find_plugin_files(src_dir) |
|
916 |
|
917 for file_path, plugin_name in itertools.chain(py_files, plugin_files): |
|
918 file_path = canonical_filename(file_path) |
|
919 if self.omit_match and self.omit_match.match(file_path): |
|
920 # Turns out this file was omitted, so don't pull it back |
|
921 # in as unexecuted. |
|
922 continue |
|
923 self.data.touch_file(file_path, plugin_name) |
847 |
924 |
848 # Backward compatibility with version 1. |
925 # Backward compatibility with version 1. |
849 def analysis(self, morf): |
926 def analysis(self, morf): |
850 """Like `analysis2` but doesn't return excluded line numbers.""" |
927 """Like `analysis2` but doesn't return excluded line numbers.""" |
851 f, s, _, m, mf = self.analysis2(morf) |
928 f, s, _, m, mf = self.analysis2(morf) |
1069 |
1150 |
1070 import coverage as covmod |
1151 import coverage as covmod |
1071 |
1152 |
1072 self._init() |
1153 self._init() |
1073 |
1154 |
1074 ft_plugins = [] |
1155 def plugin_info(plugins): |
1075 for ft in self.plugins.file_tracers: |
1156 """Make an entry for the sys_info from a list of plug-ins.""" |
1076 ft_name = ft._coverage_plugin_name |
1157 entries = [] |
1077 if not ft._coverage_enabled: |
1158 for plugin in plugins: |
1078 ft_name += " (disabled)" |
1159 entry = plugin._coverage_plugin_name |
1079 ft_plugins.append(ft_name) |
1160 if not plugin._coverage_enabled: |
|
1161 entry += " (disabled)" |
|
1162 entries.append(entry) |
|
1163 return entries |
1080 |
1164 |
1081 info = [ |
1165 info = [ |
1082 ('version', covmod.__version__), |
1166 ('version', covmod.__version__), |
1083 ('coverage', covmod.__file__), |
1167 ('coverage', covmod.__file__), |
1084 ('cover_dirs', self.cover_dirs), |
1168 ('cover_paths', self.cover_paths), |
1085 ('pylib_dirs', self.pylib_dirs), |
1169 ('pylib_paths', self.pylib_paths), |
1086 ('tracer', self.collector.tracer_name()), |
1170 ('tracer', self.collector.tracer_name()), |
1087 ('plugins.file_tracers', ft_plugins), |
1171 ('plugins.file_tracers', plugin_info(self.plugins.file_tracers)), |
|
1172 ('plugins.configurers', plugin_info(self.plugins.configurers)), |
1088 ('config_files', self.config.attempted_config_files), |
1173 ('config_files', self.config.attempted_config_files), |
1089 ('configs_read', self.config.config_files), |
1174 ('configs_read', self.config.config_files), |
1090 ('data_path', self.data_files.filename), |
1175 ('data_path', self.data_files.filename), |
1091 ('python', sys.version.replace('\n', '')), |
1176 ('python', sys.version.replace('\n', '')), |
1092 ('platform', platform.platform()), |
1177 ('platform', platform.platform()), |