coverage 7.6.7__cp311-cp311-win_amd64.whl → 7.11.1__cp311-cp311-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. coverage/__init__.py +2 -0
  2. coverage/__main__.py +2 -0
  3. coverage/annotate.py +1 -2
  4. coverage/bytecode.py +177 -3
  5. coverage/cmdline.py +329 -154
  6. coverage/collector.py +31 -42
  7. coverage/config.py +166 -62
  8. coverage/context.py +4 -5
  9. coverage/control.py +164 -85
  10. coverage/core.py +70 -33
  11. coverage/data.py +3 -4
  12. coverage/debug.py +112 -56
  13. coverage/disposition.py +1 -0
  14. coverage/env.py +65 -55
  15. coverage/exceptions.py +35 -7
  16. coverage/execfile.py +18 -13
  17. coverage/files.py +23 -18
  18. coverage/html.py +134 -88
  19. coverage/htmlfiles/style.css +42 -2
  20. coverage/htmlfiles/style.scss +65 -1
  21. coverage/inorout.py +61 -44
  22. coverage/jsonreport.py +17 -8
  23. coverage/lcovreport.py +16 -20
  24. coverage/misc.py +50 -46
  25. coverage/multiproc.py +12 -7
  26. coverage/numbits.py +3 -4
  27. coverage/parser.py +193 -269
  28. coverage/patch.py +166 -0
  29. coverage/phystokens.py +24 -25
  30. coverage/plugin.py +13 -13
  31. coverage/plugin_support.py +36 -35
  32. coverage/python.py +9 -13
  33. coverage/pytracer.py +40 -33
  34. coverage/regions.py +2 -1
  35. coverage/report.py +59 -43
  36. coverage/report_core.py +6 -9
  37. coverage/results.py +118 -66
  38. coverage/sqldata.py +260 -210
  39. coverage/sqlitedb.py +33 -25
  40. coverage/sysmon.py +195 -157
  41. coverage/templite.py +6 -6
  42. coverage/tomlconfig.py +12 -12
  43. coverage/tracer.cp311-win_amd64.pyd +0 -0
  44. coverage/tracer.pyi +2 -0
  45. coverage/types.py +25 -22
  46. coverage/version.py +3 -18
  47. coverage/xmlreport.py +16 -13
  48. {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/METADATA +40 -18
  49. coverage-7.11.1.dist-info/RECORD +59 -0
  50. {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/WHEEL +1 -1
  51. coverage-7.6.7.dist-info/RECORD +0 -58
  52. {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/entry_points.txt +0 -0
  53. {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info/licenses}/LICENSE.txt +0 -0
  54. {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/top_level.txt +0 -0
coverage/control.py CHANGED
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
  import atexit
9
9
  import collections
10
10
  import contextlib
11
+ import datetime
11
12
  import functools
12
13
  import os
13
14
  import os.path
@@ -17,20 +18,23 @@ import sys
17
18
  import threading
18
19
  import time
19
20
  import warnings
20
-
21
- from types import FrameType
22
- from typing import cast, Any, Callable, IO
23
21
  from collections.abc import Iterable, Iterator
22
+ from types import FrameType
23
+ from typing import IO, Any, Callable, cast
24
24
 
25
25
  from coverage import env
26
26
  from coverage.annotate import AnnotateReporter
27
27
  from coverage.collector import Collector
28
28
  from coverage.config import CoverageConfig, read_coverage_config
29
- from coverage.context import should_start_context_test_function, combine_context_switchers
30
- from coverage.core import Core, HAS_CTRACER
29
+ from coverage.context import combine_context_switchers, should_start_context_test_function
30
+ from coverage.core import CTRACER_FILE, Core
31
31
  from coverage.data import CoverageData, combine_parallel_data
32
32
  from coverage.debug import (
33
- DebugControl, NoDebugging, short_stack, write_formatted_info, relevant_environment_display,
33
+ DebugControl,
34
+ NoDebugging,
35
+ relevant_environment_display,
36
+ short_stack,
37
+ write_formatted_info,
34
38
  )
35
39
  from coverage.disposition import disposition_debug_msg
36
40
  from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError
@@ -39,23 +43,37 @@ from coverage.html import HtmlReporter
39
43
  from coverage.inorout import InOrOut
40
44
  from coverage.jsonreport import JsonReporter
41
45
  from coverage.lcovreport import LcovReporter
42
- from coverage.misc import bool_or_none, join_regex
43
- from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module
46
+ from coverage.misc import (
47
+ DefaultValue,
48
+ bool_or_none,
49
+ ensure_dir_for_file,
50
+ isolate_module,
51
+ join_regex,
52
+ )
44
53
  from coverage.multiproc import patch_multiprocessing
54
+ from coverage.patch import apply_patches
45
55
  from coverage.plugin import FileReporter
46
- from coverage.plugin_support import Plugins
56
+ from coverage.plugin_support import Plugins, TCoverageInit
47
57
  from coverage.python import PythonFileReporter
48
58
  from coverage.report import SummaryReporter
49
59
  from coverage.report_core import render_report
50
60
  from coverage.results import Analysis, analysis_from_file_reporter
51
61
  from coverage.types import (
52
- FilePath, TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigValueOut,
53
- TFileDisposition, TLineNo, TMorf,
62
+ FilePath,
63
+ TConfigSectionIn,
64
+ TConfigurable,
65
+ TConfigValueIn,
66
+ TConfigValueOut,
67
+ TFileDisposition,
68
+ TLineNo,
69
+ TMorf,
54
70
  )
71
+ from coverage.version import __url__
55
72
  from coverage.xmlreport import XmlReporter
56
73
 
57
74
  os = isolate_module(os)
58
75
 
76
+
59
77
  @contextlib.contextmanager
60
78
  def override_config(cov: Coverage, **kwargs: TConfigValueIn) -> Iterator[None]:
61
79
  """Temporarily tweak the configuration of `cov`.
@@ -74,6 +92,8 @@ def override_config(cov: Coverage, **kwargs: TConfigValueIn) -> Iterator[None]:
74
92
 
75
93
  DEFAULT_DATAFILE = DefaultValue("MISSING")
76
94
  _DEFAULT_DATAFILE = DEFAULT_DATAFILE # Just in case, for backwards compatibility
95
+ CONFIG_DATA_PREFIX = ":data:"
96
+
77
97
 
78
98
  class Coverage(TConfigurable):
79
99
  """Programmatic access to coverage.py.
@@ -120,7 +140,7 @@ class Coverage(TConfigurable):
120
140
  else:
121
141
  return None
122
142
 
123
- def __init__( # pylint: disable=too-many-arguments
143
+ def __init__( # pylint: disable=too-many-arguments
124
144
  self,
125
145
  data_file: FilePath | DefaultValue | None = DEFAULT_DATAFILE,
126
146
  data_suffix: str | bool | None = None,
@@ -131,6 +151,7 @@ class Coverage(TConfigurable):
131
151
  config_file: FilePath | bool = True,
132
152
  source: Iterable[str] | None = None,
133
153
  source_pkgs: Iterable[str] | None = None,
154
+ source_dirs: Iterable[str] | None = None,
134
155
  omit: str | Iterable[str] | None = None,
135
156
  include: str | Iterable[str] | None = None,
136
157
  debug: Iterable[str] | None = None,
@@ -138,6 +159,7 @@ class Coverage(TConfigurable):
138
159
  check_preimported: bool = False,
139
160
  context: str | None = None,
140
161
  messages: bool = False,
162
+ plugins: Iterable[Callable[..., None]] | None = None,
141
163
  ) -> None:
142
164
  """
143
165
  Many of these arguments duplicate and override values that can be
@@ -187,6 +209,10 @@ class Coverage(TConfigurable):
187
209
  `source`, but can be used to name packages where the name can also be
188
210
  interpreted as a file path.
189
211
 
212
+ `source_dirs` is a list of file paths. It works the same as
213
+ `source`, but raises an error if the path doesn't exist, rather
214
+ than being treated as a package name.
215
+
190
216
  `include` and `omit` are lists of file name patterns. Files that match
191
217
  `include` will be measured, files that match `omit` will not. Each
192
218
  will also accept a single string argument.
@@ -211,6 +237,11 @@ class Coverage(TConfigurable):
211
237
  If `messages` is true, some messages will be printed to stdout
212
238
  indicating what is happening.
213
239
 
240
+ If `plugins` are passed, they are an iterable of function objects
241
+ accepting a `reg` object to register plugins, as described in
242
+ :ref:`api_plugin`. When they are provided, they will override the
243
+ plugins found in the coverage configuration file.
244
+
214
245
  .. versionadded:: 4.0
215
246
  The `concurrency` parameter.
216
247
 
@@ -226,6 +257,11 @@ class Coverage(TConfigurable):
226
257
  .. versionadded:: 6.0
227
258
  The `messages` parameter.
228
259
 
260
+ .. versionadded:: 7.7
261
+ The `plugins` parameter.
262
+
263
+ .. versionadded:: 7.8
264
+ The `source_dirs` parameter.
229
265
  """
230
266
  # Start self.config as a usable default configuration. It will soon be
231
267
  # replaced with the real configuration.
@@ -249,9 +285,12 @@ class Coverage(TConfigurable):
249
285
  self._warn_no_data = True
250
286
  self._warn_unimported_source = True
251
287
  self._warn_preimported_source = check_preimported
252
- self._no_warn_slugs: list[str] = []
288
+ self._no_warn_slugs: set[str] = set()
253
289
  self._messages = messages
254
290
 
291
+ # If we're invoked from a .pth file, we shouldn't try to make another one.
292
+ self._make_pth_file = True
293
+
255
294
  # A record of all the warnings that have been issued.
256
295
  self._warnings: list[str] = []
257
296
 
@@ -260,7 +299,9 @@ class Coverage(TConfigurable):
260
299
  self._debug: DebugControl = NoDebugging()
261
300
  self._inorout: InOrOut | None = None
262
301
  self._plugins: Plugins = Plugins()
302
+ self._plugin_override = cast(Iterable[TCoverageInit] | None, plugins)
263
303
  self._data: CoverageData | None = None
304
+ self._data_to_close: list[CoverageData] = []
264
305
  self._core: Core | None = None
265
306
  self._collector: Collector | None = None
266
307
  self._metacov = False
@@ -280,28 +321,32 @@ class Coverage(TConfigurable):
280
321
  self._should_write_debug = True
281
322
 
282
323
  # Build our configuration from a number of sources.
283
- if not isinstance(config_file, bool):
284
- config_file = os.fspath(config_file)
285
- self.config = read_coverage_config(
286
- config_file=config_file,
287
- warn=self._warn,
288
- data_file=data_file,
289
- cover_pylib=cover_pylib,
290
- timid=timid,
291
- branch=branch,
292
- parallel=bool_or_none(data_suffix),
293
- source=source,
294
- source_pkgs=source_pkgs,
295
- run_omit=omit,
296
- run_include=include,
297
- debug=debug,
298
- report_omit=omit,
299
- report_include=include,
300
- concurrency=concurrency,
301
- context=context,
302
- )
324
+ if isinstance(config_file, str) and config_file.startswith(CONFIG_DATA_PREFIX):
325
+ self.config = CoverageConfig.deserialize(config_file[len(CONFIG_DATA_PREFIX) :])
326
+ else:
327
+ if not isinstance(config_file, bool):
328
+ config_file = os.fspath(config_file)
329
+ self.config = read_coverage_config(
330
+ config_file=config_file,
331
+ warn=self._warn,
332
+ data_file=data_file,
333
+ cover_pylib=cover_pylib,
334
+ timid=timid,
335
+ branch=branch,
336
+ parallel=bool_or_none(data_suffix),
337
+ source=source,
338
+ source_pkgs=source_pkgs,
339
+ source_dirs=source_dirs,
340
+ run_omit=omit,
341
+ run_include=include,
342
+ debug=debug,
343
+ report_omit=omit,
344
+ report_include=include,
345
+ concurrency=concurrency,
346
+ context=context,
347
+ )
303
348
 
304
- # If we have sub-process measurement happening automatically, then we
349
+ # If we have subprocess measurement happening automatically, then we
305
350
  # want any explicit creation of a Coverage object to mean, this process
306
351
  # is already coverage-aware, so don't auto-measure it. By now, the
307
352
  # auto-creation of a Coverage object has already happened. But we can
@@ -340,7 +385,11 @@ class Coverage(TConfigurable):
340
385
  self._file_mapper = relative_filename
341
386
 
342
387
  # Load plugins
343
- self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self._debug)
388
+ self._plugins = Plugins(self._debug)
389
+ if self._plugin_override:
390
+ self._plugins.load_from_callables(self._plugin_override)
391
+ else:
392
+ self._plugins.load_from_config(self.config.plugins, self.config)
344
393
 
345
394
  # Run configuring plugins.
346
395
  for plugin in self._plugins.configurers:
@@ -366,22 +415,24 @@ class Coverage(TConfigurable):
366
415
  wrote_any = False
367
416
  with self._debug.without_callers():
368
417
  if self._debug.should("config"):
369
- config_info = self.config.debug_info()
370
- write_formatted_info(self._debug.write, "config", config_info)
418
+ write_formatted_info(self._debug.write, "config", self.config.debug_info())
371
419
  wrote_any = True
372
420
 
373
421
  if self._debug.should("sys"):
374
422
  write_formatted_info(self._debug.write, "sys", self.sys_info())
375
423
  for plugin in self._plugins:
376
424
  header = "sys: " + plugin._coverage_plugin_name
377
- info = plugin.sys_info()
378
- write_formatted_info(self._debug.write, header, info)
425
+ write_formatted_info(self._debug.write, header, plugin.sys_info())
379
426
  wrote_any = True
380
427
 
381
428
  if self._debug.should("pybehave"):
382
429
  write_formatted_info(self._debug.write, "pybehave", env.debug_info())
383
430
  wrote_any = True
384
431
 
432
+ if self._debug.should("sqlite"):
433
+ write_formatted_info(self._debug.write, "sqlite", CoverageData.sys_info())
434
+ wrote_any = True
435
+
385
436
  if wrote_any:
386
437
  write_formatted_info(self._debug.write, "end", ())
387
438
 
@@ -424,7 +475,7 @@ class Coverage(TConfigurable):
424
475
 
425
476
  """
426
477
  if not self._no_warn_slugs:
427
- self._no_warn_slugs = list(self.config.disable_warnings)
478
+ self._no_warn_slugs = set(self.config.disable_warnings)
428
479
 
429
480
  if slug in self._no_warn_slugs:
430
481
  # Don't issue the warning
@@ -432,14 +483,14 @@ class Coverage(TConfigurable):
432
483
 
433
484
  self._warnings.append(msg)
434
485
  if slug:
435
- msg = f"{msg} ({slug})"
486
+ msg = f"{msg} ({slug}); see {__url__}/messages.html#warning-{slug}"
436
487
  if self._debug.should("pid"):
437
488
  msg = f"[{os.getpid()}] {msg}"
438
489
  warnings.warn(msg, category=CoverageWarning, stacklevel=2)
439
490
 
440
491
  if once:
441
492
  assert slug is not None
442
- self._no_warn_slugs.append(slug)
493
+ self._no_warn_slugs.add(slug)
443
494
 
444
495
  def _message(self, msg: str) -> None:
445
496
  """Write a message to the user, if configured to do so."""
@@ -512,7 +563,7 @@ class Coverage(TConfigurable):
512
563
  def _init_for_start(self) -> None:
513
564
  """Initialization for start()"""
514
565
  # Construct the collector.
515
- concurrency: list[str] = self.config.concurrency or []
566
+ concurrency: list[str] = self.config.concurrency
516
567
  if "multiprocessing" in concurrency:
517
568
  if self.config.config_file is None:
518
569
  raise ConfigError("multiprocessing requires a configuration file")
@@ -534,7 +585,9 @@ class Coverage(TConfigurable):
534
585
 
535
586
  self._core = Core(
536
587
  warn=self._warn,
537
- timid=self.config.timid,
588
+ debug=(self._debug if self._debug.should("core") else None),
589
+ config=self.config,
590
+ dynamic_contexts=(should_start_context is not None),
538
591
  metacov=self._metacov,
539
592
  )
540
593
  self._collector = Collector(
@@ -571,8 +624,7 @@ class Coverage(TConfigurable):
571
624
  self._warn(
572
625
  "Plugin file tracers ({}) aren't supported with {}".format(
573
626
  ", ".join(
574
- plugin._coverage_plugin_name
575
- for plugin in self._plugins.file_tracers
627
+ plugin._coverage_plugin_name for plugin in self._plugins.file_tracers
576
628
  ),
577
629
  self._collector.tracer_name(),
578
630
  ),
@@ -596,13 +648,14 @@ class Coverage(TConfigurable):
596
648
  # Register our clean-up handlers.
597
649
  atexit.register(self._atexit)
598
650
  if self.config.sigterm:
599
- is_main = (threading.current_thread() == threading.main_thread())
651
+ is_main = (threading.current_thread() == threading.main_thread()) # fmt: skip
600
652
  if is_main and not env.WINDOWS:
601
653
  # The Python docs seem to imply that SIGTERM works uniformly even
602
654
  # on Windows, but that's not my experience, and this agrees:
603
655
  # https://stackoverflow.com/questions/35772001/x/35792192#35792192
604
- self._old_sigterm = signal.signal( # type: ignore[assignment]
605
- signal.SIGTERM, self._on_sigterm,
656
+ self._old_sigterm = signal.signal( # type: ignore[assignment]
657
+ signal.SIGTERM,
658
+ self._on_sigterm,
606
659
  )
607
660
 
608
661
  def _init_data(self, suffix: str | bool | None) -> None:
@@ -619,6 +672,7 @@ class Coverage(TConfigurable):
619
672
  debug=self._debug,
620
673
  no_disk=self._no_disk,
621
674
  )
675
+ self._data_to_close.append(self._data)
622
676
 
623
677
  def start(self) -> None:
624
678
  """Start measuring code coverage.
@@ -654,6 +708,8 @@ class Coverage(TConfigurable):
654
708
  if self._auto_load:
655
709
  self.load()
656
710
 
711
+ apply_patches(self, self.config, self._debug, make_pth_file=self._make_pth_file)
712
+
657
713
  self._collector.start()
658
714
  self._started = True
659
715
  self._instances.append(self)
@@ -679,7 +735,7 @@ class Coverage(TConfigurable):
679
735
  try:
680
736
  yield
681
737
  finally:
682
- self.stop() # pragma: nested
738
+ self.stop() # pragma: nested
683
739
 
684
740
  def _atexit(self, event: str = "atexit") -> None:
685
741
  """Clean up on process shutdown."""
@@ -689,14 +745,16 @@ class Coverage(TConfigurable):
689
745
  self.stop()
690
746
  if self._auto_save or event == "sigterm":
691
747
  self.save()
748
+ for d in self._data_to_close:
749
+ d.close(force=True)
692
750
 
693
751
  def _on_sigterm(self, signum_unused: int, frame_unused: FrameType | None) -> None:
694
752
  """A handler for signal.SIGTERM."""
695
753
  self._atexit("sigterm")
696
754
  # Statements after here won't be seen by metacov because we just wrote
697
755
  # the data, and are about to kill the process.
698
- signal.signal(signal.SIGTERM, self._old_sigterm) # pragma: not covered
699
- os.kill(os.getpid(), signal.SIGTERM) # pragma: not covered
756
+ signal.signal(signal.SIGTERM, self._old_sigterm) # pragma: not covered
757
+ os.kill(os.getpid(), signal.SIGTERM) # pragma: not covered
700
758
 
701
759
  def erase(self) -> None:
702
760
  """Erase previously collected coverage data.
@@ -728,7 +786,7 @@ class Coverage(TConfigurable):
728
786
  .. versionadded:: 5.0
729
787
 
730
788
  """
731
- if not self._started: # pragma: part started
789
+ if not self._started: # pragma: part started
732
790
  raise CoverageException("Cannot switch context, coverage is not started")
733
791
 
734
792
  assert self._collector is not None
@@ -740,7 +798,7 @@ class Coverage(TConfigurable):
740
798
  def clear_exclude(self, which: str = "exclude") -> None:
741
799
  """Clear the exclude list."""
742
800
  self._init()
743
- setattr(self.config, which + "_list", [])
801
+ setattr(self.config, f"{which}_list", [])
744
802
  self._exclude_regex_stale()
745
803
 
746
804
  def exclude(self, regex: str, which: str = "exclude") -> None:
@@ -759,7 +817,7 @@ class Coverage(TConfigurable):
759
817
 
760
818
  """
761
819
  self._init()
762
- excl_list = getattr(self.config, which + "_list")
820
+ excl_list = getattr(self.config, f"{which}_list")
763
821
  excl_list.append(regex)
764
822
  self._exclude_regex_stale()
765
823
 
@@ -770,7 +828,7 @@ class Coverage(TConfigurable):
770
828
  def _exclude_regex(self, which: str) -> str:
771
829
  """Return a regex string for the given exclusion list."""
772
830
  if which not in self._exclude_re:
773
- excl_list = getattr(self.config, which + "_list")
831
+ excl_list = getattr(self.config, f"{which}_list")
774
832
  self._exclude_re[which] = join_regex(excl_list)
775
833
  return self._exclude_re[which]
776
834
 
@@ -782,7 +840,7 @@ class Coverage(TConfigurable):
782
840
 
783
841
  """
784
842
  self._init()
785
- return cast(list[str], getattr(self.config, which + "_list"))
843
+ return cast(list[str], getattr(self.config, f"{which}_list"))
786
844
 
787
845
  def save(self) -> None:
788
846
  """Save the collected coverage data to the data file."""
@@ -935,6 +993,7 @@ class Coverage(TConfigurable):
935
993
  analysis.missing_formatted(),
936
994
  )
937
995
 
996
+ @functools.lru_cache(maxsize=1)
938
997
  def _analyze(self, morf: TMorf) -> Analysis:
939
998
  """Analyze a module or file. Private for now."""
940
999
  self._init()
@@ -945,6 +1004,20 @@ class Coverage(TConfigurable):
945
1004
  filename = self._file_mapper(file_reporter.filename)
946
1005
  return analysis_from_file_reporter(data, self.config.precision, file_reporter, filename)
947
1006
 
1007
+ def branch_stats(self, morf: TMorf) -> dict[TLineNo, tuple[int, int]]:
1008
+ """Get branch statistics about a module.
1009
+
1010
+ `morf` is a module or a file name.
1011
+
1012
+ Returns a dict mapping line numbers to a tuple:
1013
+ (total_exits, taken_exits).
1014
+
1015
+ .. versionadded:: 7.7
1016
+
1017
+ """
1018
+ analysis = self._analyze(morf)
1019
+ return analysis.branch_stats()
1020
+
948
1021
  @functools.lru_cache(maxsize=1)
949
1022
  def _get_file_reporter(self, morf: TMorf) -> FileReporter:
950
1023
  """Get a FileReporter for a module or file name."""
@@ -963,7 +1036,8 @@ class Coverage(TConfigurable):
963
1036
  if file_reporter is None:
964
1037
  raise PluginError(
965
1038
  "Plugin {!r} did not provide a file reporter for {!r}.".format(
966
- plugin._coverage_plugin_name, morf,
1039
+ plugin._coverage_plugin_name,
1040
+ morf,
967
1041
  ),
968
1042
  )
969
1043
 
@@ -993,8 +1067,9 @@ class Coverage(TConfigurable):
993
1067
 
994
1068
  # Be sure we have a collection.
995
1069
  if not isinstance(morfs, (list, tuple, set)):
996
- morfs = [morfs] # type: ignore[list-item]
1070
+ morfs = [morfs] # type: ignore[list-item]
997
1071
 
1072
+ morfs = sorted(morfs, key=lambda m: m if isinstance(m, str) else m.__name__)
998
1073
  return [(self._get_file_reporter(morf), morf) for morf in morfs]
999
1074
 
1000
1075
  def _prepare_data_for_reporting(self) -> None:
@@ -1004,6 +1079,7 @@ class Coverage(TConfigurable):
1004
1079
  if self._data is not None:
1005
1080
  mapped_data.update(self._data, map_path=self._make_aliases().map)
1006
1081
  self._data = mapped_data
1082
+ self._data_to_close.append(mapped_data)
1007
1083
 
1008
1084
  def report(
1009
1085
  self,
@@ -1172,8 +1248,7 @@ class Coverage(TConfigurable):
1172
1248
  precision=precision,
1173
1249
  ):
1174
1250
  reporter = HtmlReporter(self)
1175
- ret = reporter.report(morfs)
1176
- return ret
1251
+ return reporter.report(morfs)
1177
1252
 
1178
1253
  def xml_report(
1179
1254
  self,
@@ -1298,21 +1373,23 @@ class Coverage(TConfigurable):
1298
1373
  ("coverage_version", covmod.__version__),
1299
1374
  ("coverage_module", covmod.__file__),
1300
1375
  ("core", self._collector.tracer_name() if self._collector is not None else "-none-"),
1301
- ("CTracer", "available" if HAS_CTRACER else "unavailable"),
1376
+ ("CTracer", f"available from {CTRACER_FILE}" if CTRACER_FILE else "unavailable"),
1302
1377
  ("plugins.file_tracers", plugin_info(self._plugins.file_tracers)),
1303
1378
  ("plugins.configurers", plugin_info(self._plugins.configurers)),
1304
1379
  ("plugins.context_switchers", plugin_info(self._plugins.context_switchers)),
1305
1380
  ("configs_attempted", self.config.config_files_attempted),
1306
1381
  ("configs_read", self.config.config_files_read),
1307
1382
  ("config_file", self.config.config_file),
1308
- ("config_contents",
1383
+ (
1384
+ "config_contents",
1309
1385
  repr(self.config._config_contents) if self.config._config_contents else "-none-",
1310
1386
  ),
1311
1387
  ("data_file", self._data.data_filename() if self._data is not None else "-none-"),
1312
1388
  ("python", sys.version.replace("\n", "")),
1313
1389
  ("platform", platform.platform()),
1314
1390
  ("implementation", platform.python_implementation()),
1315
- ("gil_enabled", getattr(sys, '_is_gil_enabled', lambda: True)()),
1391
+ ("build", platform.python_build()),
1392
+ ("gil_enabled", getattr(sys, "_is_gil_enabled", lambda: True)()),
1316
1393
  ("executable", sys.executable),
1317
1394
  ("def_encoding", sys.getdefaultencoding()),
1318
1395
  ("fs_encoding", sys.getfilesystemencoding()),
@@ -1321,52 +1398,46 @@ class Coverage(TConfigurable):
1321
1398
  ("path", sys.path),
1322
1399
  ("environment", [f"{k} = {v}" for k, v in relevant_environment_display(os.environ)]),
1323
1400
  ("command_line", " ".join(getattr(sys, "argv", ["-none-"]))),
1401
+ ("time", f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S}"),
1324
1402
  ]
1325
1403
 
1326
1404
  if self._inorout is not None:
1327
1405
  info.extend(self._inorout.sys_info())
1328
1406
 
1329
- info.extend(CoverageData.sys_info())
1330
-
1331
1407
  return info
1332
1408
 
1333
1409
 
1334
1410
  # Mega debugging...
1335
1411
  # $set_env.py: COVERAGE_DEBUG_CALLS - Lots and lots of output about calls to Coverage.
1336
- if int(os.getenv("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugging
1412
+ if int(os.getenv("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugging
1337
1413
  from coverage.debug import decorate_methods, show_calls
1338
1414
 
1339
- Coverage = decorate_methods( # type: ignore[misc]
1415
+ Coverage = decorate_methods( # type: ignore[misc]
1340
1416
  show_calls(show_args=True),
1341
1417
  butnot=["get_data"],
1342
1418
  )(Coverage)
1343
1419
 
1344
1420
 
1345
- def process_startup() -> Coverage | None:
1421
+ def process_startup(*, force: bool = False) -> Coverage | None:
1346
1422
  """Call this at Python start-up to perhaps measure coverage.
1347
1423
 
1348
1424
  If the environment variable COVERAGE_PROCESS_START is defined, coverage
1349
1425
  measurement is started. The value of the variable is the config file
1350
1426
  to use.
1351
1427
 
1352
- There are two ways to configure your Python installation to invoke this
1353
- function when Python starts:
1354
-
1355
- #. Create or append to sitecustomize.py to add these lines::
1356
-
1357
- import coverage
1358
- coverage.process_startup()
1359
-
1360
- #. Create a .pth file in your Python installation containing::
1361
-
1362
- import coverage; coverage.process_startup()
1428
+ For details, see https://coverage.readthedocs.io/en/latest/subprocess.html.
1363
1429
 
1364
1430
  Returns the :class:`Coverage` instance that was started, or None if it was
1365
1431
  not started by this call.
1366
1432
 
1367
1433
  """
1434
+ config_data = os.getenv("COVERAGE_PROCESS_CONFIG")
1368
1435
  cps = os.getenv("COVERAGE_PROCESS_START")
1369
- if not cps:
1436
+ if config_data is not None:
1437
+ config_file = CONFIG_DATA_PREFIX + config_data
1438
+ elif cps is not None:
1439
+ config_file = cps
1440
+ else:
1370
1441
  # No request for coverage, nothing to do.
1371
1442
  return None
1372
1443
 
@@ -1379,22 +1450,30 @@ def process_startup() -> Coverage | None:
1379
1450
  #
1380
1451
  # https://github.com/nedbat/coveragepy/issues/340 has more details.
1381
1452
 
1382
- if hasattr(process_startup, "coverage"):
1453
+ if not force and hasattr(process_startup, "coverage"):
1383
1454
  # We've annotated this function before, so we must have already
1384
- # started coverage.py in this process. Nothing to do.
1455
+ # auto-started coverage.py in this process. Nothing to do.
1385
1456
  return None
1386
1457
 
1387
- cov = Coverage(config_file=cps)
1388
- process_startup.coverage = cov # type: ignore[attr-defined]
1458
+ cov = Coverage(config_file=config_file)
1459
+ process_startup.coverage = cov # type: ignore[attr-defined]
1389
1460
  cov._warn_no_data = False
1390
1461
  cov._warn_unimported_source = False
1391
1462
  cov._warn_preimported_source = False
1392
1463
  cov._auto_save = True
1464
+ cov._make_pth_file = False
1393
1465
  cov.start()
1394
1466
 
1395
1467
  return cov
1396
1468
 
1397
1469
 
1470
+ def _after_fork_in_child() -> None:
1471
+ """Used by patch=fork in the child process to restart coverage."""
1472
+ if cov := Coverage.current():
1473
+ cov.stop()
1474
+ process_startup(force=True)
1475
+
1476
+
1398
1477
  def _prevent_sub_process_measurement() -> None:
1399
1478
  """Stop any subprocess auto-measurement from writing data."""
1400
1479
  auto_created_coverage = getattr(process_startup, "coverage", None)