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/collector.py CHANGED
@@ -9,13 +9,11 @@ import contextlib
9
9
  import functools
10
10
  import os
11
11
  import sys
12
-
13
12
  from collections.abc import Mapping
14
13
  from types import FrameType
15
- from typing import cast, Any, Callable, TypeVar
14
+ from typing import Any, Callable, TypeVar, cast
16
15
 
17
16
  from coverage import env
18
- from coverage.config import CoverageConfig
19
17
  from coverage.core import Core
20
18
  from coverage.data import CoverageData
21
19
  from coverage.debug import short_stack
@@ -26,11 +24,11 @@ from coverage.types import (
26
24
  TArc,
27
25
  TCheckIncludeFn,
28
26
  TFileDisposition,
27
+ Tracer,
29
28
  TShouldStartContextFn,
30
29
  TShouldTraceFn,
31
30
  TTraceData,
32
31
  TTraceFn,
33
- Tracer,
34
32
  TWarnFn,
35
33
  )
36
34
 
@@ -61,9 +59,6 @@ class Collector:
61
59
  # the top, and resumed when they become the top again.
62
60
  _collectors: list[Collector] = []
63
61
 
64
- # The concurrency settings we support here.
65
- LIGHT_THREADS = {"greenlet", "eventlet", "gevent"}
66
-
67
62
  def __init__(
68
63
  self,
69
64
  core: Core,
@@ -113,8 +108,7 @@ class Collector:
113
108
  self.file_mapper = file_mapper
114
109
  self.branch = branch
115
110
  self.warn = warn
116
- self.concurrency = concurrency
117
- assert isinstance(self.concurrency, list), f"Expected a list: {self.concurrency!r}"
111
+ assert isinstance(concurrency, list), f"Expected a list: {concurrency!r}"
118
112
 
119
113
  self.pid = os.getpid()
120
114
 
@@ -126,34 +120,27 @@ class Collector:
126
120
 
127
121
  self.concur_id_func = None
128
122
 
129
- # We can handle a few concurrency options here, but only one at a time.
130
- concurrencies = set(self.concurrency)
131
- unknown = concurrencies - CoverageConfig.CONCURRENCY_CHOICES
132
- if unknown:
133
- show = ", ".join(sorted(unknown))
134
- raise ConfigError(f"Unknown concurrency choices: {show}")
135
- light_threads = concurrencies & self.LIGHT_THREADS
136
- if len(light_threads) > 1:
137
- show = ", ".join(sorted(light_threads))
138
- raise ConfigError(f"Conflicting concurrency settings: {show}")
139
123
  do_threading = False
140
124
 
141
125
  tried = "nothing" # to satisfy pylint
142
126
  try:
143
- if "greenlet" in concurrencies:
127
+ if "greenlet" in concurrency:
144
128
  tried = "greenlet"
145
129
  import greenlet
130
+
146
131
  self.concur_id_func = greenlet.getcurrent
147
- elif "eventlet" in concurrencies:
132
+ elif "eventlet" in concurrency:
148
133
  tried = "eventlet"
149
- import eventlet.greenthread # pylint: disable=import-error,useless-suppression
134
+ import eventlet.greenthread
135
+
150
136
  self.concur_id_func = eventlet.greenthread.getcurrent
151
- elif "gevent" in concurrencies:
137
+ elif "gevent" in concurrency:
152
138
  tried = "gevent"
153
- import gevent # pylint: disable=import-error,useless-suppression
139
+ import gevent
140
+
154
141
  self.concur_id_func = gevent.getcurrent
155
142
 
156
- if "thread" in concurrencies:
143
+ if "thread" in concurrency:
157
144
  do_threading = True
158
145
  except ImportError as ex:
159
146
  msg = f"Couldn't trace with concurrency={tried}, the module isn't installed."
@@ -162,15 +149,17 @@ class Collector:
162
149
  if self.concur_id_func and not hasattr(core.tracer_class, "concur_id_func"):
163
150
  raise ConfigError(
164
151
  "Can't support concurrency={} with {}, only threads are supported.".format(
165
- tried, self.tracer_name(),
152
+ tried,
153
+ self.tracer_name(),
166
154
  ),
167
155
  )
168
156
 
169
- if do_threading or not concurrencies:
157
+ if do_threading or not concurrency:
170
158
  # It's important to import threading only if we need it. If
171
159
  # it's imported early, and the program being measured uses
172
160
  # gevent, then gevent's monkey-patching won't work properly.
173
161
  import threading
162
+
174
163
  self.threading = threading
175
164
 
176
165
  self.reset()
@@ -222,7 +211,8 @@ class Collector:
222
211
  # being excluded by the inclusion rules, in which case the
223
212
  # FileDisposition will be replaced by None in the cache.
224
213
  if env.PYPY:
225
- import __pypy__ # pylint: disable=import-error
214
+ import __pypy__ # pylint: disable=import-error
215
+
226
216
  # Alex Gaynor said:
227
217
  # should_trace_cache is a strictly growing key: once a key is in
228
218
  # it, it never changes. Further, the keys used to access it are
@@ -267,19 +257,19 @@ class Collector:
267
257
  tracer.should_trace_cache = self.should_trace_cache
268
258
  tracer.warn = self.warn
269
259
 
270
- if hasattr(tracer, 'concur_id_func'):
260
+ if hasattr(tracer, "concur_id_func"):
271
261
  tracer.concur_id_func = self.concur_id_func
272
- if hasattr(tracer, 'file_tracers'):
262
+ if hasattr(tracer, "file_tracers"):
273
263
  tracer.file_tracers = self.file_tracers
274
- if hasattr(tracer, 'threading'):
264
+ if hasattr(tracer, "threading"):
275
265
  tracer.threading = self.threading
276
- if hasattr(tracer, 'check_include'):
266
+ if hasattr(tracer, "check_include"):
277
267
  tracer.check_include = self.check_include
278
- if hasattr(tracer, 'should_start_context'):
268
+ if hasattr(tracer, "should_start_context"):
279
269
  tracer.should_start_context = self.should_start_context
280
- if hasattr(tracer, 'switch_context'):
270
+ if hasattr(tracer, "switch_context"):
281
271
  tracer.switch_context = self.switch_context
282
- if hasattr(tracer, 'disable_plugin'):
272
+ if hasattr(tracer, "disable_plugin"):
283
273
  tracer.disable_plugin = self.disable_plugin
284
274
 
285
275
  fn = tracer.start()
@@ -367,7 +357,7 @@ class Collector:
367
357
  tracer.stop()
368
358
  stats = tracer.get_stats()
369
359
  if stats:
370
- print("\nCoverage.py tracer stats:")
360
+ print(f"\nCoverage.py {tracer.__class__.__name__} stats:")
371
361
  for k, v in human_sorted_items(stats.items()):
372
362
  print(f"{k:>20}: {v}")
373
363
  if self.threading:
@@ -419,7 +409,7 @@ class Collector:
419
409
  plugin._coverage_enabled = False
420
410
  disposition.trace = False
421
411
 
422
- @functools.cache # pylint: disable=method-cache-max-size-none
412
+ @functools.cache # pylint: disable=method-cache-max-size-none
423
413
  def cached_mapped_file(self, filename: str) -> str:
424
414
  """A locally cached version of file names mapped through file_mapper."""
425
415
  return self.file_mapper(filename)
@@ -431,14 +421,14 @@ class Collector:
431
421
  # in other threads. We try three times in case of concurrent
432
422
  # access, hoping to get a clean copy.
433
423
  runtime_err = None
434
- for _ in range(3): # pragma: part covered
424
+ for _ in range(3): # pragma: part covered
435
425
  try:
436
426
  items = list(d.items())
437
- except RuntimeError as ex: # pragma: cant happen
427
+ except RuntimeError as ex: # pragma: cant happen
438
428
  runtime_err = ex
439
429
  else:
440
430
  break
441
- else: # pragma: cant happen
431
+ else: # pragma: cant happen
442
432
  assert isinstance(runtime_err, Exception)
443
433
  raise runtime_err
444
434
 
@@ -488,8 +478,7 @@ class Collector:
488
478
  self.covdata.add_lines(self.mapped_file_dict(line_data))
489
479
 
490
480
  file_tracers = {
491
- k: v for k, v in self.file_tracers.items()
492
- if v not in self.disabled_plugins
481
+ k: v for k, v in self.file_tracers.items() if v not in self.disabled_plugins
493
482
  }
494
483
  self.covdata.add_file_tracers(self.mapped_file_dict(file_tracers))
495
484
 
coverage/config.py CHANGED
@@ -5,24 +5,27 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import base64
8
9
  import collections
9
10
  import configparser
10
11
  import copy
12
+ import json
11
13
  import os
12
14
  import os.path
13
15
  import re
14
-
15
- from typing import (
16
- Any, Callable, Union,
17
- )
18
16
  from collections.abc import Iterable
17
+ from typing import Any, Callable, Final, Mapping
19
18
 
20
19
  from coverage.exceptions import ConfigError
21
- from coverage.misc import isolate_module, human_sorted_items, substitute_variables
20
+ from coverage.misc import human_sorted_items, isolate_module, substitute_variables
22
21
  from coverage.tomlconfig import TomlConfigParser, TomlDecodeError
23
22
  from coverage.types import (
24
- TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigSectionOut,
25
- TConfigValueOut, TPluginConfig,
23
+ TConfigSectionIn,
24
+ TConfigSectionOut,
25
+ TConfigurable,
26
+ TConfigValueIn,
27
+ TConfigValueOut,
28
+ TPluginConfig,
26
29
  )
27
30
 
28
31
  os = isolate_module(os)
@@ -44,7 +47,7 @@ class HandyConfigParser(configparser.ConfigParser):
44
47
  if our_file:
45
48
  self.section_prefixes.append("")
46
49
 
47
- def read( # type: ignore[override]
50
+ def read( # type: ignore[override]
48
51
  self,
49
52
  filenames: Iterable[str],
50
53
  encoding_unused: str | None = None,
@@ -61,16 +64,16 @@ class HandyConfigParser(configparser.ConfigParser):
61
64
  return real_section
62
65
  return None
63
66
 
64
- def has_option(self, section: str, option: str) -> bool:
67
+ def has_option(self, section: str, option: str) -> bool: # type: ignore[override]
65
68
  real_section = self.real_section(section)
66
69
  if real_section is not None:
67
70
  return super().has_option(real_section, option)
68
71
  return False
69
72
 
70
- def has_section(self, section: str) -> bool:
73
+ def has_section(self, section: str) -> bool: # type: ignore[override]
71
74
  return bool(self.real_section(section))
72
75
 
73
- def options(self, section: str) -> list[str]:
76
+ def options(self, section: str) -> list[str]: # type: ignore[override]
74
77
  real_section = self.real_section(section)
75
78
  if real_section is not None:
76
79
  return super().options(real_section)
@@ -83,7 +86,7 @@ class HandyConfigParser(configparser.ConfigParser):
83
86
  d[opt] = self.get(section, opt)
84
87
  return d
85
88
 
86
- def get(self, section: str, option: str, *args: Any, **kwargs: Any) -> str: # type: ignore
89
+ def get(self, section: str, option: str, *args: Any, **kwargs: Any) -> str: # type: ignore
87
90
  """Get a value, replacing environment variables also.
88
91
 
89
92
  The arguments are the same as `ConfigParser.get`, but in the found
@@ -104,6 +107,11 @@ class HandyConfigParser(configparser.ConfigParser):
104
107
  v = substitute_variables(v, os.environ)
105
108
  return v
106
109
 
110
+ def getfile(self, section: str, option: str) -> str:
111
+ """Fix up a file path setting."""
112
+ path = self.get(section, option)
113
+ return process_file_value(path)
114
+
107
115
  def getlist(self, section: str, option: str) -> list[str]:
108
116
  """Read a list of strings.
109
117
 
@@ -132,26 +140,17 @@ class HandyConfigParser(configparser.ConfigParser):
132
140
 
133
141
  """
134
142
  line_list = self.get(section, option)
135
- value_list = []
136
- for value in line_list.splitlines():
137
- value = value.strip()
138
- try:
139
- re.compile(value)
140
- except re.error as e:
141
- raise ConfigError(
142
- f"Invalid [{section}].{option} value {value!r}: {e}",
143
- ) from e
144
- if value:
145
- value_list.append(value)
146
- return value_list
143
+ return process_regexlist(section, option, line_list.splitlines())
147
144
 
148
145
 
149
- TConfigParser = Union[HandyConfigParser, TomlConfigParser]
146
+ TConfigParser = HandyConfigParser | TomlConfigParser
150
147
 
151
148
 
152
149
  # The default line exclusion regexes.
153
150
  DEFAULT_EXCLUDE = [
154
151
  r"#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)",
152
+ r"^\s*(((async )?def .*?)?\)(\s*->.*?)?:\s*)?\.\.\.\s*(#|$)",
153
+ r"if (typing\.)?TYPE_CHECKING:",
155
154
  ]
156
155
 
157
156
  # The default partial branch regexes, to be modified by the user.
@@ -175,6 +174,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
175
174
  operation of coverage.py.
176
175
 
177
176
  """
177
+
178
178
  # pylint: disable=too-many-instance-attributes
179
179
 
180
180
  def __init__(self) -> None:
@@ -197,6 +197,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
197
197
  self.command_line: str | None = None
198
198
  self.concurrency: list[str] = []
199
199
  self.context: str | None = None
200
+ self.core: str | None = None
200
201
  self.cover_pylib = False
201
202
  self.data_file = ".coverage"
202
203
  self.debug: list[str] = []
@@ -204,6 +205,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
204
205
  self.disable_warnings: list[str] = []
205
206
  self.dynamic_context: str | None = None
206
207
  self.parallel = False
208
+ self.patch: list[str] = []
207
209
  self.plugins: list[str] = []
208
210
  self.relative_files = False
209
211
  self.run_include: list[str] = []
@@ -211,6 +213,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
211
213
  self.sigterm = False
212
214
  self.source: list[str] | None = None
213
215
  self.source_pkgs: list[str] = []
216
+ self.source_dirs: list[str] = []
214
217
  self.timid = False
215
218
  self._crash: str | None = None
216
219
 
@@ -225,6 +228,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
225
228
  self.report_omit: list[str] | None = None
226
229
  self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:]
227
230
  self.partial_list = DEFAULT_PARTIAL[:]
231
+ self.partial_also: list[str] = []
228
232
  self.precision = 0
229
233
  self.report_contexts: list[str] | None = None
230
234
  self.show_missing = False
@@ -260,9 +264,25 @@ class CoverageConfig(TConfigurable, TPluginConfig):
260
264
  self.plugin_options: dict[str, TConfigSectionOut] = {}
261
265
 
262
266
  MUST_BE_LIST = {
263
- "debug", "concurrency", "plugins",
264
- "report_omit", "report_include",
265
- "run_omit", "run_include",
267
+ "debug",
268
+ "concurrency",
269
+ "plugins",
270
+ "report_omit",
271
+ "report_include",
272
+ "run_omit",
273
+ "run_include",
274
+ "patch",
275
+ }
276
+
277
+ # File paths to make absolute during serialization.
278
+ # The pairs are (config_key, must_exist).
279
+ SERIALIZE_ABSPATH = {
280
+ ("data_file", False),
281
+ ("debug_file", False),
282
+ # `source` can be directories or modules, so don't abspath it if it
283
+ # doesn't exist.
284
+ ("source", True),
285
+ ("source_dirs", False),
266
286
  }
267
287
 
268
288
  def from_args(self, **kwargs: TConfigValueIn) -> None:
@@ -325,7 +345,9 @@ class CoverageConfig(TConfigurable, TPluginConfig):
325
345
  for unknown in set(cp.options(section)) - options:
326
346
  warn(
327
347
  "Unrecognized option '[{}] {}=' in config file {}".format(
328
- real_section, unknown, filename,
348
+ real_section,
349
+ unknown,
350
+ filename,
329
351
  ),
330
352
  )
331
353
 
@@ -360,7 +382,16 @@ class CoverageConfig(TConfigurable, TPluginConfig):
360
382
  """Return a copy of the configuration."""
361
383
  return copy.deepcopy(self)
362
384
 
363
- CONCURRENCY_CHOICES = {"thread", "gevent", "greenlet", "eventlet", "multiprocessing"}
385
+ CONCURRENCY_CHOICES: Final[set[str]] = {
386
+ "thread",
387
+ "gevent",
388
+ "greenlet",
389
+ "eventlet",
390
+ "multiprocessing",
391
+ }
392
+
393
+ # Mutually exclusive concurrency settings.
394
+ LIGHT_THREADS = {"greenlet", "eventlet", "gevent"}
364
395
 
365
396
  CONFIG_FILE_OPTIONS = [
366
397
  # These are *args for _set_attr_from_config_option:
@@ -370,19 +401,21 @@ class CoverageConfig(TConfigurable, TPluginConfig):
370
401
  # where is the section:name to read from the configuration file.
371
402
  # type_ is the optional type to apply, by using .getTYPE to read the
372
403
  # configuration value from the file.
373
-
404
+ #
374
405
  # [run]
375
406
  ("branch", "run:branch", "boolean"),
376
407
  ("command_line", "run:command_line"),
377
408
  ("concurrency", "run:concurrency", "list"),
378
409
  ("context", "run:context"),
410
+ ("core", "run:core"),
379
411
  ("cover_pylib", "run:cover_pylib", "boolean"),
380
- ("data_file", "run:data_file"),
412
+ ("data_file", "run:data_file", "file"),
381
413
  ("debug", "run:debug", "list"),
382
- ("debug_file", "run:debug_file"),
414
+ ("debug_file", "run:debug_file", "file"),
383
415
  ("disable_warnings", "run:disable_warnings", "list"),
384
416
  ("dynamic_context", "run:dynamic_context"),
385
417
  ("parallel", "run:parallel", "boolean"),
418
+ ("patch", "run:patch", "list"),
386
419
  ("plugins", "run:plugins", "list"),
387
420
  ("relative_files", "run:relative_files", "boolean"),
388
421
  ("run_include", "run:include", "list"),
@@ -390,9 +423,10 @@ class CoverageConfig(TConfigurable, TPluginConfig):
390
423
  ("sigterm", "run:sigterm", "boolean"),
391
424
  ("source", "run:source", "list"),
392
425
  ("source_pkgs", "run:source_pkgs", "list"),
426
+ ("source_dirs", "run:source_dirs", "list"),
393
427
  ("timid", "run:timid", "boolean"),
394
428
  ("_crash", "run:_crash"),
395
-
429
+ #
396
430
  # [report]
397
431
  ("exclude_list", "report:exclude_lines", "regexlist"),
398
432
  ("exclude_also", "report:exclude_also", "regexlist"),
@@ -402,6 +436,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
402
436
  ("include_namespace_packages", "report:include_namespace_packages", "boolean"),
403
437
  ("partial_always_list", "report:partial_branches_always", "regexlist"),
404
438
  ("partial_list", "report:partial_branches", "regexlist"),
439
+ ("partial_also", "report:partial_also", "regexlist"),
405
440
  ("precision", "report:precision", "int"),
406
441
  ("report_contexts", "report:contexts", "list"),
407
442
  ("report_include", "report:include", "list"),
@@ -410,27 +445,27 @@ class CoverageConfig(TConfigurable, TPluginConfig):
410
445
  ("skip_covered", "report:skip_covered", "boolean"),
411
446
  ("skip_empty", "report:skip_empty", "boolean"),
412
447
  ("sort", "report:sort"),
413
-
448
+ #
414
449
  # [html]
415
450
  ("extra_css", "html:extra_css"),
416
- ("html_dir", "html:directory"),
451
+ ("html_dir", "html:directory", "file"),
417
452
  ("html_skip_covered", "html:skip_covered", "boolean"),
418
453
  ("html_skip_empty", "html:skip_empty", "boolean"),
419
454
  ("html_title", "html:title"),
420
455
  ("show_contexts", "html:show_contexts", "boolean"),
421
-
456
+ #
422
457
  # [xml]
423
- ("xml_output", "xml:output"),
458
+ ("xml_output", "xml:output", "file"),
424
459
  ("xml_package_depth", "xml:package_depth", "int"),
425
-
460
+ #
426
461
  # [json]
427
- ("json_output", "json:output"),
462
+ ("json_output", "json:output", "file"),
428
463
  ("json_pretty_print", "json:pretty_print", "boolean"),
429
464
  ("json_show_contexts", "json:show_contexts", "boolean"),
430
-
465
+ #
431
466
  # [lcov]
432
- ("lcov_output", "lcov:output"),
433
- ("lcov_line_checksums", "lcov:line_checksums", "boolean")
467
+ ("lcov_output", "lcov:output", "file"),
468
+ ("lcov_line_checksums", "lcov:line_checksums", "boolean"),
434
469
  ]
435
470
 
436
471
  def _set_attr_from_config_option(
@@ -447,7 +482,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
447
482
  """
448
483
  section, option = where.split(":")
449
484
  if cp.has_option(section, option):
450
- method = getattr(cp, "get" + type_)
485
+ method = getattr(cp, f"get{type_}")
451
486
  setattr(self, attr, method(section, option))
452
487
  return True
453
488
  return False
@@ -468,7 +503,13 @@ class CoverageConfig(TConfigurable, TPluginConfig):
468
503
  """
469
504
  # Special-cased options.
470
505
  if option_name == "paths":
471
- self.paths = value # type: ignore[assignment]
506
+ # This is ugly, but type-checks and ensures the values are close
507
+ # to right.
508
+ self.paths = {}
509
+ assert isinstance(value, Mapping)
510
+ for k, v in value.items():
511
+ assert isinstance(v, Iterable)
512
+ self.paths[k] = list(v)
472
513
  return
473
514
 
474
515
  # Check all the hard-coded options.
@@ -481,7 +522,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
481
522
  # See if it's a plugin option.
482
523
  plugin_name, _, key = option_name.partition(":")
483
524
  if key and plugin_name in self.plugins:
484
- self.plugin_options.setdefault(plugin_name, {})[key] = value # type: ignore[index]
525
+ self.plugin_options.setdefault(plugin_name, {})[key] = value # type: ignore[index]
485
526
  return
486
527
 
487
528
  # If we get here, we didn't find the option.
@@ -499,7 +540,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
499
540
  """
500
541
  # Special-cased options.
501
542
  if option_name == "paths":
502
- return self.paths # type: ignore[return-value]
543
+ return self.paths
503
544
 
504
545
  # Check all the hard-coded options.
505
546
  for option_spec in self.CONFIG_FILE_OPTIONS:
@@ -515,26 +556,82 @@ class CoverageConfig(TConfigurable, TPluginConfig):
515
556
  # If we get here, we didn't find the option.
516
557
  raise ConfigError(f"No such option: {option_name!r}")
517
558
 
518
- def post_process_file(self, path: str) -> str:
519
- """Make final adjustments to a file path to make it usable."""
520
- return os.path.expanduser(path)
521
-
522
559
  def post_process(self) -> None:
523
560
  """Make final adjustments to settings to make them usable."""
524
- self.data_file = self.post_process_file(self.data_file)
525
- self.html_dir = self.post_process_file(self.html_dir)
526
- self.xml_output = self.post_process_file(self.xml_output)
527
- self.paths = {
528
- k: [self.post_process_file(f) for f in v]
529
- for k, v in self.paths.items()
530
- }
561
+ self.paths = {k: [process_file_value(f) for f in v] for k, v in self.paths.items()}
562
+
531
563
  self.exclude_list += self.exclude_also
564
+ self.partial_list += self.partial_also
565
+
566
+ if "subprocess" in self.patch:
567
+ self.parallel = True
568
+
569
+ # We can handle a few concurrency options here, but only one at a time.
570
+ concurrencies = set(self.concurrency)
571
+ unknown = concurrencies - self.CONCURRENCY_CHOICES
572
+ if unknown:
573
+ show = ", ".join(sorted(unknown))
574
+ raise ConfigError(f"Unknown concurrency choices: {show}")
575
+ light_threads = concurrencies & self.LIGHT_THREADS
576
+ if len(light_threads) > 1:
577
+ show = ", ".join(sorted(light_threads))
578
+ raise ConfigError(f"Conflicting concurrency settings: {show}")
532
579
 
533
580
  def debug_info(self) -> list[tuple[str, Any]]:
534
581
  """Make a list of (name, value) pairs for writing debug info."""
535
- return human_sorted_items(
536
- (k, v) for k, v in self.__dict__.items() if not k.startswith("_")
537
- )
582
+ return human_sorted_items((k, v) for k, v in self.__dict__.items() if not k.startswith("_"))
583
+
584
+ def serialize(self) -> str:
585
+ """Convert to a string that can be ingested with `deserialize`.
586
+
587
+ File paths used by `coverage run` are made absolute to ensure the
588
+ deserialized config will refer to the same files.
589
+ """
590
+ data = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
591
+ for k, must_exist in self.SERIALIZE_ABSPATH:
592
+ abs_fn = abs_path_if_exists if must_exist else os.path.abspath
593
+ v = data[k]
594
+ if isinstance(v, list):
595
+ v = list(map(abs_fn, v))
596
+ elif isinstance(v, str):
597
+ v = abs_fn(v)
598
+ data[k] = v
599
+ return base64.b64encode(json.dumps(data).encode()).decode()
600
+
601
+ @classmethod
602
+ def deserialize(cls, config_str: str) -> CoverageConfig:
603
+ """Take a string from `serialize`, and make a CoverageConfig."""
604
+ data = json.loads(base64.b64decode(config_str.encode()).decode())
605
+ config = cls()
606
+ config.__dict__.update(data)
607
+ return config
608
+
609
+
610
+ def process_file_value(path: str) -> str:
611
+ """Make adjustments to a file path to make it usable."""
612
+ return os.path.expanduser(path)
613
+
614
+
615
+ def abs_path_if_exists(path: str) -> str:
616
+ """os.path.abspath, but only if the path exists."""
617
+ if os.path.exists(path):
618
+ return os.path.abspath(path)
619
+ else:
620
+ return path
621
+
622
+
623
+ def process_regexlist(name: str, option: str, values: list[str]) -> list[str]:
624
+ """Check the values in a regex list and keep the non-blank ones."""
625
+ value_list = []
626
+ for value in values:
627
+ value = value.strip()
628
+ try:
629
+ re.compile(value)
630
+ except re.error as e:
631
+ raise ConfigError(f"Invalid [{name}].{option} value {value!r}: {e}") from e
632
+ if value:
633
+ value_list.append(value)
634
+ return value_list
538
635
 
539
636
 
540
637
  def config_files_to_try(config_file: bool | str) -> list[tuple[str, bool, bool]]:
@@ -548,7 +645,7 @@ def config_files_to_try(config_file: bool | str) -> list[tuple[str, bool, bool]]
548
645
  # True, so make it so.
549
646
  if config_file == ".coveragerc":
550
647
  config_file = True
551
- specified_file = (config_file is not True)
648
+ specified_file = config_file is not True
552
649
  if not specified_file:
553
650
  # No file was specified. Check COVERAGE_RCFILE.
554
651
  rcfile = os.getenv("COVERAGE_RCFILE")
@@ -607,11 +704,18 @@ def read_coverage_config(
607
704
  env_data_file = os.getenv("COVERAGE_FILE")
608
705
  if env_data_file:
609
706
  config.data_file = env_data_file
707
+
610
708
  # $set_env.py: COVERAGE_DEBUG - Debug options: https://coverage.rtfd.io/cmd.html#debug
611
709
  debugs = os.getenv("COVERAGE_DEBUG")
612
710
  if debugs:
613
711
  config.debug.extend(d.strip() for d in debugs.split(","))
614
712
 
713
+ # Read the COVERAGE_CORE environment variable for backward compatibility,
714
+ # and because we use it in the test suite to pick a specific core.
715
+ env_core = os.getenv("COVERAGE_CORE")
716
+ if env_core:
717
+ config.core = env_core
718
+
615
719
  # 4) from constructor arguments:
616
720
  config.from_args(**kwargs)
617
721
 
coverage/context.py CHANGED
@@ -5,9 +5,8 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from types import FrameType
9
- from typing import cast
10
8
  from collections.abc import Sequence
9
+ from types import FrameType
11
10
 
12
11
  from coverage.types import TShouldStartContextFn
13
12
 
@@ -65,11 +64,11 @@ def qualname_from_frame(frame: FrameType) -> str | None:
65
64
  func = frame.f_globals.get(fname)
66
65
  if func is None:
67
66
  return None
68
- return cast(str, func.__module__ + "." + fname)
67
+ return f"{func.__module__}.{fname}"
69
68
 
70
69
  func = getattr(method, "__func__", None)
71
70
  if func is None:
72
71
  cls = self.__class__
73
- return cast(str, cls.__module__ + "." + cls.__name__ + "." + fname)
72
+ return f"{cls.__module__}.{cls.__name__}.{fname}"
74
73
 
75
- return cast(str, func.__module__ + "." + func.__qualname__)
74
+ return f"{func.__module__}.{func.__qualname__}"