1"""brain-dead simple parser for ini-style files.
2(C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed
3"""
4
5from __future__ import annotations
6from typing import (
7 Callable,
8 Iterator,
9 Mapping,
10 TypeVar,
11 TYPE_CHECKING,
12 overload,
13)
14
15import os
16
17if TYPE_CHECKING:
18 from typing import Final
19
20__all__ = ["IniConfig", "ParseError", "COMMENTCHARS", "iscommentline"]
21
22from .exceptions import ParseError
23from . import _parse
24from ._parse import COMMENTCHARS, iscommentline
25
26_D = TypeVar("_D")
27_T = TypeVar("_T")
28
29
30class SectionWrapper:
31 config: Final[IniConfig]
32 name: Final[str]
33
34 def __init__(self, config: IniConfig, name: str) -> None:
35 self.config = config
36 self.name = name
37
38 def lineof(self, name: str) -> int | None:
39 return self.config.lineof(self.name, name)
40
41 @overload
42 def get(self, key: str) -> str | None: ...
43
44 @overload
45 def get(
46 self,
47 key: str,
48 convert: Callable[[str], _T],
49 ) -> _T | None: ...
50
51 @overload
52 def get(
53 self,
54 key: str,
55 default: None,
56 convert: Callable[[str], _T],
57 ) -> _T | None: ...
58
59 @overload
60 def get(self, key: str, default: _D, convert: None = None) -> str | _D: ...
61
62 @overload
63 def get(
64 self,
65 key: str,
66 default: _D,
67 convert: Callable[[str], _T],
68 ) -> _T | _D: ...
69
70 # TODO: investigate possible mypy bug wrt matching the passed over data
71 def get( # type: ignore [misc]
72 self,
73 key: str,
74 default: _D | None = None,
75 convert: Callable[[str], _T] | None = None,
76 ) -> _D | _T | str | None:
77 return self.config.get(self.name, key, convert=convert, default=default)
78
79 def __getitem__(self, key: str) -> str:
80 return self.config.sections[self.name][key]
81
82 def __iter__(self) -> Iterator[str]:
83 section: Mapping[str, str] = self.config.sections.get(self.name, {})
84
85 def lineof(key: str) -> int:
86 return self.config.lineof(self.name, key) # type: ignore[return-value]
87
88 yield from sorted(section, key=lineof)
89
90 def items(self) -> Iterator[tuple[str, str]]:
91 for name in self:
92 yield name, self[name]
93
94
95class IniConfig:
96 path: Final[str]
97 sections: Final[Mapping[str, Mapping[str, str]]]
98
99 def __init__(
100 self,
101 path: str | os.PathLike[str],
102 data: str | None = None,
103 encoding: str = "utf-8",
104 ) -> None:
105 self.path = os.fspath(path)
106 if data is None:
107 with open(self.path, encoding=encoding) as fp:
108 data = fp.read()
109
110 tokens = _parse.parse_lines(self.path, data.splitlines(True))
111
112 self._sources = {}
113 sections_data: dict[str, dict[str, str]]
114 self.sections = sections_data = {}
115
116 for lineno, section, name, value in tokens:
117 if section is None:
118 raise ParseError(self.path, lineno, "no section header defined")
119 self._sources[section, name] = lineno
120 if name is None:
121 if section in self.sections:
122 raise ParseError(
123 self.path, lineno, f"duplicate section {section!r}"
124 )
125 sections_data[section] = {}
126 else:
127 if name in self.sections[section]:
128 raise ParseError(self.path, lineno, f"duplicate name {name!r}")
129 assert value is not None
130 sections_data[section][name] = value
131
132 def lineof(self, section: str, name: str | None = None) -> int | None:
133 lineno = self._sources.get((section, name))
134 return None if lineno is None else lineno + 1
135
136 @overload
137 def get(
138 self,
139 section: str,
140 name: str,
141 ) -> str | None: ...
142
143 @overload
144 def get(
145 self,
146 section: str,
147 name: str,
148 convert: Callable[[str], _T],
149 ) -> _T | None: ...
150
151 @overload
152 def get(
153 self,
154 section: str,
155 name: str,
156 default: None,
157 convert: Callable[[str], _T],
158 ) -> _T | None: ...
159
160 @overload
161 def get(
162 self, section: str, name: str, default: _D, convert: None = None
163 ) -> str | _D: ...
164
165 @overload
166 def get(
167 self,
168 section: str,
169 name: str,
170 default: _D,
171 convert: Callable[[str], _T],
172 ) -> _T | _D: ...
173
174 def get( # type: ignore
175 self,
176 section: str,
177 name: str,
178 default: _D | None = None,
179 convert: Callable[[str], _T] | None = None,
180 ) -> _D | _T | str | None:
181 try:
182 value: str = self.sections[section][name]
183 except KeyError:
184 return default
185 else:
186 if convert is not None:
187 return convert(value)
188 else:
189 return value
190
191 def __getitem__(self, name: str) -> SectionWrapper:
192 if name not in self.sections:
193 raise KeyError(name)
194 return SectionWrapper(self, name)
195
196 def __iter__(self) -> Iterator[SectionWrapper]:
197 for name in sorted(self.sections, key=self.lineof): # type: ignore
198 yield SectionWrapper(self, name)
199
200 def __contains__(self, arg: str) -> bool:
201 return arg in self.sections