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

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 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78

7 

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

12 

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

17 

18os = isolate_module(os) 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78

19 

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

22 

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

27 

28 

29class TomlDecodeError(Exception): 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78

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

31 

32 pass 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78

33 

34 

35TWant = TypeVar("TWant") 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78

36 

37 

38class TomlConfigParser: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78

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: 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

48 

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

54 

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

73 

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. 

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."] 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

100 

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

112 

113 def _get_single(self, section: str, option: str) -> Any: 1abcdefhijklmnopqrstuvwxyzABCDEFG#HIJKL%MN9OPQRS'TU$VWXYZ(01!23456)g78

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) 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

123 

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

129 

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

133 

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

137 

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

143 

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

147 

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

151 

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. 

162 

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 ) 

177 

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

182 

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

186 

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

193 

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

197 

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

201 

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

205 

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