document.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. """This module focus in the top level data layer API of ConfigUpdater, i.e.
  2. how to access and modify the sections of the configurations.
  3. Differently from :mod:`configparser`, the different aspects of the ConfigUpdater API are
  4. split between several modules.
  5. """
  6. import sys
  7. from configparser import (
  8. ConfigParser,
  9. DuplicateSectionError,
  10. NoOptionError,
  11. NoSectionError,
  12. )
  13. from enum import Enum
  14. from typing import Optional, Tuple, TypeVar, Union, overload
  15. if sys.version_info[:2] >= (3, 9): # pragma: no cover
  16. from collections.abc import Iterable, Iterator, MutableMapping
  17. List = list
  18. Dict = dict
  19. else: # pragma: no cover
  20. from typing import Dict, Iterable, Iterator, List, MutableMapping
  21. from .block import Comment, Space
  22. from .container import Container
  23. from .option import Option
  24. from .section import Section
  25. # Used in parser getters to indicate the default behaviour when a specific
  26. # option is not found it to raise an exception. Created to enable 'None' as
  27. # a valid fallback value.
  28. _UniqueValues = Enum("UniqueValues", "_UNSET")
  29. _UNSET = _UniqueValues._UNSET
  30. T = TypeVar("T")
  31. D = TypeVar("D", bound="Document")
  32. ConfigContent = Union["Section", "Comment", "Space"]
  33. Value = Union["Option", str]
  34. class Document(Container[ConfigContent], MutableMapping[str, Section]):
  35. """Access to the data manipulation API from **ConfigUpdater**.
  36. A ``Document`` object tries to implement a familiar *dict-like* interface,
  37. via :class:`MutableMapping`. However, it also tries to be as compatible as possible
  38. with the stdlib's :class:`~configparser.ConfigParser`. This means that there are a
  39. few methods that will work differently from what users familiar with *dict-like*
  40. interfaces would expect. The most notable example is :meth:`get`.
  41. A important difference between ConfigUpdater's ``Document`` model and
  42. :class:`~configparser.ConfigParser` is the behaviour of the :class:`Section`
  43. objects.
  44. If we represent the type of a *dict-like* (or :class:`MutableMapping`) object by
  45. ``M[K, V]``, where ``K`` is the type of its keys and ``V`` is the type of the
  46. associated values, ConfigUpdater's sections would be equivalent to
  47. ``M[str, Option]``, while ConfigParser's would be ``M[str, str]``.
  48. This means that when you try to access a key inside a section in ConfigUpdater, you
  49. are going to receive a :class:`Option` object, not its value.
  50. To access the value of the option you need to call :class:`Option.value`.
  51. """
  52. def _get_section_idx(self, name: str) -> int:
  53. return next(
  54. i
  55. for i, entry in enumerate(self._structure)
  56. if isinstance(entry, Section) and entry.name == name
  57. )
  58. def optionxform(self, optionstr) -> str:
  59. """Converts an option key for unification
  60. By default it uses :meth:`str.lower`, which means that ConfigUpdater will
  61. compare options in a case insensitive way.
  62. This implementation mimics ConfigParser API, and can be configured as described
  63. in :meth:`configparser.ConfigParser.optionxform`.
  64. Args:
  65. optionstr (str): key name
  66. Returns:
  67. str: unified option name
  68. """
  69. return optionstr.lower()
  70. def validate_format(self, **kwargs):
  71. """Call ConfigParser to validate config
  72. Args:
  73. kwargs: are passed to :class:`configparser.ConfigParser`
  74. Raises:
  75. configparser.ParsingError: if syntax errors are found
  76. Returns:
  77. ``True`` when no error is found
  78. """
  79. kwargs.pop("space_around_delimiters", None)
  80. parser = ConfigParser(**kwargs)
  81. parser.read_string(str(self))
  82. return True
  83. def iter_sections(self) -> Iterator[Section]:
  84. """Iterate only over section blocks"""
  85. return (block for block in self._structure if isinstance(block, Section))
  86. def section_blocks(self) -> List[Section]:
  87. """Returns all section blocks
  88. Returns:
  89. list: list of :class:`Section` blocks
  90. """
  91. return list(self.iter_sections())
  92. def sections(self) -> List[str]:
  93. """Return a list of section names
  94. Returns:
  95. list: list of section names
  96. """
  97. return [section.name for section in self.iter_sections()]
  98. def __iter__(self) -> Iterator[str]:
  99. return (b.name for b in self.iter_blocks() if isinstance(b, Section))
  100. def __str__(self) -> str:
  101. return "".join(str(block) for block in self._structure)
  102. def __getitem__(self, key) -> Section:
  103. for section in self.section_blocks():
  104. if section.name == key:
  105. return section
  106. raise KeyError(f"No section `{key}` found", {"key": key})
  107. def __setitem__(self, key: str, value: Section):
  108. if not isinstance(value, Section):
  109. raise ValueError("Value must be of type Section!")
  110. if isinstance(key, str) and key in self:
  111. idx = self._get_section_idx(key)
  112. self.__getitem__(key).detach()
  113. value.attach(self)
  114. self._structure.insert(idx, value)
  115. else:
  116. if value.name != key:
  117. raise ValueError(
  118. f"Set key `{key}` does not equal option key `{value.name}`"
  119. )
  120. self.add_section(value)
  121. def __delitem__(self, key: str):
  122. if not self.remove_section(key):
  123. raise KeyError(f"No section `{key}` found", {"key": key})
  124. # MutableMapping[str, Section] for some reason accepts key: object
  125. # it actually doesn't matter for the implementation, so we omit the typing
  126. def __contains__(self, key) -> bool:
  127. """Returns whether the given section exists.
  128. Args:
  129. key (str): name of section
  130. Returns:
  131. bool: wether the section exists
  132. """
  133. return next((True for s in self.iter_sections() if s.name == key), False)
  134. has_section = __contains__
  135. def __eq__(self, other) -> bool:
  136. if isinstance(other, self.__class__):
  137. return self._structure == other._structure
  138. else:
  139. return False
  140. def clear(self):
  141. for block in self._structure:
  142. block.detach()
  143. self._structure.clear()
  144. def add_section(self, section: Union[str, Section]):
  145. """Create a new section in the configuration.
  146. Raise DuplicateSectionError if a section by the specified name
  147. already exists. Raise ValueError if name is DEFAULT.
  148. Args:
  149. section (str or :class:`Section`): name or Section type
  150. """
  151. if isinstance(section, str):
  152. # create a new section
  153. section_obj = Section(section)
  154. elif isinstance(section, Section):
  155. section_obj = section
  156. else:
  157. raise ValueError("Parameter must be a string or Section type!")
  158. if self.has_section(section_obj.name):
  159. raise DuplicateSectionError(section_obj.name)
  160. section_obj.attach(self)
  161. self._structure.append(section_obj)
  162. def options(self, section: str) -> List[str]:
  163. """Returns list of configuration options for the named section.
  164. Args:
  165. section (str): name of section
  166. Returns:
  167. list: list of option names
  168. """
  169. if not self.has_section(section):
  170. raise NoSectionError(section) from None
  171. return self.__getitem__(section).options()
  172. # The following is a pragmatic violation of Liskov substitution principle:
  173. # As dicts, Mappings should have get(self, key: str, default: T) -> T
  174. # but ConfigParser overwrites it and uses the function to offer a different
  175. # functionality
  176. @overload # type: ignore[override]
  177. def get(self, section: str, option: str) -> Option:
  178. ...
  179. @overload
  180. def get(self, section: str, option: str, fallback: T) -> Union[Option, T]: # noqa
  181. ...
  182. def get(self, section, option, fallback=_UNSET): # noqa
  183. """Gets an option object for a given section or a fallback value.
  184. Warning:
  185. Please notice this method works differently from what is expected of
  186. :meth:`MutableMapping.get` (or :meth:`dict.get`).
  187. Similarly to :meth:`configparser.ConfigParser.get`, will take least 2
  188. arguments, and the second argument does not correspond to a default value.
  189. This happens because this function is not designed to return a
  190. :obj:`Section` of the :obj:`ConfigUpdater` document, but instead a nested
  191. :obj:`Option`.
  192. See :meth:`get_section`, if instead, you want to retrieve a :obj:`Section`.
  193. Args:
  194. section (str): section name
  195. option (str): option name
  196. fallback (T): if the key is not found and fallback is provided, the
  197. ``fallback`` value will be returned. ``None`` is a valid fallback value.
  198. .. attention::
  199. When ``option`` is not present, the ``fallback`` value itself is
  200. returned. If you want instead to obtain a new ``Option`` object
  201. with a default value associated with it, you can try the following::
  202. configupdater.get("section", "option", fallback=Option("name", value))
  203. ... which roughly corresponds to::
  204. configupdater["section"].get("option", Option("name", value))
  205. Raises:
  206. :class:`NoSectionError`: if ``section`` cannot be found
  207. :class:`NoOptionError`: if the option cannot be found and no ``fallback``
  208. was given
  209. Returns:
  210. :class:`Option` object holding key/value pair when it exists. Otherwise,
  211. the value passed via the ``fallback`` argument itself (type ``T``).
  212. """
  213. section_obj = self.get_section(section, _UNSET)
  214. if section_obj is _UNSET:
  215. raise NoSectionError(section) from None
  216. option = self.optionxform(option)
  217. value = section_obj.get(option, fallback)
  218. # ^ we checked section_obj against _UNSET, so we are sure about its type
  219. if value is _UNSET:
  220. raise NoOptionError(option, section)
  221. return value
  222. @overload
  223. def get_section(self, name: str) -> Optional[Section]:
  224. ...
  225. @overload
  226. def get_section(self, name: str, default: T) -> Union[Section, T]:
  227. ...
  228. def get_section(self, name, default=None):
  229. """This method works similarly to :meth:`dict.get`, and allows you
  230. to retrieve an entire section by its name, or provide a ``default`` value in
  231. case it cannot be found.
  232. """
  233. return next((s for s in self.iter_sections() if s.name == name), default)
  234. # The following is a pragmatic violation of Liskov substitution principle
  235. # For some reason MutableMapping.items return a Set-like object
  236. # but we want to preserve ordering
  237. @overload # type: ignore[override]
  238. def items(self) -> List[Tuple[str, Section]]:
  239. ...
  240. @overload
  241. def items(self, section: str) -> List[Tuple[str, Option]]: # noqa
  242. ...
  243. def items(self, section=_UNSET): # noqa
  244. """Return a list of (name, value) tuples for options or sections.
  245. If section is given, return a list of tuples with (name, value) for
  246. each option in the section. Otherwise, return a list of tuples with
  247. (section_name, section_type) for each section.
  248. Args:
  249. section (str): optional section name, default UNSET
  250. Returns:
  251. list: list of :class:`Section` or :class:`Option` objects
  252. """
  253. if section is _UNSET:
  254. return [(sect.name, sect) for sect in self.iter_sections()]
  255. section = self.__getitem__(section)
  256. return [(opt.key, opt) for opt in section.iter_options()]
  257. def has_option(self, section: str, option: str) -> bool:
  258. """Checks for the existence of a given option in a given section.
  259. Args:
  260. section (str): name of section
  261. option (str): name of option
  262. Returns:
  263. bool: whether the option exists in the given section
  264. """
  265. key = self.optionxform(option)
  266. return key in self.get_section(section, {})
  267. def set(
  268. self: D,
  269. section: str,
  270. option: str,
  271. value: Union[None, str, Iterable[str]] = None,
  272. ) -> D:
  273. """Set an option.
  274. Args:
  275. section: section name
  276. option: option name
  277. value: value, default None
  278. """
  279. try:
  280. section_obj = self.__getitem__(section)
  281. except KeyError:
  282. raise NoSectionError(section) from None
  283. option = self.optionxform(option)
  284. section_obj.set(option, value)
  285. return self
  286. def remove_option(self, section: str, option: str) -> bool:
  287. """Remove an option.
  288. Args:
  289. section (str): section name
  290. option (str): option name
  291. Returns:
  292. bool: whether the option was actually removed
  293. """
  294. try:
  295. section_obj = self.__getitem__(section)
  296. except KeyError:
  297. raise NoSectionError(section) from None
  298. option = self.optionxform(option)
  299. existed = option in section_obj.options()
  300. if existed:
  301. del section_obj[option]
  302. return existed
  303. def remove_section(self, name: str) -> bool:
  304. """Remove a file section.
  305. Args:
  306. name: name of the section
  307. Returns:
  308. bool: whether the section was actually removed
  309. """
  310. try:
  311. idx = self._get_section_idx(name)
  312. del self._structure[idx]
  313. return True
  314. except StopIteration:
  315. return False
  316. def to_dict(self) -> Dict[str, Dict[str, Optional[str]]]:
  317. """Transform to dictionary
  318. Returns:
  319. dict: dictionary with same content
  320. """
  321. return {sect.name: sect.to_dict() for sect in self.iter_sections()}