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
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
1"""brain-dead simple parser for ini-style files.
2(C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed
3"""
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
13__all__ = ["IniConfig", "ParseError", "COMMENTCHARS", "iscommentline"]
15from . import _parse
16from ._parse import COMMENTCHARS
17from ._parse import iscommentline
18from .exceptions import ParseError
20_D = TypeVar("_D")
21_T = TypeVar("_T")
24class SectionWrapper:
25 config: Final["IniConfig"]
26 name: Final[str]
28 def __init__(self, config: "IniConfig", name: str) -> None:
29 self.config = config
30 self.name = name
32 def lineof(self, name: str) -> int | None:
33 return self.config.lineof(self.name, name)
35 @overload
36 def get(self, key: str) -> str | None: ...
38 @overload
39 def get(
40 self,
41 key: str,
42 convert: Callable[[str], _T],
43 ) -> _T | None: ...
45 @overload
46 def get(
47 self,
48 key: str,
49 default: None,
50 convert: Callable[[str], _T],
51 ) -> _T | None: ...
53 @overload
54 def get(self, key: str, default: _D, convert: None = None) -> str | _D: ...
56 @overload
57 def get(
58 self,
59 key: str,
60 default: _D,
61 convert: Callable[[str], _T],
62 ) -> _T | _D: ...
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)
73 def __getitem__(self, key: str) -> str:
74 return self.config.sections[self.name][key]
76 def __iter__(self) -> Iterator[str]:
77 section: Mapping[str, str] = self.config.sections.get(self.name, {})
79 def lineof(key: str) -> int:
80 return self.config.lineof(self.name, key) # type: ignore[return-value]
82 yield from sorted(section, key=lineof)
84 def items(self) -> Iterator[tuple[str, str]]:
85 for name in self:
86 yield name, self[name]
89class IniConfig:
90 path: Final[str]
91 sections: Final[Mapping[str, Mapping[str, str]]]
92 _sources: Final[Mapping[tuple[str, str | None], int]]
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)
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()
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 )
121 # Assign once to Final attributes
122 self._sources = sources
123 self.sections = sections_data
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.
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.
148 Returns:
149 IniConfig instance with parsed configuration
151 Example:
152 # With comment stripping (default):
153 config = IniConfig.parse("setup.cfg")
154 # value = "foo" instead of "foo # comment"
156 # Without comment stripping (old behavior):
157 config = IniConfig.parse("setup.cfg", strip_inline_comments=False)
158 # value = "foo # comment"
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)
166 if data is None:
167 with open(fspath, encoding=encoding) as fp:
168 data = fp.read()
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 )
177 # Call constructor with pre-parsed sections and sources
178 return cls(path=fspath, _sections=sections_data, _sources=sources)
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
184 @overload
185 def get(
186 self,
187 section: str,
188 name: str,
189 ) -> str | None: ...
191 @overload
192 def get(
193 self,
194 section: str,
195 name: str,
196 convert: Callable[[str], _T],
197 ) -> _T | None: ...
199 @overload
200 def get(
201 self,
202 section: str,
203 name: str,
204 default: None,
205 convert: Callable[[str], _T],
206 ) -> _T | None: ...
208 @overload
209 def get(
210 self, section: str, name: str, default: _D, convert: None = None
211 ) -> str | _D: ...
213 @overload
214 def get(
215 self,
216 section: str,
217 name: str,
218 default: _D,
219 convert: Callable[[str], _T],
220 ) -> _T | _D: ...
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
239 def __getitem__(self, name: str) -> SectionWrapper:
240 if name not in self.sections:
241 raise KeyError(name)
242 return SectionWrapper(self, name)
244 def __iter__(self) -> Iterator[SectionWrapper]:
245 for name in sorted(self.sections, key=self.lineof): # type: ignore
246 yield SectionWrapper(self, name)
248 def __contains__(self, arg: str) -> bool:
249 return arg in self.sections