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

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 

3 

4"""TOML configuration support for coverage.py""" 

5 

6from __future__ import annotations 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$

7 

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#$

12 

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#$

17 

18os = isolate_module(os) 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$

19 

20if env.PYVERSION >= (3, 11, 0, "alpha", 7): 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$

21 import tomllib # pylint: disable=import-error 1hijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)#$

22 

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

27 

28 

29class TomlDecodeError(Exception): 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$

30 """An exception class that exists even when toml isn't installed.""" 

31 

32 pass 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$

33 

34 

35TWant = TypeVar("TWant") 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$

36 

37 

38class TomlConfigParser: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$

39 """TOML file reading with the interface of HandyConfigParser.""" 

40 

41 # This class has the same interface as config.HandyConfigParser, no 

42 # need for docstrings. 

43 # pylint: disable=missing-function-docstring 

44 

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#$

48 

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#$

54 

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

73 

74 def _get_section(self, section: str) -> tuple[str | None, TConfigSectionOut | None]: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$

75 """Get a section from the data. 

76 

77 Arguments: 

78 section (str): A section name, which can be dotted. 

79 

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. 

84 

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#$

100 

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#$

112 

113 def _get_single(self, section: str, option: str) -> Any: 1abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLM%NOPQRSTU'VWXYZ012(3456789!)g#$

114 """Get a single-valued option. 

115 

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#$

123 

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#$

129 

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#$

133 

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#$

137 

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#$

143 

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#$

147 

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#$

151 

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. 

162 

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 ) 

177 

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#$

182 

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#$

186 

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#$

193 

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#$

197 

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#$

201 

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#$

205 

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#$