__init__.py 5.3 KB

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