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

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

3"""

4 from __future__ import annotations

5 from typing import (

6 Callable,

7 Iterator,

8 Mapping,

9 Optional,

10 Tuple,

11 TypeVar,

12 Union,

13 TYPE_CHECKING,

14 NoReturn,

15 NamedTuple,

16 overload,

17 cast,

18)

19

20 import os

21

22 if TYPE_CHECKING:

23 from typing_extensions import Final

24

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

26

27 from .exceptions import ParseError

28 from . import _parse

29 from ._parse import COMMENTCHARS, iscommentline

30

31_D = TypeVar("_D")

32_T = TypeVar("_T")

33

34

35 class SectionWrapper:

36 config: Final[IniConfig]

37 name: Final[str]

38

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

40 self.config = config

41 self.name = name

42

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

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

45

46 @overload

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

48 ...

49

50 @overload

51 def get(

52 self,

53 key: str,

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

55 ) -> _T | None:

56 ...

57

58 @overload

59 def get(

60 self,

61 key: str,

62 default: None,

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

64 ) -> _T | None:

65 ...

66

67 @overload

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

69 ...

70

71 @overload

72 def get(

73 self,

74 key: str,

75 default: _D,

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

77 ) -> _T | _D:

78 ...

79

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

81 def get( # type: ignore [misc]

82 self,

83 key: str,

84 default: _D | None = None,

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

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

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

88

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

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

91

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

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

94

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

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

97

98 yield from sorted(section, key=lineof)

99

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

101 for name in self:

102 yield name, self[name]

103

104

105 class IniConfig:

106 path: Final[str]

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

108

109 def __init__(

110 self,

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

112 data: str | None = None,

113 encoding: str = "utf-8",

114 ) -> None:

115 self.path = os.fspath(path)

116 if data is None:

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

118 data = fp.read()

119

120 tokens = _parse.parse_lines(self.path, data.splitlines(True))

121

122 self._sources = {}

123 sections_data: dict[str, dict[str, str]]

124 self.sections = sections_data = {}

125

126 for lineno, section, name, value in tokens:

127 if section is None:

128 raise ParseError(self.path, lineno, "no section header defined")

129 self._sources[section, name] = lineno

130 if name is None:

131 if section in self.sections:

132 raise ParseError(

133 self.path, lineno, f"duplicate section {section!r}"

134 )

135 sections_data[section] = {}

136 else:

137 if name in self.sections[section]:

138 raise ParseError(self.path, lineno, f"duplicate name {name!r}")

139 assert value is not None

140 sections_data[section][name] = value

141

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

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

144 return None if lineno is None else lineno + 1

145

146 @overload

147 def get(

148 self,

149 section: str,

150 name: str,

151 ) -> str | None:

152 ...

153

154 @overload

155 def get(

156 self,

157 section: str,

158 name: str,

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

160 ) -> _T | None:

161 ...

162

163 @overload

164 def get(

165 self,

166 section: str,

167 name: str,

168 default: None,

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

170 ) -> _T | None:

171 ...

172

173 @overload

174 def get(

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

176 ) -> str | _D:

177 ...

178

179 @overload

180 def get(

181 self,

182 section: str,

183 name: str,

184 default: _D,

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

186 ) -> _T | _D:

187 ...

188

189 def get( # type: ignore

190 self,

191 section: str,

192 name: str,

193 default: _D | None = None,

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

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

196 try:

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

198 except KeyError:

199 return default

200 else:

201 if convert is not None:

202 return convert(value)

203 else:

204 return value

205

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

207 if name not in self.sections:

208 raise KeyError(name)

209 return SectionWrapper(self, name)

210

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

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

213 yield SectionWrapper(self, name)

214

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

216 return arg in self.sections