9 import os |
9 import os |
10 import os.path |
10 import os.path |
11 import platform |
11 import platform |
12 import sys |
12 import sys |
13 import time |
13 import time |
|
14 import warnings |
14 |
15 |
15 from coverage import env |
16 from coverage import env |
16 from coverage.annotate import AnnotateReporter |
17 from coverage.annotate import AnnotateReporter |
17 from coverage.backward import string_class, iitems |
|
18 from coverage.collector import Collector, CTracer |
18 from coverage.collector import Collector, CTracer |
19 from coverage.config import read_coverage_config |
19 from coverage.config import read_coverage_config |
20 from coverage.context import should_start_context_test_function, combine_context_switchers |
20 from coverage.context import should_start_context_test_function, combine_context_switchers |
21 from coverage.data import CoverageData, combine_parallel_data |
21 from coverage.data import CoverageData, combine_parallel_data |
22 from coverage.debug import DebugControl, short_stack, write_formatted_info |
22 from coverage.debug import DebugControl, short_stack, write_formatted_info |
23 from coverage.disposition import disposition_debug_msg |
23 from coverage.disposition import disposition_debug_msg |
|
24 from coverage.exceptions import CoverageException, CoverageWarning |
24 from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory |
25 from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory |
25 from coverage.html import HtmlReporter |
26 from coverage.html import HtmlReporter |
26 from coverage.inorout import InOrOut |
27 from coverage.inorout import InOrOut |
27 from coverage.jsonreport import JsonReporter |
28 from coverage.jsonreport import JsonReporter |
28 from coverage.misc import CoverageException, bool_or_none, join_regex |
29 from coverage.misc import bool_or_none, join_regex, human_sorted, human_sorted_items |
29 from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module |
30 from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module |
30 from coverage.plugin import FileReporter |
31 from coverage.plugin import FileReporter |
31 from coverage.plugin_support import Plugins |
32 from coverage.plugin_support import Plugins |
32 from coverage.python import PythonFileReporter |
33 from coverage.python import PythonFileReporter |
33 from coverage.report import render_report |
34 from coverage.report import render_report |
34 from coverage.results import Analysis, Numbers |
35 from coverage.results import Analysis |
35 from coverage.summary import SummaryReporter |
36 from coverage.summary import SummaryReporter |
36 from coverage.xmlreport import XmlReporter |
37 from coverage.xmlreport import XmlReporter |
37 |
38 |
38 try: |
39 try: |
39 from coverage.multiproc import patch_multiprocessing |
40 from coverage.multiproc import patch_multiprocessing |
100 def __init__( |
101 def __init__( |
101 self, data_file=_DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None, |
102 self, data_file=_DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None, |
102 auto_data=False, timid=None, branch=None, config_file=True, |
103 auto_data=False, timid=None, branch=None, config_file=True, |
103 source=None, source_pkgs=None, omit=None, include=None, debug=None, |
104 source=None, source_pkgs=None, omit=None, include=None, debug=None, |
104 concurrency=None, check_preimported=False, context=None, |
105 concurrency=None, check_preimported=False, context=None, |
|
106 messages=False, |
105 ): # pylint: disable=too-many-arguments |
107 ): # pylint: disable=too-many-arguments |
106 """ |
108 """ |
107 Many of these arguments duplicate and override values that can be |
109 Many of these arguments duplicate and override values that can be |
108 provided in a configuration file. Parameters that are missing here |
110 provided in a configuration file. Parameters that are missing here |
109 will use values from the config file. |
111 will use values from the config file. |
170 started can mean that code is missed. |
172 started can mean that code is missed. |
171 |
173 |
172 `context` is a string to use as the :ref:`static context |
174 `context` is a string to use as the :ref:`static context |
173 <static_contexts>` label for collected data. |
175 <static_contexts>` label for collected data. |
174 |
176 |
|
177 If `messages` is true, some messages will be printed to stdout |
|
178 indicating what is happening. |
|
179 |
175 .. versionadded:: 4.0 |
180 .. versionadded:: 4.0 |
176 The `concurrency` parameter. |
181 The `concurrency` parameter. |
177 |
182 |
178 .. versionadded:: 4.2 |
183 .. versionadded:: 4.2 |
179 The `concurrency` parameter can now be a list of strings. |
184 The `concurrency` parameter can now be a list of strings. |
181 .. versionadded:: 5.0 |
186 .. versionadded:: 5.0 |
182 The `check_preimported` and `context` parameters. |
187 The `check_preimported` and `context` parameters. |
183 |
188 |
184 .. versionadded:: 5.3 |
189 .. versionadded:: 5.3 |
185 The `source_pkgs` parameter. |
190 The `source_pkgs` parameter. |
|
191 |
|
192 .. versionadded:: 6.0 |
|
193 The `messages` parameter. |
186 |
194 |
187 """ |
195 """ |
188 # data_file=None means no disk file at all. data_file missing means |
196 # data_file=None means no disk file at all. data_file missing means |
189 # use the value from the config file. |
197 # use the value from the config file. |
190 self._no_disk = data_file is None |
198 self._no_disk = data_file is None |
191 if data_file is _DEFAULT_DATAFILE: |
199 if data_file is _DEFAULT_DATAFILE: |
192 data_file = None |
200 data_file = None |
193 |
201 |
194 # Build our configuration from a number of sources. |
202 self.config = None |
195 self.config = read_coverage_config( |
|
196 config_file=config_file, |
|
197 data_file=data_file, cover_pylib=cover_pylib, timid=timid, |
|
198 branch=branch, parallel=bool_or_none(data_suffix), |
|
199 source=source, source_pkgs=source_pkgs, run_omit=omit, run_include=include, debug=debug, |
|
200 report_omit=omit, report_include=include, |
|
201 concurrency=concurrency, context=context, |
|
202 ) |
|
203 |
203 |
204 # This is injectable by tests. |
204 # This is injectable by tests. |
205 self._debug_file = None |
205 self._debug_file = None |
206 |
206 |
207 self._auto_load = self._auto_save = auto_data |
207 self._auto_load = self._auto_save = auto_data |
210 # Is it ok for no data to be collected? |
210 # Is it ok for no data to be collected? |
211 self._warn_no_data = True |
211 self._warn_no_data = True |
212 self._warn_unimported_source = True |
212 self._warn_unimported_source = True |
213 self._warn_preimported_source = check_preimported |
213 self._warn_preimported_source = check_preimported |
214 self._no_warn_slugs = None |
214 self._no_warn_slugs = None |
|
215 self._messages = messages |
215 |
216 |
216 # A record of all the warnings that have been issued. |
217 # A record of all the warnings that have been issued. |
217 self._warnings = [] |
218 self._warnings = [] |
218 |
219 |
219 # Other instance attributes, set later. |
220 # Other instance attributes, set later. |
232 # Have we started collecting and not stopped it? |
233 # Have we started collecting and not stopped it? |
233 self._started = False |
234 self._started = False |
234 # Should we write the debug output? |
235 # Should we write the debug output? |
235 self._should_write_debug = True |
236 self._should_write_debug = True |
236 |
237 |
|
238 # Build our configuration from a number of sources. |
|
239 self.config = read_coverage_config( |
|
240 config_file=config_file, warn=self._warn, |
|
241 data_file=data_file, cover_pylib=cover_pylib, timid=timid, |
|
242 branch=branch, parallel=bool_or_none(data_suffix), |
|
243 source=source, source_pkgs=source_pkgs, run_omit=omit, run_include=include, debug=debug, |
|
244 report_omit=omit, report_include=include, |
|
245 concurrency=concurrency, context=context, |
|
246 ) |
|
247 |
237 # If we have sub-process measurement happening automatically, then we |
248 # If we have sub-process measurement happening automatically, then we |
238 # want any explicit creation of a Coverage object to mean, this process |
249 # want any explicit creation of a Coverage object to mean, this process |
239 # is already coverage-aware, so don't auto-measure it. By now, the |
250 # is already coverage-aware, so don't auto-measure it. By now, the |
240 # auto-creation of a Coverage object has already happened. But we can |
251 # auto-creation of a Coverage object has already happened. But we can |
241 # find it and tell it not to save its data. |
252 # find it and tell it not to save its data. |
289 self._write_startup_debug() |
300 self._write_startup_debug() |
290 |
301 |
291 # '[run] _crash' will raise an exception if the value is close by in |
302 # '[run] _crash' will raise an exception if the value is close by in |
292 # the call stack, for testing error handling. |
303 # the call stack, for testing error handling. |
293 if self.config._crash and self.config._crash in short_stack(limit=4): |
304 if self.config._crash and self.config._crash in short_stack(limit=4): |
294 raise Exception("Crashing because called by {}".format(self.config._crash)) |
305 raise Exception(f"Crashing because called by {self.config._crash}") |
295 |
306 |
296 def _write_startup_debug(self): |
307 def _write_startup_debug(self): |
297 """Write out debug info at startup if needed.""" |
308 """Write out debug info at startup if needed.""" |
298 wrote_any = False |
309 wrote_any = False |
299 with self._debug.without_callers(): |
310 with self._debug.without_callers(): |
300 if self._debug.should('config'): |
311 if self._debug.should('config'): |
301 config_info = sorted(self.config.__dict__.items()) |
312 config_info = human_sorted_items(self.config.__dict__.items()) |
302 config_info = [(k, v) for k, v in config_info if not k.startswith('_')] |
313 config_info = [(k, v) for k, v in config_info if not k.startswith('_')] |
303 write_formatted_info(self._debug, "config", config_info) |
314 write_formatted_info(self._debug, "config", config_info) |
304 wrote_any = True |
315 wrote_any = True |
305 |
316 |
306 if self._debug.should('sys'): |
317 if self._debug.should('sys'): |
332 |
343 |
333 """ |
344 """ |
334 reason = self._inorout.check_include_omit_etc(filename, frame) |
345 reason = self._inorout.check_include_omit_etc(filename, frame) |
335 if self._debug.should('trace'): |
346 if self._debug.should('trace'): |
336 if not reason: |
347 if not reason: |
337 msg = "Including %r" % (filename,) |
348 msg = f"Including {filename!r}" |
338 else: |
349 else: |
339 msg = "Not including %r: %s" % (filename, reason) |
350 msg = f"Not including {filename!r}: {reason}" |
340 self._debug.write(msg) |
351 self._debug.write(msg) |
341 |
352 |
342 return not reason |
353 return not reason |
343 |
354 |
344 def _warn(self, msg, slug=None, once=False): |
355 def _warn(self, msg, slug=None, once=False): |
349 If `once` is true, only show this warning once (determined by the |
360 If `once` is true, only show this warning once (determined by the |
350 slug.) |
361 slug.) |
351 |
362 |
352 """ |
363 """ |
353 if self._no_warn_slugs is None: |
364 if self._no_warn_slugs is None: |
354 self._no_warn_slugs = list(self.config.disable_warnings) |
365 if self.config is not None: |
355 |
366 self._no_warn_slugs = list(self.config.disable_warnings) |
356 if slug in self._no_warn_slugs: |
367 |
357 # Don't issue the warning |
368 if self._no_warn_slugs is not None: |
358 return |
369 if slug in self._no_warn_slugs: |
|
370 # Don't issue the warning |
|
371 return |
359 |
372 |
360 self._warnings.append(msg) |
373 self._warnings.append(msg) |
361 if slug: |
374 if slug: |
362 msg = "%s (%s)" % (msg, slug) |
375 msg = f"{msg} ({slug})" |
363 if self._debug.should('pid'): |
376 if self._debug is not None and self._debug.should('pid'): |
364 msg = "[%d] %s" % (os.getpid(), msg) |
377 msg = f"[{os.getpid()}] {msg}" |
365 sys.stderr.write("Coverage.py warning: %s\n" % msg) |
378 warnings.warn(msg, category=CoverageWarning, stacklevel=2) |
366 |
379 |
367 if once: |
380 if once: |
368 self._no_warn_slugs.append(slug) |
381 self._no_warn_slugs.append(slug) |
|
382 |
|
383 def _message(self, msg): |
|
384 """Write a message to the user, if configured to do so.""" |
|
385 if self._messages: |
|
386 print(msg) |
369 |
387 |
370 def get_option(self, option_name): |
388 def get_option(self, option_name): |
371 """Get an option from the configuration. |
389 """Get an option from the configuration. |
372 |
390 |
373 `option_name` is a colon-separated string indicating the section and |
391 `option_name` is a colon-separated string indicating the section and |
440 if not dycon or dycon == "none": |
458 if not dycon or dycon == "none": |
441 context_switchers = [] |
459 context_switchers = [] |
442 elif dycon == "test_function": |
460 elif dycon == "test_function": |
443 context_switchers = [should_start_context_test_function] |
461 context_switchers = [should_start_context_test_function] |
444 else: |
462 else: |
445 raise CoverageException( |
463 raise CoverageException(f"Don't understand dynamic_context setting: {dycon!r}") |
446 "Don't understand dynamic_context setting: {!r}".format(dycon) |
|
447 ) |
|
448 |
464 |
449 context_switchers.extend( |
465 context_switchers.extend( |
450 plugin.dynamic_context for plugin in self._plugins.context_switchers |
466 plugin.dynamic_context for plugin in self._plugins.context_switchers |
451 ) |
467 ) |
452 |
468 |
476 self._collector.use_data(self._data, self.config.context) |
492 self._collector.use_data(self._data, self.config.context) |
477 |
493 |
478 # Early warning if we aren't going to be able to support plugins. |
494 # Early warning if we aren't going to be able to support plugins. |
479 if self._plugins.file_tracers and not self._collector.supports_plugins: |
495 if self._plugins.file_tracers and not self._collector.supports_plugins: |
480 self._warn( |
496 self._warn( |
481 "Plugin file tracers (%s) aren't supported with %s" % ( |
497 "Plugin file tracers ({}) aren't supported with {}".format( |
482 ", ".join( |
498 ", ".join( |
483 plugin._coverage_plugin_name |
499 plugin._coverage_plugin_name |
484 for plugin in self._plugins.file_tracers |
500 for plugin in self._plugins.file_tracers |
485 ), |
501 ), |
486 self._collector.tracer_name(), |
502 self._collector.tracer_name(), |
596 |
612 |
597 .. versionadded:: 5.0 |
613 .. versionadded:: 5.0 |
598 |
614 |
599 """ |
615 """ |
600 if not self._started: # pragma: part started |
616 if not self._started: # pragma: part started |
601 raise CoverageException( |
617 raise CoverageException("Cannot switch context, coverage is not started") |
602 "Cannot switch context, coverage is not started" |
|
603 ) |
|
604 |
618 |
605 if self._collector.should_start_context: |
619 if self._collector.should_start_context: |
606 self._warn("Conflicting dynamic contexts", slug="dynamic-conflict", once=True) |
620 self._warn("Conflicting dynamic contexts", slug="dynamic-conflict", once=True) |
607 |
621 |
608 self._collector.switch_context(new_context) |
622 self._collector.switch_context(new_context) |
796 Returns an `Analysis` object. |
811 Returns an `Analysis` object. |
797 |
812 |
798 """ |
813 """ |
799 # All reporting comes through here, so do reporting initialization. |
814 # All reporting comes through here, so do reporting initialization. |
800 self._init() |
815 self._init() |
801 Numbers.set_precision(self.config.precision) |
|
802 self._post_init() |
816 self._post_init() |
803 |
817 |
804 data = self.get_data() |
818 data = self.get_data() |
805 if not isinstance(it, FileReporter): |
819 if not isinstance(it, FileReporter): |
806 it = self._get_file_reporter(it) |
820 it = self._get_file_reporter(it) |
807 |
821 |
808 return Analysis(data, it, self._file_mapper) |
822 return Analysis(data, self.config.precision, it, self._file_mapper) |
809 |
823 |
810 def _get_file_reporter(self, morf): |
824 def _get_file_reporter(self, morf): |
811 """Get a FileReporter for a module or file name.""" |
825 """Get a FileReporter for a module or file name.""" |
812 plugin = None |
826 plugin = None |
813 file_reporter = "python" |
827 file_reporter = "python" |
814 |
828 |
815 if isinstance(morf, string_class): |
829 if isinstance(morf, str): |
816 mapped_morf = self._file_mapper(morf) |
830 mapped_morf = self._file_mapper(morf) |
817 plugin_name = self._data.file_tracer(mapped_morf) |
831 plugin_name = self._data.file_tracer(mapped_morf) |
818 if plugin_name: |
832 if plugin_name: |
819 plugin = self._plugins.get(plugin_name) |
833 plugin = self._plugins.get(plugin_name) |
820 |
834 |
821 if plugin: |
835 if plugin: |
822 file_reporter = plugin.file_reporter(mapped_morf) |
836 file_reporter = plugin.file_reporter(mapped_morf) |
823 if file_reporter is None: |
837 if file_reporter is None: |
824 raise CoverageException( |
838 raise CoverageException( |
825 "Plugin %r did not provide a file reporter for %r." % ( |
839 "Plugin {!r} did not provide a file reporter for {!r}.".format( |
826 plugin._coverage_plugin_name, morf |
840 plugin._coverage_plugin_name, morf |
827 ) |
841 ) |
828 ) |
842 ) |
829 |
843 |
830 if file_reporter == "python": |
844 if file_reporter == "python": |
916 self, morfs=None, directory=None, ignore_errors=None, |
930 self, morfs=None, directory=None, ignore_errors=None, |
917 omit=None, include=None, contexts=None, |
931 omit=None, include=None, contexts=None, |
918 ): |
932 ): |
919 """Annotate a list of modules. |
933 """Annotate a list of modules. |
920 |
934 |
|
935 .. note:: |
|
936 This method has been obsoleted by more modern reporting tools, |
|
937 including the :meth:`html_report` method. It will be removed in a |
|
938 future version. |
|
939 |
921 Each module in `morfs` is annotated. The source is written to a new |
940 Each module in `morfs` is annotated. The source is written to a new |
922 file, named with a ",cover" suffix, with each line prefixed with a |
941 file, named with a ",cover" suffix, with each line prefixed with a |
923 marker to indicate the coverage of the line. Covered lines have ">", |
942 marker to indicate the coverage of the line. Covered lines have ">", |
924 excluded lines have "-", and missing lines have "!". |
943 excluded lines have "-", and missing lines have "!". |
925 |
944 |
926 See :meth:`report` for other arguments. |
945 See :meth:`report` for other arguments. |
927 |
946 |
928 """ |
947 """ |
|
948 print("The annotate command will be removed in a future version.") |
|
949 print("Get in touch if you still use it: ned@nedbatchelder.com") |
|
950 |
929 with override_config(self, |
951 with override_config(self, |
930 ignore_errors=ignore_errors, report_omit=omit, |
952 ignore_errors=ignore_errors, report_omit=omit, |
931 report_include=include, report_contexts=contexts, |
953 report_include=include, report_contexts=contexts, |
932 ): |
954 ): |
933 reporter = AnnotateReporter(self) |
955 reporter = AnnotateReporter(self) |
967 html_dir=directory, extra_css=extra_css, html_title=title, |
989 html_dir=directory, extra_css=extra_css, html_title=title, |
968 html_skip_covered=skip_covered, show_contexts=show_contexts, report_contexts=contexts, |
990 html_skip_covered=skip_covered, show_contexts=show_contexts, report_contexts=contexts, |
969 html_skip_empty=skip_empty, precision=precision, |
991 html_skip_empty=skip_empty, precision=precision, |
970 ): |
992 ): |
971 reporter = HtmlReporter(self) |
993 reporter = HtmlReporter(self) |
972 return reporter.report(morfs) |
994 ret = reporter.report(morfs) |
|
995 return ret |
973 |
996 |
974 def xml_report( |
997 def xml_report( |
975 self, morfs=None, outfile=None, ignore_errors=None, |
998 self, morfs=None, outfile=None, ignore_errors=None, |
976 omit=None, include=None, contexts=None, skip_empty=None, |
999 omit=None, include=None, contexts=None, skip_empty=None, |
977 ): |
1000 ): |
989 """ |
1012 """ |
990 with override_config(self, |
1013 with override_config(self, |
991 ignore_errors=ignore_errors, report_omit=omit, report_include=include, |
1014 ignore_errors=ignore_errors, report_omit=omit, report_include=include, |
992 xml_output=outfile, report_contexts=contexts, skip_empty=skip_empty, |
1015 xml_output=outfile, report_contexts=contexts, skip_empty=skip_empty, |
993 ): |
1016 ): |
994 return render_report(self.config.xml_output, XmlReporter(self), morfs) |
1017 return render_report(self.config.xml_output, XmlReporter(self), morfs, self._message) |
995 |
1018 |
996 def json_report( |
1019 def json_report( |
997 self, morfs=None, outfile=None, ignore_errors=None, |
1020 self, morfs=None, outfile=None, ignore_errors=None, |
998 omit=None, include=None, contexts=None, pretty_print=None, |
1021 omit=None, include=None, contexts=None, pretty_print=None, |
999 show_contexts=None |
1022 show_contexts=None |
1013 with override_config(self, |
1036 with override_config(self, |
1014 ignore_errors=ignore_errors, report_omit=omit, report_include=include, |
1037 ignore_errors=ignore_errors, report_omit=omit, report_include=include, |
1015 json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print, |
1038 json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print, |
1016 json_show_contexts=show_contexts |
1039 json_show_contexts=show_contexts |
1017 ): |
1040 ): |
1018 return render_report(self.config.json_output, JsonReporter(self), morfs) |
1041 return render_report(self.config.json_output, JsonReporter(self), morfs, self._message) |
1019 |
1042 |
1020 def sys_info(self): |
1043 def sys_info(self): |
1021 """Return a list of (key, value) pairs showing internal information.""" |
1044 """Return a list of (key, value) pairs showing internal information.""" |
1022 |
1045 |
1023 import coverage as covmod |
1046 import coverage as covmod |
1034 entry += " (disabled)" |
1057 entry += " (disabled)" |
1035 entries.append(entry) |
1058 entries.append(entry) |
1036 return entries |
1059 return entries |
1037 |
1060 |
1038 info = [ |
1061 info = [ |
1039 ('version', covmod.__version__), |
1062 ('coverage_version', covmod.__version__), |
1040 ('coverage', covmod.__file__), |
1063 ('coverage_module', covmod.__file__), |
1041 ('tracer', self._collector.tracer_name() if self._collector else "-none-"), |
1064 ('tracer', self._collector.tracer_name() if self._collector else "-none-"), |
1042 ('CTracer', 'available' if CTracer else "unavailable"), |
1065 ('CTracer', 'available' if CTracer else "unavailable"), |
1043 ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), |
1066 ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), |
1044 ('plugins.configurers', plugin_info(self._plugins.configurers)), |
1067 ('plugins.configurers', plugin_info(self._plugins.configurers)), |
1045 ('plugins.context_switchers', plugin_info(self._plugins.context_switchers)), |
1068 ('plugins.context_switchers', plugin_info(self._plugins.context_switchers)), |
1059 ('def_encoding', sys.getdefaultencoding()), |
1082 ('def_encoding', sys.getdefaultencoding()), |
1060 ('fs_encoding', sys.getfilesystemencoding()), |
1083 ('fs_encoding', sys.getfilesystemencoding()), |
1061 ('pid', os.getpid()), |
1084 ('pid', os.getpid()), |
1062 ('cwd', os.getcwd()), |
1085 ('cwd', os.getcwd()), |
1063 ('path', sys.path), |
1086 ('path', sys.path), |
1064 ('environment', sorted( |
1087 ('environment', human_sorted( |
1065 ("%s = %s" % (k, v)) |
1088 f"{k} = {v}" |
1066 for k, v in iitems(os.environ) |
1089 for k, v in os.environ.items() |
1067 if any(slug in k for slug in ("COV", "PY")) |
1090 if ( |
|
1091 any(slug in k for slug in ("COV", "PY")) or |
|
1092 (k in ("HOME", "TEMP", "TMP")) |
|
1093 ) |
1068 )), |
1094 )), |
1069 ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))), |
1095 ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))), |
1070 ] |
1096 ] |
1071 |
1097 |
1072 if self._inorout: |
1098 if self._inorout: |