Coverage for coverage / config.py: 100.000%
326 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
4"""Config file for coverage.py"""
6from __future__ import annotations 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
8import base64 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
9import collections 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
10import configparser 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
11import copy 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
12import json 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
13import os 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
14import os.path 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
15import re 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
16from collections.abc import Iterable 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
17from typing import Any, Callable, Final, Mapping 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
19from coverage.exceptions import ConfigError 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
20from coverage.misc import human_sorted_items, isolate_module, substitute_variables 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
21from coverage.tomlconfig import TomlConfigParser, TomlDecodeError 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
22from coverage.types import ( 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
23 TConfigSectionIn,
24 TConfigSectionOut,
25 TConfigurable,
26 TConfigValueIn,
27 TConfigValueOut,
28 TPluginConfig,
29)
31os = isolate_module(os) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
34class HandyConfigParser(configparser.ConfigParser): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
35 """Our specialization of ConfigParser."""
37 def __init__(self, our_file: bool) -> None: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
38 """Create the HandyConfigParser.
40 `our_file` is True if this config file is specifically for coverage,
41 False if we are examining another config file (tox.ini, setup.cfg)
42 for possible settings.
43 """
45 super().__init__(interpolation=None) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
46 self.section_prefixes = ["coverage:"] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
47 if our_file: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
48 self.section_prefixes.append("") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
50 def read( # type: ignore[override] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)
51 self,
52 filenames: Iterable[str],
53 encoding_unused: str | None = None,
54 ) -> list[str]:
55 """Read a file name as UTF-8 configuration data."""
56 return super().read(filenames, encoding="utf-8") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
58 def real_section(self, section: str) -> str | None: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
59 """Get the actual name of a section."""
60 for section_prefix in self.section_prefixes: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
61 real_section = section_prefix + section 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
62 has = super().has_section(real_section) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
63 if has: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
64 return real_section 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
65 return None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
67 def has_option(self, section: str, option: str) -> bool: # type: ignore[override] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
68 real_section = self.real_section(section) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
69 if real_section is not None: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
70 return super().has_option(real_section, option) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
71 return False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
73 def has_section(self, section: str) -> bool: # type: ignore[override] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
74 return bool(self.real_section(section)) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
76 def options(self, section: str) -> list[str]: # type: ignore[override] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
77 real_section = self.real_section(section) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
78 if real_section is not None: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
79 return super().options(real_section) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
80 raise ConfigError(f"No section: {section!r}") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCDEF!YZGH7IJ01234
82 def get_section(self, section: str) -> TConfigSectionOut: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
83 """Get the contents of a section, as a dictionary."""
84 d: dict[str, TConfigValueOut] = {} 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
85 for opt in self.options(section): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
86 d[opt] = self.get(section, opt) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
87 return d 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
89 def get(self, section: str, option: str, *args: Any, **kwargs: Any) -> str: # type: ignore 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
90 """Get a value, replacing environment variables also.
92 The arguments are the same as `ConfigParser.get`, but in the found
93 value, ``$WORD`` or ``${WORD}`` are replaced by the value of the
94 environment variable ``WORD``.
96 Returns the finished value.
98 """
99 for section_prefix in self.section_prefixes: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
100 real_section = section_prefix + section 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
101 if super().has_option(real_section, option): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
102 break 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
103 else:
104 raise ConfigError(f"No option {option!r} in section: {section!r}") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCDEF!YZGH7IJ01234
106 v: str = super().get(real_section, option, *args, **kwargs) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
107 v = substitute_variables(v, os.environ) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
108 return v 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
110 def getfile(self, section: str, option: str) -> str: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
111 """Fix up a file path setting."""
112 path = self.get(section, option) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
113 return process_file_value(path) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
115 def getlist(self, section: str, option: str) -> list[str]: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
116 """Read a list of strings.
118 The value of `section` and `option` is treated as a comma- and newline-
119 separated list of strings. Each value is stripped of white space.
121 Returns the list of strings.
123 """
124 value_list = self.get(section, option) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
125 values = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
126 for value_line in value_list.split("\n"): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
127 for value in value_line.split(","): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
128 value = value.strip() 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
129 if value: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
130 values.append(value) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
131 return values 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
133 def getregexlist(self, section: str, option: str) -> list[str]: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
134 """Read a list of full-line regexes.
136 The value of `section` and `option` is treated as a newline-separated
137 list of regexes. Each value is stripped of white space.
139 Returns the list of strings.
141 """
142 line_list = self.get(section, option) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
143 return process_regexlist(section, option, line_list.splitlines()) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
146TConfigParser = HandyConfigParser | TomlConfigParser 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
149# The default line exclusion regexes.
150DEFAULT_EXCLUDE = [ 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
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:",
154]
156# The default partial branch regexes, to be modified by the user.
157DEFAULT_PARTIAL = [ 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
158 r"#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(branch|BRANCH)",
159]
161# The default partial branch regexes, based on Python semantics.
162# These are any Python branching constructs that can't actually execute all
163# their branches.
164DEFAULT_PARTIAL_ALWAYS = [ 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
165 "while (True|1|False|0):",
166 "if (True|1|False|0):",
167]
170class CoverageConfig(TConfigurable, TPluginConfig): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
171 """Coverage.py configuration.
173 The attributes of this class are the various settings that control the
174 operation of coverage.py.
176 """
178 # pylint: disable=too-many-instance-attributes
180 def __init__(self) -> None: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
181 """Initialize the configuration attributes to their defaults."""
182 # Metadata about the config.
183 # We tried to read these config files.
184 self.config_files_attempted: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
185 # We did read these config files, but maybe didn't find any content for us.
186 self.config_files_read: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
187 # The file that gave us our configuration.
188 self.config_file: str | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
189 self._config_contents: bytes | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
191 # Defaults for [run] and [report]
192 self._include = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
193 self._omit = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
195 # Defaults for [run]
196 self.branch = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
197 self.command_line: str | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
198 self.concurrency: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
199 self.context: str | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
200 self.core: str | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
201 self.cover_pylib = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
202 self.data_file = ".coverage" 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
203 self.debug: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
204 self.debug_file: str | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
205 self.disable_warnings: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
206 self.dynamic_context: str | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
207 self.parallel = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
208 self.patch: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
209 self.plugins: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
210 self.relative_files = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
211 self.run_include: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
212 self.run_omit: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
213 self.sigterm = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
214 self.source: list[str] | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
215 self.source_pkgs: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
216 self.source_dirs: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
217 self.timid = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
218 self._crash: str | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
220 # Defaults for [report]
221 self.exclude_list = DEFAULT_EXCLUDE[:] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
222 self.exclude_also: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
223 self.fail_under = 0.0 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
224 self.format: str | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
225 self.ignore_errors = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
226 self.include_namespace_packages = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
227 self.report_include: list[str] | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
228 self.report_omit: list[str] | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
229 self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
230 self.partial_list = DEFAULT_PARTIAL[:] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
231 self.partial_also: list[str] = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
232 self.precision = 0 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
233 self.report_contexts: list[str] | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
234 self.show_missing = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
235 self.skip_covered = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
236 self.skip_empty = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
237 self.sort: str | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
239 # Defaults for [html]
240 self.extra_css: str | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
241 self.html_dir = "htmlcov" 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
242 self.html_skip_covered: bool | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
243 self.html_skip_empty: bool | None = None 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
244 self.html_title = "Coverage report" 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
245 self.show_contexts = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
247 # Defaults for [xml]
248 self.xml_output = "coverage.xml" 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
249 self.xml_package_depth = 99 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
251 # Defaults for [json]
252 self.json_output = "coverage.json" 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
253 self.json_pretty_print = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
254 self.json_show_contexts = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
256 # Defaults for [lcov]
257 self.lcov_output = "coverage.lcov" 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
258 self.lcov_line_checksums = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
260 # Defaults for [paths]
261 self.paths: dict[str, list[str]] = {} 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
263 # Options for plugins
264 self.plugin_options: dict[str, TConfigSectionOut] = {} 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
266 MUST_BE_LIST = { 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
267 "debug",
268 "concurrency",
269 "plugins",
270 "report_omit",
271 "report_include",
272 "run_omit",
273 "run_include",
274 "patch",
275 }
277 # File paths to make absolute during serialization.
278 # The pairs are (config_key, must_exist).
279 SERIALIZE_ABSPATH = { 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
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),
286 }
288 def from_args(self, **kwargs: TConfigValueIn) -> None: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
289 """Read config values from `kwargs`."""
290 for k, v in kwargs.items(): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
291 if v is not None: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
292 if k in self.MUST_BE_LIST and isinstance(v, str): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
293 v = [v] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wxUV'yz6ABWXCD#EFYZGH7IJ01234
294 setattr(self, k, v) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
296 def from_file(self, filename: str, warn: Callable[[str], None], our_file: bool) -> bool: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
297 """Read configuration from a .rc file.
299 `filename` is a file name to read.
301 `our_file` is True if this config file is specifically for coverage,
302 False if we are examining another config file (tox.ini, setup.cfg)
303 for possible settings.
305 Returns True or False, whether the file could be read, and it had some
306 coverage.py settings in it.
308 """
309 _, ext = os.path.splitext(filename) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
310 cp: TConfigParser
311 if ext == ".toml": 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
312 cp = TomlConfigParser(our_file) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
313 else:
314 cp = HandyConfigParser(our_file) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
316 self.config_files_attempted.append(os.path.abspath(filename)) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
318 try: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
319 files_read = cp.read(filename) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
320 except (configparser.Error, TomlDecodeError) as err: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
321 raise ConfigError(f"Couldn't read config file {filename}: {err}") from err 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
322 if not files_read: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
323 return False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
325 self.config_files_read.extend(map(os.path.abspath, files_read)) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
327 any_set = False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
328 try: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
329 for option_spec in self.CONFIG_FILE_OPTIONS: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
330 was_set = self._set_attr_from_config_option(cp, *option_spec) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
331 if was_set: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
332 any_set = True 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
333 except ValueError as err: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
334 raise ConfigError(f"Couldn't read config file {filename}: {err}") from err 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
336 # Check that there are no unrecognized options.
337 all_options = collections.defaultdict(set) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
338 for option_spec in self.CONFIG_FILE_OPTIONS: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
339 section, option = option_spec[1].split(":") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
340 all_options[section].add(option) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
342 for section, options in all_options.items(): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
343 real_section = cp.real_section(section) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
344 if real_section: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
345 for unknown in set(cp.options(section)) - options: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
346 warn( 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
347 "Unrecognized option '[{}] {}=' in config file {}".format(
348 real_section,
349 unknown,
350 filename,
351 ),
352 )
354 # [paths] is special
355 if cp.has_section("paths"): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
356 for option in cp.options("paths"): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
357 self.paths[option] = cp.getlist("paths", option) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
358 any_set = True 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
360 # plugins can have options
361 for plugin in self.plugins: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
362 if cp.has_section(plugin): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
363 self.plugin_options[plugin] = cp.get_section(plugin) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
364 any_set = True 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
366 # Was this file used as a config file? If it's specifically our file,
367 # then it was used. If we're piggybacking on someone else's file,
368 # then it was only used if we found some settings in it.
369 if our_file: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
370 used = True 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
371 else:
372 used = any_set 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
374 if used: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
375 self.config_file = os.path.abspath(filename) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
376 with open(filename, "rb") as f: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
377 self._config_contents = f.read() 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
379 return used 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
381 def copy(self) -> CoverageConfig: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
382 """Return a copy of the configuration."""
383 return copy.deepcopy(self) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
385 CONCURRENCY_CHOICES: Final[set[str]] = { 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
386 "thread",
387 "gevent",
388 "greenlet",
389 "eventlet",
390 "multiprocessing",
391 }
393 # Mutually exclusive concurrency settings.
394 LIGHT_THREADS = {"greenlet", "eventlet", "gevent"} 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
396 CONFIG_FILE_OPTIONS = [ 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
397 # These are *args for _set_attr_from_config_option:
398 # (attr, where, type_="")
399 #
400 # attr is the attribute to set on the CoverageConfig object.
401 # where is the section:name to read from the configuration file.
402 # type_ is the optional type to apply, by using .getTYPE to read the
403 # configuration value from the file.
404 #
405 # [run]
406 ("branch", "run:branch", "boolean"),
407 ("command_line", "run:command_line"),
408 ("concurrency", "run:concurrency", "list"),
409 ("context", "run:context"),
410 ("core", "run:core"),
411 ("cover_pylib", "run:cover_pylib", "boolean"),
412 ("data_file", "run:data_file", "file"),
413 ("debug", "run:debug", "list"),
414 ("debug_file", "run:debug_file", "file"),
415 ("disable_warnings", "run:disable_warnings", "list"),
416 ("dynamic_context", "run:dynamic_context"),
417 ("parallel", "run:parallel", "boolean"),
418 ("patch", "run:patch", "list"),
419 ("plugins", "run:plugins", "list"),
420 ("relative_files", "run:relative_files", "boolean"),
421 ("run_include", "run:include", "list"),
422 ("run_omit", "run:omit", "list"),
423 ("sigterm", "run:sigterm", "boolean"),
424 ("source", "run:source", "list"),
425 ("source_pkgs", "run:source_pkgs", "list"),
426 ("source_dirs", "run:source_dirs", "list"),
427 ("timid", "run:timid", "boolean"),
428 ("_crash", "run:_crash"),
429 #
430 # [report]
431 ("exclude_list", "report:exclude_lines", "regexlist"),
432 ("exclude_also", "report:exclude_also", "regexlist"),
433 ("fail_under", "report:fail_under", "float"),
434 ("format", "report:format"),
435 ("ignore_errors", "report:ignore_errors", "boolean"),
436 ("include_namespace_packages", "report:include_namespace_packages", "boolean"),
437 ("partial_always_list", "report:partial_branches_always", "regexlist"),
438 ("partial_list", "report:partial_branches", "regexlist"),
439 ("partial_also", "report:partial_also", "regexlist"),
440 ("precision", "report:precision", "int"),
441 ("report_contexts", "report:contexts", "list"),
442 ("report_include", "report:include", "list"),
443 ("report_omit", "report:omit", "list"),
444 ("show_missing", "report:show_missing", "boolean"),
445 ("skip_covered", "report:skip_covered", "boolean"),
446 ("skip_empty", "report:skip_empty", "boolean"),
447 ("sort", "report:sort"),
448 #
449 # [html]
450 ("extra_css", "html:extra_css"),
451 ("html_dir", "html:directory", "file"),
452 ("html_skip_covered", "html:skip_covered", "boolean"),
453 ("html_skip_empty", "html:skip_empty", "boolean"),
454 ("html_title", "html:title"),
455 ("show_contexts", "html:show_contexts", "boolean"),
456 #
457 # [xml]
458 ("xml_output", "xml:output", "file"),
459 ("xml_package_depth", "xml:package_depth", "int"),
460 #
461 # [json]
462 ("json_output", "json:output", "file"),
463 ("json_pretty_print", "json:pretty_print", "boolean"),
464 ("json_show_contexts", "json:show_contexts", "boolean"),
465 #
466 # [lcov]
467 ("lcov_output", "lcov:output", "file"),
468 ("lcov_line_checksums", "lcov:line_checksums", "boolean"),
469 ]
471 def _set_attr_from_config_option( 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)
472 self,
473 cp: TConfigParser,
474 attr: str,
475 where: str,
476 type_: str = "",
477 ) -> bool:
478 """Set an attribute on self if it exists in the ConfigParser.
480 Returns True if the attribute was set.
482 """
483 section, option = where.split(":") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
484 if cp.has_option(section, option): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
485 method = getattr(cp, f"get{type_}") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
486 setattr(self, attr, method(section, option)) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
487 return True 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
488 return False 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
490 def get_plugin_options(self, plugin: str) -> TConfigSectionOut: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
491 """Get a dictionary of options for the plugin named `plugin`."""
492 return self.plugin_options.get(plugin, {}) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
494 def set_option(self, option_name: str, value: TConfigValueIn | TConfigSectionIn) -> None: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
495 """Set an option in the configuration.
497 `option_name` is a colon-separated string indicating the section and
498 option name. For example, the ``branch`` option in the ``[run]``
499 section of the config file would be indicated with `"run:branch"`.
501 `value` is the new value for the option.
503 """
504 # Special-cased options.
505 if option_name == "paths": 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
506 # This is ugly, but type-checks and ensures the values are close
507 # to right.
508 self.paths = {} 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZ%GH7IJ$01234
509 assert isinstance(value, Mapping) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZ%GH7IJ$01234
510 for k, v in value.items(): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZ%GH7IJ$01234
511 assert isinstance(v, Iterable) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZ%GH7IJ$01234
512 self.paths[k] = list(v) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZ%GH7IJ$01234
513 return 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZ%GH7IJ$01234
515 # Check all the hard-coded options.
516 for option_spec in self.CONFIG_FILE_OPTIONS: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
517 attr, where = option_spec[:2] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
518 if where == option_name: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
519 setattr(self, attr, value) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
520 return 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
522 # See if it's a plugin option.
523 plugin_name, _, key = option_name.partition(":") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZ%GH7IJ$01234
524 if key and plugin_name in self.plugins: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZ%GH7IJ$01234
525 self.plugin_options.setdefault(plugin_name, {})[key] = value # type: ignore[index] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZ%GH7IJ$01234
526 return 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZ%GH7IJ$01234
528 # If we get here, we didn't find the option.
529 raise ConfigError(f"No such option: {option_name!r}") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZ%GH7IJ$01234
531 def get_option(self, option_name: str) -> TConfigValueOut | None: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
532 """Get an option from the configuration.
534 `option_name` is a colon-separated string indicating the section and
535 option name. For example, the ``branch`` option in the ``[run]``
536 section of the config file would be indicated with `"run:branch"`.
538 Returns the value of the option.
540 """
541 # Special-cased options.
542 if option_name == "paths": 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
543 return self.paths 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
545 # Check all the hard-coded options.
546 for option_spec in self.CONFIG_FILE_OPTIONS: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
547 attr, where = option_spec[:2] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
548 if where == option_name: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
549 return getattr(self, attr) # type: ignore[no-any-return] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
551 # See if it's a plugin option.
552 plugin_name, _, key = option_name.partition(":") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
553 if key and plugin_name in self.plugins: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
554 return self.plugin_options.get(plugin_name, {}).get(key) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
556 # If we get here, we didn't find the option.
557 raise ConfigError(f"No such option: {option_name!r}") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
559 def post_process(self) -> None: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
560 """Make final adjustments to settings to make them usable."""
561 self.paths = {k: [process_file_value(f) for f in v] for k, v in self.paths.items()} 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
563 self.exclude_list += self.exclude_also 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
564 self.partial_list += self.partial_also 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
566 if "subprocess" in self.patch: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
567 self.parallel = True 1abcdefghijklmnopqrstuv5wx8yz6AB9CD#EF!GH7IJ$23
569 # We can handle a few concurrency options here, but only one at a time.
570 concurrencies = set(self.concurrency) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
571 unknown = concurrencies - self.CONCURRENCY_CHOICES 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
572 if unknown: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
573 show = ", ".join(sorted(unknown)) 1abcdKLefghMNijklOPmnopQRqrstSTuvwxUVyzABWXCDEFYZGHIJ01234
574 raise ConfigError(f"Unknown concurrency choices: {show}") 1abcdKLefghMNijklOPmnopQRqrstSTuvwxUVyzABWXCDEFYZGHIJ01234
575 light_threads = concurrencies & self.LIGHT_THREADS 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
576 if len(light_threads) > 1: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
577 show = ", ".join(sorted(light_threads)) 1abcdKLefghMNijklOPmnopQRqrstSTuvwxUVyzABWXCDEFYZGHIJ01234
578 raise ConfigError(f"Conflicting concurrency settings: {show}") 1abcdKLefghMNijklOPmnopQRqrstSTuvwxUVyzABWXCDEFYZGHIJ01234
580 def debug_info(self) -> list[tuple[str, Any]]: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
581 """Make a list of (name, value) pairs for writing debug info."""
582 return human_sorted_items((k, v) for k, v in self.__dict__.items() if not k.startswith("_")) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
584 def serialize(self) -> str: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
585 """Convert to a string that can be ingested with `deserialize`.
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("_")} 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
591 for k, must_exist in self.SERIALIZE_ABSPATH: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
592 abs_fn = abs_path_if_exists if must_exist else os.path.abspath 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
593 v = data[k] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
594 if isinstance(v, list): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
595 v = list(map(abs_fn, v)) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
596 elif isinstance(v, str): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
597 v = abs_fn(v) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
598 data[k] = v 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
599 return base64.b64encode(json.dumps(data).encode()).decode() 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
601 @classmethod 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
602 def deserialize(cls, config_str: str) -> CoverageConfig: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
603 """Take a string from `serialize`, and make a CoverageConfig."""
604 data = json.loads(base64.b64decode(config_str.encode()).decode()) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
605 config = cls() 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
606 config.__dict__.update(data) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
607 return config 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
610def process_file_value(path: str) -> str: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
611 """Make adjustments to a file path to make it usable."""
612 return os.path.expanduser(path) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
615def abs_path_if_exists(path: str) -> str: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
616 """os.path.abspath, but only if the path exists."""
617 if os.path.exists(path): 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
618 return os.path.abspath(path) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
619 else:
620 return path 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
623def process_regexlist(name: str, option: str, values: list[str]) -> list[str]: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
624 """Check the values in a regex list and keep the non-blank ones."""
625 value_list = [] 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
626 for value in values: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
627 value = value.strip() 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
628 try: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
629 re.compile(value) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
630 except re.error as e: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
631 raise ConfigError(f"Invalid [{name}].{option} value {value!r}: {e}") from e 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
632 if value: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
633 value_list.append(value) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
634 return value_list 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
637def config_files_to_try(config_file: bool | str) -> list[tuple[str, bool, bool]]: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
638 """What config files should we try to read?
640 Returns a list of tuples:
641 (filename, is_our_file, was_file_specified)
642 """
644 # Some API users were specifying ".coveragerc" to mean the same as
645 # True, so make it so.
646 if config_file == ".coveragerc": 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
647 config_file = True 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
648 specified_file = config_file is not True 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
649 if not specified_file: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
650 # No file was specified. Check COVERAGE_RCFILE.
651 rcfile = os.getenv("COVERAGE_RCFILE") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
652 if rcfile: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
653 config_file = rcfile 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
654 specified_file = True 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
655 if not specified_file: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
656 # Still no file specified. Default to .coveragerc
657 config_file = ".coveragerc" 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
658 assert isinstance(config_file, str) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
659 files_to_try = [ 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
660 (config_file, True, specified_file),
661 ("setup.cfg", False, False),
662 ("tox.ini", False, False),
663 ("pyproject.toml", False, False),
664 ]
665 return files_to_try 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
668def read_coverage_config( 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
669 config_file: bool | str,
670 warn: Callable[[str], None],
671 **kwargs: TConfigValueIn,
672) -> CoverageConfig:
673 """Read the coverage.py configuration.
675 Arguments:
676 config_file: a boolean or string, see the `Coverage` class for the
677 tricky details.
678 warn: a function to issue warnings.
679 all others: keyword arguments from the `Coverage` class, used for
680 setting values in the configuration.
682 Returns:
683 config:
684 config is a CoverageConfig object read from the appropriate
685 configuration file.
687 """
688 # Build the configuration from a number of sources:
689 # 1) defaults:
690 config = CoverageConfig() 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
692 # 2) from a file:
693 if config_file: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
694 files_to_try = config_files_to_try(config_file) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
696 for fname, our_file, specified_file in files_to_try: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
697 config_read = config.from_file(fname, warn, our_file=our_file) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
698 if config_read: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
699 break 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
700 if specified_file: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
701 raise ConfigError(f"Couldn't read {fname!r} as a config file") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
703 # 3) from environment variables:
704 env_data_file = os.getenv("COVERAGE_FILE") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
705 if env_data_file: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
706 config.data_file = env_data_file 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
708 # $set_env.py: COVERAGE_DEBUG - Debug options: https://coverage.rtfd.io/cmd.html#debug
709 debugs = os.getenv("COVERAGE_DEBUG") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
710 if debugs: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
711 config.debug.extend(d.strip() for d in debugs.split(",")) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
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") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
716 if env_core: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
717 config.core = env_core 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
719 # 4) from constructor arguments:
720 config.from_args(**kwargs) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
722 # 5) for our benchmark, force settings using a secret environment variable:
723 force_file = os.getenv("COVERAGE_FORCE_CONFIG") 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
724 if force_file: 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
725 config.from_file(force_file, warn, our_file=True) 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UVyz6AB9WXCD#EF!YZGH7IJ$01234
727 # Once all the config has been collected, there's a little post-processing
728 # to do.
729 config.post_process() 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234
731 return config 1abcdKLefghMNijklOPmnopQRqrstSTuv5wx8UV'yz6AB9WX(CD#EF!YZ%GH7IJ$01)234