1""" brain-dead simple parser for ini-style files.
2(C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed
3"""
4from __future__ import annotations
5from 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
20import os
21
22if TYPE_CHECKING:
23 from typing import Final
24
25__all__ = ["IniConfig", "ParseError", "COMMENTCHARS", "iscommentline"]
26
27from .exceptions import ParseError
28from . import _parse
29from ._parse import COMMENTCHARS, iscommentline
30
31_D = TypeVar("_D")
32_T = TypeVar("_T")
33
34
35class 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
105class 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