Coverage for coverage / tomlconfig.py: 100.000%
126 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"""TOML configuration support for coverage.py"""
6from __future__ import annotations 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
8import os 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
9import re 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
10from collections.abc import Iterable 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
11from typing import Any, Callable, TypeVar 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
13from coverage import config, env 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
14from coverage.exceptions import ConfigError 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
15from coverage.misc import import_third_party, isolate_module, substitute_variables 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
16from coverage.types import TConfigSectionOut, TConfigValueOut 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
18os = isolate_module(os) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
20if env.PYVERSION >= (3, 11, 0, "alpha", 7): 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
21 import tomllib # pylint: disable=import-error 1hijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)#$
23 has_tomllib = True 1hijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)#$
24else:
25 # TOML support on Python 3.10 and below is an install-time extra option.
26 tomllib, has_tomllib = import_third_party("tomli") 1abcdefg
29class TomlDecodeError(Exception): 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
30 """An exception class that exists even when toml isn't installed."""
32 pass 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
35TWant = TypeVar("TWant") 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
38class TomlConfigParser: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
39 """TOML file reading with the interface of HandyConfigParser."""
41 # This class has the same interface as config.HandyConfigParser, no
42 # need for docstrings.
43 # pylint: disable=missing-function-docstring
45 def __init__(self, our_file: bool) -> None: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
46 self.our_file = our_file 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
47 self.data: dict[str, Any] = {} 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
49 def read(self, filenames: Iterable[str]) -> list[str]: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
50 # RawConfigParser takes a filename or list of filenames, but we only
51 # ever call this with a single filename.
52 assert isinstance(filenames, (bytes, str, os.PathLike)) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
53 filename = os.fspath(filenames) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
55 try: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
56 with open(filename, encoding="utf-8") as fp: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
57 toml_text = fp.read() 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!g#$
58 except OSError: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
59 return [] 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
60 if has_tomllib: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!g#$
61 try: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!g#$
62 self.data = tomllib.loads(toml_text) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!g#$
63 except tomllib.TOMLDecodeError as err: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!g#$
64 raise TomlDecodeError(str(err)) from err 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!g#$
65 return [filename] 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!g#$
66 else:
67 has_toml = re.search(r"^\[tool\.coverage(\.|])", toml_text, flags=re.MULTILINE) 1abcdefg
68 if self.our_file or has_toml: 1abcdefg
69 # Looks like they meant to read TOML, but we can't read it.
70 msg = "Can't read {!r} without TOML support. Install with [toml] extra" 1abcdefg
71 raise ConfigError(msg.format(filename)) 1abcdefg
72 return [] 1abcdefg
74 def _get_section(self, section: str) -> tuple[str | None, TConfigSectionOut | None]: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
75 """Get a section from the data.
77 Arguments:
78 section (str): A section name, which can be dotted.
80 Returns:
81 name (str): the actual name of the section that was found, if any,
82 or None.
83 data (str): the dict of data in the section, or None if not found.
85 """
86 prefixes = ["tool.coverage."] 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
87 for prefix in prefixes: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
88 real_section = prefix + section 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
89 parts = real_section.split(".") 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
90 try: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
91 data = self.data[parts[0]] 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
92 for part in parts[1:]: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
93 data = data[part] 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
94 except KeyError: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
95 continue 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
96 break 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
97 else:
98 return None, None 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
99 return real_section, data 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
101 def _get(self, section: str, option: str) -> tuple[str, TConfigValueOut]: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
102 """Like .get, but returns the real section name and the value."""
103 name, data = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
104 if data is None: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
105 raise ConfigError(f"No section: {section!r}") 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
106 assert name is not None 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
107 try: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
108 value = data[option] 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
109 except KeyError: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
110 raise ConfigError(f"No option {option!r} in section: {name!r}") from None 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
111 return name, value 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
113 def _get_single(self, section: str, option: str) -> Any: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
114 """Get a single-valued option.
116 Performs environment substitution if the value is a string. Other types
117 will be converted later as needed.
118 """
119 name, value = self._get(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
120 if isinstance(value, str): 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
121 value = substitute_variables(value, os.environ) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
122 return name, value 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
124 def has_option(self, section: str, option: str) -> bool: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
125 _, data = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
126 if data is None: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
127 return False 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
128 return option in data 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
130 def real_section(self, section: str) -> str | None: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
131 name, _ = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
132 return name 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
134 def has_section(self, section: str) -> bool: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
135 name, _ = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
136 return bool(name) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
138 def options(self, section: str) -> list[str]: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
139 _, data = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
140 if data is None: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
141 raise ConfigError(f"No section: {section!r}") 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
142 return list(data.keys()) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
144 def get_section(self, section: str) -> TConfigSectionOut: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
145 _, data = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
146 return data or {} 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
148 def get(self, section: str, option: str) -> Any: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
149 _, value = self._get_single(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
150 return value 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
152 def _check_type( 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
153 self,
154 section: str,
155 option: str,
156 value: Any,
157 type_: type[TWant],
158 converter: Callable[[Any], TWant] | None,
159 type_desc: str,
160 ) -> TWant:
161 """Check that `value` has the type we want, converting if needed.
163 Returns the resulting value of the desired type.
164 """
165 if isinstance(value, type_): 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
166 return value 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
167 if isinstance(value, str) and converter is not None: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
168 try: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
169 return converter(value) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
170 except Exception as e: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
171 raise ValueError( 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
172 f"Option [{section}]{option} couldn't convert to {type_desc}: {value!r}",
173 ) from e
174 raise ValueError( 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
175 f"Option [{section}]{option} is not {type_desc}: {value!r}",
176 )
178 def getboolean(self, section: str, option: str) -> bool: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
179 name, value = self._get_single(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
180 bool_strings = {"true": True, "false": False} 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
181 return self._check_type(name, option, value, bool, bool_strings.__getitem__, "a boolean") 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
183 def getfile(self, section: str, option: str) -> str: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
184 _, value = self._get_single(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
185 return config.process_file_value(value) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
187 def _get_list(self, section: str, option: str) -> tuple[str, list[str]]: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
188 """Get a list of strings, substituting environment variables in the elements."""
189 name, values = self._get(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
190 values = self._check_type(name, option, values, list, None, "a list") 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
191 values = [substitute_variables(value, os.environ) for value in values] 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
192 return name, values 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
194 def getlist(self, section: str, option: str) -> list[str]: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
195 _, values = self._get_list(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
196 return values 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
198 def getregexlist(self, section: str, option: str) -> list[str]: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
199 name, values = self._get_list(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
200 return config.process_regexlist(name, option, values) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
202 def getint(self, section: str, option: str) -> int: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
203 name, value = self._get_single(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
204 return self._check_type(name, option, value, int, int, "an integer") 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
206 def getfloat(self, section: str, option: str) -> float: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
207 name, value = self._get_single(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
208 if isinstance(value, int): 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
209 value = float(value) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$
210 return self._check_type(name, option, value, float, float, "a float") 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$