section.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. """Sections are intermediate containers in **ConfigUpdater**'s data model for
  2. configuration files.
  3. They are at the same time :class:`containers <Container>` that hold :mod:`options
  4. <Option>` and :class:`blocks <Block>` nested inside the top level configuration
  5. :class:`~configupdater.document.Document`.
  6. Note:
  7. Please remember that :meth:`Section.get` method is implemented to mirror the
  8. :meth:`ConfigParser API <configparser.ConfigParser.set>` and do not correspond to
  9. the more usual :meth:`~collections.abc.Mapping.get` method of *dict-like* objects.
  10. """
  11. import sys
  12. from typing import TYPE_CHECKING, Optional, Tuple, TypeVar, Union, cast, overload
  13. if sys.version_info[:2] >= (3, 9): # pragma: no cover
  14. from collections.abc import Iterable, Iterator, MutableMapping
  15. List = list
  16. Dict = dict
  17. else: # pragma: no cover
  18. from typing import Dict, Iterable, Iterator, List, MutableMapping
  19. if TYPE_CHECKING:
  20. from .document import Document
  21. from .block import Block, Comment, Space
  22. from .builder import BlockBuilder
  23. from .container import Container
  24. from .option import Option
  25. T = TypeVar("T")
  26. S = TypeVar("S", bound="Section")
  27. Content = Union["Option", "Comment", "Space"]
  28. Value = Union["Option", str]
  29. class Section(Block, Container[Content], MutableMapping[str, "Option"]):
  30. """Section block holding options"""
  31. def __init__(
  32. self, name: str, container: Optional["Document"] = None, raw_comment: str = ""
  33. ):
  34. self._container: Optional["Document"] = container
  35. self._name = name
  36. self._raw_comment = raw_comment
  37. self._structure: List[Content] = []
  38. self._updated = False
  39. super().__init__(container=container)
  40. @property
  41. def document(self) -> "Document":
  42. return cast("Document", self.container)
  43. def add_option(self: S, entry: "Option") -> S:
  44. """Add an Option object to the section
  45. Used during initial parsing mainly
  46. Args:
  47. entry (Option): key value pair as Option object
  48. """
  49. entry.attach(self)
  50. self._structure.append(entry)
  51. return self
  52. def add_comment(self: S, line: str) -> S:
  53. """Add a Comment object to the section
  54. Used during initial parsing mainly
  55. Args:
  56. line (str): one line in the comment
  57. """
  58. if isinstance(self.last_block, Comment):
  59. comment: Comment = self.last_block
  60. else:
  61. comment = Comment(container=self)
  62. self._structure.append(comment)
  63. comment.add_line(line)
  64. return self
  65. def add_space(self: S, line: str) -> S:
  66. """Add a Space object to the section
  67. Used during initial parsing mainly
  68. Args:
  69. line (str): one line that defines the space, maybe whitespaces
  70. """
  71. if isinstance(self.last_block, Space):
  72. space = self.last_block
  73. else:
  74. space = Space(container=self)
  75. self._structure.append(space)
  76. space.add_line(line)
  77. return self
  78. def _get_option_idx(self, key: str) -> int:
  79. return next(
  80. i
  81. for i, entry in enumerate(self._structure)
  82. if isinstance(entry, Option) and entry.key == key
  83. )
  84. def __str__(self) -> str:
  85. if not self.updated:
  86. s = super().__str__()
  87. if self._structure and not s.endswith("\n"):
  88. s += "\n"
  89. else:
  90. s = "[{}]{}\n".format(self._name, self.raw_comment)
  91. for entry in self._structure:
  92. s += str(entry)
  93. return s
  94. def __repr__(self) -> str:
  95. return f"<Section: {self.name!r} {super()._repr_blocks()}>"
  96. def _instantiate_copy(self: S) -> S:
  97. """Will be called by :meth:`Block.__deepcopy__`"""
  98. clone = self.__class__(self._name, container=None)
  99. # ^ A fresh copy should always be made detached from any container
  100. clone._raw_comment = self._raw_comment
  101. return clone
  102. def __deepcopy__(self: S, memo: dict) -> S:
  103. clone = Block.__deepcopy__(self, memo) # specific due to multi-inheritance
  104. return clone._copy_structure(self._structure, memo)
  105. def __getitem__(self, key: str) -> "Option":
  106. key = self.document.optionxform(key)
  107. try:
  108. return next(o for o in self.iter_options() if o.key == key)
  109. except StopIteration as ex:
  110. raise KeyError(f"No option `{key}` found", {"key": key}) from ex
  111. def __setitem__(self, key: str, value: Optional[Value] = None):
  112. """Set the value of an option.
  113. Please notice that this method used
  114. :meth:`~configupdater.document.Document.optionxform` to verify if the given
  115. option already exists inside the section object.
  116. """
  117. # First we check for inconsistencies
  118. given_key = self.document.optionxform(key)
  119. if isinstance(value, Option):
  120. value_key = self.document.optionxform(value.raw_key)
  121. # ^ Calculate value_key according to the optionxform of the current
  122. # document, in the case the option is imported from a document with a
  123. # different optionxform
  124. option = value
  125. if value_key != given_key:
  126. msg = f"Set key `{given_key}` does not equal option key `{value_key}`"
  127. raise ValueError(msg)
  128. else:
  129. option = self.create_option(key, value)
  130. if given_key in self: # Replace an existing option
  131. if isinstance(value, Option):
  132. curr_value = self.__getitem__(given_key)
  133. idx = curr_value.container_idx
  134. curr_value.detach()
  135. value.attach(self)
  136. self._structure.insert(idx, value)
  137. else:
  138. option = self.__getitem__(given_key)
  139. option.value = value
  140. else: # Append a new option
  141. option.attach(self)
  142. self._structure.append(option)
  143. def __delitem__(self, key: str):
  144. try:
  145. idx = self._get_option_idx(key=key)
  146. del self._structure[idx]
  147. except StopIteration as ex:
  148. raise KeyError(f"No option `{key}` found", {"key": key}) from ex
  149. # MutableMapping[str, Option] for some reason accepts key: object
  150. # it actually doesn't matter for the implementation, so we omit the typing
  151. def __contains__(self, key) -> bool:
  152. """Returns whether the given option exists.
  153. Args:
  154. option (str): name of option
  155. Returns:
  156. bool: whether the section exists
  157. """
  158. return next((True for o in self.iter_options() if o.key == key), False)
  159. # Omit typing so it can represent any object
  160. def __eq__(self, other) -> bool:
  161. if isinstance(other, self.__class__):
  162. return self.name == other.name and self._structure == other._structure
  163. else:
  164. return False
  165. def __iter__(self) -> Iterator[str]:
  166. return (b.key for b in self.iter_blocks() if isinstance(b, Option))
  167. def iter_options(self) -> Iterator["Option"]:
  168. """Iterate only over option blocks"""
  169. return (entry for entry in self.iter_blocks() if isinstance(entry, Option))
  170. def option_blocks(self) -> List["Option"]:
  171. """Returns option blocks
  172. Returns:
  173. list: list of :class:`~configupdater.option.Option` blocks
  174. """
  175. return list(self.iter_options())
  176. def options(self) -> List[str]:
  177. """Returns option names
  178. Returns:
  179. list: list of option names as strings
  180. """
  181. return [option.key for option in self.iter_options()]
  182. has_option = __contains__
  183. def to_dict(self) -> Dict[str, Optional[str]]:
  184. """Transform to dictionary
  185. Returns:
  186. dict: dictionary with same content
  187. """
  188. return {opt.key: opt.value for opt in self.iter_options()}
  189. @property
  190. def name(self) -> str:
  191. """Name of the section"""
  192. return self._name
  193. @name.setter
  194. def name(self, value: str):
  195. self._name = str(value)
  196. self._updated = True
  197. @property
  198. def raw_comment(self):
  199. """Raw comment (includes comment mark) inline with the section header"""
  200. return self._raw_comment
  201. @raw_comment.setter
  202. def raw_comment(self, value: str):
  203. """Add/replace a single comment inline with the section header.
  204. The given value should be a raw comment, i.e. it needs to contain the
  205. comment mark.
  206. """
  207. self._raw_comment = value
  208. self._updated = True
  209. def set(self: S, option: str, value: Union[None, str, Iterable[str]] = None) -> S:
  210. """Set an option for chaining.
  211. Args:
  212. option: option name
  213. value: value, default None
  214. """
  215. option = self.document.optionxform(option)
  216. if option not in self.options():
  217. self[option] = self.create_option(option)
  218. if not isinstance(value, Iterable) or isinstance(value, str):
  219. self[option].value = value
  220. else:
  221. self[option].set_values(value)
  222. return self
  223. def create_option(self, key: str, value: Optional[str] = None) -> "Option":
  224. """Creates an option with kwargs that respect syntax options given to
  225. the parent ConfigUpdater object (e.g. ``space_around_delimiters``).
  226. Warning:
  227. This is a low level API, not intended for public use.
  228. Prefer :meth:`set` or :meth:`__setitem__`.
  229. """
  230. syntax_opts = getattr(self._container, "syntax_options", {})
  231. kwargs_: dict = {
  232. "value": value,
  233. "container": self,
  234. "space_around_delimiters": syntax_opts.get("space_around_delimiters"),
  235. "delimiter": next(iter(syntax_opts.get("delimiters", [])), None),
  236. }
  237. kwargs = {k: v for k, v in kwargs_.items() if v is not None}
  238. return Option(key, **kwargs)
  239. @overload
  240. def get(self, key: str) -> Optional["Option"]:
  241. ...
  242. @overload
  243. def get(self, key: str, default: T) -> Union["Option", T]:
  244. ...
  245. def get(self, key, default=None):
  246. """This method works similarly to :meth:`dict.get`, and allows you
  247. to retrieve an option object by its key.
  248. """
  249. return next((o for o in self.iter_options() if o.key == key), default)
  250. # The following is a pragmatic violation of Liskov substitution principle
  251. # For some reason MutableMapping.items return a Set-like object
  252. # but we want to preserve ordering
  253. def items(self) -> List[Tuple[str, "Option"]]: # type: ignore[override]
  254. """Return a list of (name, option) tuples for each option in
  255. this section.
  256. Returns:
  257. list: list of (name, :class:`Option`) tuples
  258. """
  259. return [(opt.key, opt) for opt in self.option_blocks()]
  260. def insert_at(self, idx: int) -> "BlockBuilder":
  261. """Returns a builder inserting a new block at the given index
  262. Args:
  263. idx (int): index where to insert
  264. """
  265. return BlockBuilder(self, idx)
  266. def clear(self):
  267. for block in self._structure:
  268. block.detach()
  269. self._structure.clear()