Coverage for coverage / tomlconfig.py: 100.000%
126 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-29 20:34 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-29 20:34 +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 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
8import os 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
9import re 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
10from collections.abc import Iterable 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
11from typing import Any, Callable, TypeVar 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
13from coverage import config, env 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
14from coverage.exceptions import ConfigError 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
15from coverage.misc import import_third_party, isolate_module, substitute_variables 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
16from coverage.types import TConfigSectionOut, TConfigValueOut 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
18os = isolate_module(os) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
20if env.PYVERSION >= (3, 11, 0, "alpha", 7): 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
21 import tomllib # pylint: disable=import-error 1hijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)78
23 has_tomllib = True 1hijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)78
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): 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
30 """An exception class that exists even when toml isn't installed."""
32 pass 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
35TWant = TypeVar("TWant") 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
38class TomlConfigParser: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
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: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
46 self.our_file = our_file 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
47 self.data: dict[str, Any] = {} 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
49 def read(self, filenames: Iterable[str]) -> list[str]: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
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)) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
53 filename = os.fspath(filenames) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
55 try: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
56 with open(filename, encoding="utf-8") as fp: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
57 toml_text = fp.read() 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMN9OPQRSTUVWXYZ01!23456g78
58 except OSError: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
59 return [] 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
60 if has_tomllib: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMN9OPQRSTUVWXYZ01!23456g78
61 try: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMN9OPQRSTUVWXYZ01!23456g78
62 self.data = tomllib.loads(toml_text) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMN9OPQRSTUVWXYZ01!23456g78
63 except tomllib.TOMLDecodeError as err: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456g78
64 raise TomlDecodeError(str(err)) from err 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456g78
65 return [filename] 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMN9OPQRSTUVWXYZ01!23456g78
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]: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
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."] 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
87 for prefix in prefixes: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
88 real_section = prefix + section 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
89 parts = real_section.split(".") 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
90 try: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
91 data = self.data[parts[0]] 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
92 for part in parts[1:]: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
93 data = data[part] 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
94 except KeyError: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
95 continue 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
96 break 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKLMN9OPQRSTU$VWXYZ01!23456g78
97 else:
98 return None, None 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
99 return real_section, data 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKLMN9OPQRSTU$VWXYZ01!23456g78
101 def _get(self, section: str, option: str) -> tuple[str, TConfigValueOut]: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
102 """Like .get, but returns the real section name and the value."""
103 name, data = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
104 if data is None: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
105 raise ConfigError(f"No section: {section!r}") 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
106 assert name is not None 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
107 try: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
108 value = data[option] 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
109 except KeyError: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
110 raise ConfigError(f"No option {option!r} in section: {name!r}") from None 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
111 return name, value 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
113 def _get_single(self, section: str, option: str) -> Any: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
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) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
120 if isinstance(value, str): 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
121 value = substitute_variables(value, os.environ) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
122 return name, value 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
124 def has_option(self, section: str, option: str) -> bool: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
125 _, data = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
126 if data is None: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
127 return False 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
128 return option in data 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKLMN9OPQRSTU$VWXYZ01!23456g78
130 def real_section(self, section: str) -> str | None: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
131 name, _ = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
132 return name 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
134 def has_section(self, section: str) -> bool: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
135 name, _ = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
136 return bool(name) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
138 def options(self, section: str) -> list[str]: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
139 _, data = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
140 if data is None: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
141 raise ConfigError(f"No section: {section!r}") 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
142 return list(data.keys()) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
144 def get_section(self, section: str) -> TConfigSectionOut: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
145 _, data = self._get_section(section) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
146 return data or {} 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
148 def get(self, section: str, option: str) -> Any: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
149 _, value = self._get_single(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
150 return value 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
152 def _check_type( 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
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_): 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
166 return value 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
167 if isinstance(value, str) and converter is not None: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
168 try: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
169 return converter(value) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
170 except Exception as e: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
171 raise ValueError( 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
172 f"Option [{section}]{option} couldn't convert to {type_desc}: {value!r}",
173 ) from e
174 raise ValueError( 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
175 f"Option [{section}]{option} is not {type_desc}: {value!r}",
176 )
178 def getboolean(self, section: str, option: str) -> bool: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
179 name, value = self._get_single(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
180 bool_strings = {"true": True, "false": False} 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
181 return self._check_type(name, option, value, bool, bool_strings.__getitem__, "a boolean") 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
183 def getfile(self, section: str, option: str) -> str: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
184 _, value = self._get_single(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
185 return config.process_file_value(value) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
187 def _get_list(self, section: str, option: str) -> tuple[str, list[str]]: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
188 """Get a list of strings, substituting environment variables in the elements."""
189 name, values = self._get(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
190 values = self._check_type(name, option, values, list, None, "a list") 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
191 values = [substitute_variables(value, os.environ) for value in values] 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
192 return name, values 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
194 def getlist(self, section: str, option: str) -> list[str]: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
195 _, values = self._get_list(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
196 return values 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
198 def getregexlist(self, section: str, option: str) -> list[str]: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
199 name, values = self._get_list(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
200 return config.process_regexlist(name, option, values) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
202 def getint(self, section: str, option: str) -> int: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
203 name, value = self._get_single(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
204 return self._check_type(name, option, value, int, int, "an integer") 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
206 def getfloat(self, section: str, option: str) -> float: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
207 name, value = self._get_single(section, option) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
208 if isinstance(value, int): 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
209 value = float(value) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78
210 return self._check_type(name, option, value, float, float, "a float") 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78