Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/iniconfig/__init__.py: 63%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

98 statements  

1"""brain-dead simple parser for ini-style files. 

2(C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed 

3""" 

4 

5import os 

6from collections.abc import Callable 

7from collections.abc import Iterator 

8from collections.abc import Mapping 

9from typing import Final 

10from typing import TypeVar 

11from typing import overload 

12 

13__all__ = ["IniConfig", "ParseError", "COMMENTCHARS", "iscommentline"] 

14 

15from . import _parse 

16from ._parse import COMMENTCHARS 

17from ._parse import iscommentline 

18from .exceptions import ParseError 

19 

20_D = TypeVar("_D") 

21_T = TypeVar("_T") 

22 

23 

24class SectionWrapper: 

25 config: Final["IniConfig"] 

26 name: Final[str] 

27 

28 def __init__(self, config: "IniConfig", name: str) -> None: 

29 self.config = config 

30 self.name = name 

31 

32 def lineof(self, name: str) -> int | None: 

33 return self.config.lineof(self.name, name) 

34 

35 @overload 

36 def get(self, key: str) -> str | None: ... 

37 

38 @overload 

39 def get( 

40 self, 

41 key: str, 

42 convert: Callable[[str], _T], 

43 ) -> _T | None: ... 

44 

45 @overload 

46 def get( 

47 self, 

48 key: str, 

49 default: None, 

50 convert: Callable[[str], _T], 

51 ) -> _T | None: ... 

52 

53 @overload 

54 def get(self, key: str, default: _D, convert: None = None) -> str | _D: ... 

55 

56 @overload 

57 def get( 

58 self, 

59 key: str, 

60 default: _D, 

61 convert: Callable[[str], _T], 

62 ) -> _T | _D: ... 

63 

64 # TODO: investigate possible mypy bug wrt matching the passed over data 

65 def get( # type: ignore [misc] 

66 self, 

67 key: str, 

68 default: _D | None = None, 

69 convert: Callable[[str], _T] | None = None, 

70 ) -> _D | _T | str | None: 

71 return self.config.get(self.name, key, convert=convert, default=default) 

72 

73 def __getitem__(self, key: str) -> str: 

74 return self.config.sections[self.name][key] 

75 

76 def __iter__(self) -> Iterator[str]: 

77 section: Mapping[str, str] = self.config.sections.get(self.name, {}) 

78 

79 def lineof(key: str) -> int: 

80 return self.config.lineof(self.name, key) # type: ignore[return-value] 

81 

82 yield from sorted(section, key=lineof) 

83 

84 def items(self) -> Iterator[tuple[str, str]]: 

85 for name in self: 

86 yield name, self[name] 

87 

88 

89class IniConfig: 

90 path: Final[str] 

91 sections: Final[Mapping[str, Mapping[str, str]]] 

92 _sources: Final[Mapping[tuple[str, str | None], int]] 

93 

94 def __init__( 

95 self, 

96 path: str | os.PathLike[str], 

97 data: str | None = None, 

98 encoding: str = "utf-8", 

99 *, 

100 _sections: Mapping[str, Mapping[str, str]] | None = None, 

101 _sources: Mapping[tuple[str, str | None], int] | None = None, 

102 ) -> None: 

103 self.path = os.fspath(path) 

104 

105 # Determine sections and sources 

106 if _sections is not None and _sources is not None: 

107 # Use provided pre-parsed data (called from parse()) 

108 sections_data = _sections 

109 sources = _sources 

110 else: 

111 # Parse the data (backward compatible path) 

112 if data is None: 

113 with open(self.path, encoding=encoding) as fp: 

114 data = fp.read() 

115 

116 # Use old behavior (no stripping) for backward compatibility 

117 sections_data, sources = _parse.parse_ini_data( 

118 self.path, data, strip_inline_comments=False 

119 ) 

120 

121 # Assign once to Final attributes 

122 self._sources = sources 

123 self.sections = sections_data 

124 

125 @classmethod 

126 def parse( 

127 cls, 

128 path: str | os.PathLike[str], 

129 data: str | None = None, 

130 encoding: str = "utf-8", 

131 *, 

132 strip_inline_comments: bool = True, 

133 strip_section_whitespace: bool = False, 

134 ) -> "IniConfig": 

135 """Parse an INI file. 

136 

137 Args: 

138 path: Path to the INI file (used for error messages) 

139 data: Optional INI content as string. If None, reads from path. 

140 encoding: Encoding to use when reading the file (default: utf-8) 

141 strip_inline_comments: Whether to strip inline comments from values 

142 (default: True). When True, comments starting with # or ; are 

143 removed from values, matching the behavior for section comments. 

144 strip_section_whitespace: Whether to strip whitespace from section and key names 

145 (default: False). When True, strips Unicode whitespace from section and key names, 

146 addressing issue #4. When False, preserves existing behavior for backward compatibility. 

147 

148 Returns: 

149 IniConfig instance with parsed configuration 

150 

151 Example: 

152 # With comment stripping (default): 

153 config = IniConfig.parse("setup.cfg") 

154 # value = "foo" instead of "foo # comment" 

155 

156 # Without comment stripping (old behavior): 

157 config = IniConfig.parse("setup.cfg", strip_inline_comments=False) 

158 # value = "foo # comment" 

159 

160 # With section name stripping (opt-in for issue #4): 

161 config = IniConfig.parse("setup.cfg", strip_section_whitespace=True) 

162 # section names and keys have Unicode whitespace stripped 

163 """ 

164 fspath = os.fspath(path) 

165 

166 if data is None: 

167 with open(fspath, encoding=encoding) as fp: 

168 data = fp.read() 

169 

170 sections_data, sources = _parse.parse_ini_data( 

171 fspath, 

172 data, 

173 strip_inline_comments=strip_inline_comments, 

174 strip_section_whitespace=strip_section_whitespace, 

175 ) 

176 

177 # Call constructor with pre-parsed sections and sources 

178 return cls(path=fspath, _sections=sections_data, _sources=sources) 

179 

180 def lineof(self, section: str, name: str | None = None) -> int | None: 

181 lineno = self._sources.get((section, name)) 

182 return None if lineno is None else lineno + 1 

183 

184 @overload 

185 def get( 

186 self, 

187 section: str, 

188 name: str, 

189 ) -> str | None: ... 

190 

191 @overload 

192 def get( 

193 self, 

194 section: str, 

195 name: str, 

196 convert: Callable[[str], _T], 

197 ) -> _T | None: ... 

198 

199 @overload 

200 def get( 

201 self, 

202 section: str, 

203 name: str, 

204 default: None, 

205 convert: Callable[[str], _T], 

206 ) -> _T | None: ... 

207 

208 @overload 

209 def get( 

210 self, section: str, name: str, default: _D, convert: None = None 

211 ) -> str | _D: ... 

212 

213 @overload 

214 def get( 

215 self, 

216 section: str, 

217 name: str, 

218 default: _D, 

219 convert: Callable[[str], _T], 

220 ) -> _T | _D: ... 

221 

222 def get( # type: ignore 

223 self, 

224 section: str, 

225 name: str, 

226 default: _D | None = None, 

227 convert: Callable[[str], _T] | None = None, 

228 ) -> _D | _T | str | None: 

229 try: 

230 value: str = self.sections[section][name] 

231 except KeyError: 

232 return default 

233 else: 

234 if convert is not None: 

235 return convert(value) 

236 else: 

237 return value 

238 

239 def __getitem__(self, name: str) -> SectionWrapper: 

240 if name not in self.sections: 

241 raise KeyError(name) 

242 return SectionWrapper(self, name) 

243 

244 def __iter__(self) -> Iterator[SectionWrapper]: 

245 for name in sorted(self.sections, key=self.lineof): # type: ignore 

246 yield SectionWrapper(self, name) 

247 

248 def __contains__(self, arg: str) -> bool: 

249 return arg in self.sections