block.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. """Together with :mod:`~configupdater.container` this module forms the basis of
  2. the class hierarchy in **ConfigUpdater**.
  3. The :class:`Block` is the parent class of everything that can be nested inside a
  4. configuration file, e.g. comments, sections, options and even sequences of white space.
  5. """
  6. import sys
  7. from copy import deepcopy
  8. from inspect import cleandoc
  9. from typing import TYPE_CHECKING, Optional, TypeVar, Union, cast
  10. if sys.version_info[:2] >= (3, 9): # pragma: no cover
  11. List = list
  12. else: # pragma: no cover
  13. from typing import List
  14. if TYPE_CHECKING:
  15. from .builder import BlockBuilder
  16. from .container import Container
  17. T = TypeVar("T")
  18. B = TypeVar("B", bound="Block")
  19. def _short_repr(block) -> str:
  20. if isinstance(block, str):
  21. return block
  22. name = getattr(block, "raw_key", None) or getattr(block, "name", None)
  23. name = f" {name!r}" if name else ""
  24. return f"<{block.__class__.__name__}{name}>"
  25. class NotAttachedError(Exception):
  26. """{block} is not attached to a container yet. Try to insert it first."""
  27. def __init__(self, block: Union[str, "Block"] = "The block"):
  28. doc = cleandoc(cast(str, self.__class__.__doc__))
  29. msg = doc.format(block=_short_repr(block))
  30. super().__init__(msg)
  31. class AlreadyAttachedError(Exception):
  32. """{block} has been already attached to a container.
  33. Try to remove it first using ``detach`` or create a copy using stdlib's
  34. ``copy.deepcopy``.
  35. """
  36. def __init__(self, block: Union[str, "Block"] = "The block"):
  37. doc = cleandoc(cast(str, self.__class__.__doc__))
  38. msg = doc.format(block=_short_repr(block))
  39. super().__init__(msg)
  40. class AssignMultilineValueError(Exception):
  41. """Trying to assign a multi-line value to {block}.
  42. Use the ``set_values`` or ``append`` method to accomplish that.
  43. """
  44. def __init__(self, block: Union[str, "Block"] = "The block"):
  45. doc = cleandoc(cast(str, self.__class__.__doc__))
  46. msg = doc.format(block=_short_repr(block))
  47. super().__init__(msg)
  48. class Block:
  49. """Abstract Block type holding lines
  50. Block objects hold original lines from the configuration file and hold
  51. a reference to a container wherein the object resides.
  52. The type variable ``T`` is a reference for the type of the sibling blocks
  53. inside the container.
  54. """
  55. def __init__(self, container: Optional["Container"] = None):
  56. self._container = container
  57. self._lines: List[str] = []
  58. self._updated = False
  59. def __str__(self) -> str:
  60. return "".join(self._lines)
  61. def __repr__(self) -> str:
  62. return f"<{self.__class__.__name__}: {str(self)!r}>"
  63. def __eq__(self, other) -> bool:
  64. if isinstance(other, self.__class__):
  65. return str(self) == str(other)
  66. else:
  67. return False
  68. def __deepcopy__(self: B, memo: dict) -> B:
  69. clone = self._instantiate_copy()
  70. clone._lines = deepcopy(self._lines, memo)
  71. clone._updated = self._updated
  72. memo[id(self)] = clone
  73. return clone
  74. def _instantiate_copy(self: B) -> B:
  75. """Auxiliary method that allows subclasses calling ``__deepcopy__``"""
  76. return self.__class__(container=None) # allow overwrite for different init args
  77. # ^ A fresh copy should always be made detached from any container
  78. def add_line(self: B, line: str) -> B:
  79. """PRIVATE: this function is not part of the public API of Block.
  80. It is only used internally by other classes of the package during parsing.
  81. Add a line to the current block
  82. Args:
  83. line (str): one line to add
  84. """
  85. self._lines.append(line)
  86. return self
  87. @property
  88. def lines(self) -> List[str]:
  89. return self._lines
  90. @property
  91. def container(self) -> "Container":
  92. """Container holding the block"""
  93. if self._container is None:
  94. raise NotAttachedError(self)
  95. return self._container
  96. @property
  97. def container_idx(self: B) -> int:
  98. """Index of the block within its container"""
  99. return self.container.structure.index(self)
  100. @property
  101. def updated(self) -> bool:
  102. """True if the option was changed/updated, otherwise False"""
  103. # if no lines were added, treat it as updated since we added it
  104. return self._updated or not self._lines
  105. def _builder(self, idx: int) -> "BlockBuilder":
  106. from .builder import BlockBuilder
  107. return BlockBuilder(self.container, idx)
  108. @property
  109. def add_before(self) -> "BlockBuilder":
  110. """Block builder inserting a new block before the current block"""
  111. return self._builder(self.container_idx)
  112. @property
  113. def add_after(self) -> "BlockBuilder":
  114. """Block builder inserting a new block after the current block"""
  115. return self._builder(self.container_idx + 1)
  116. @property
  117. def next_block(self) -> Optional["Block"]:
  118. """Next block in the current container"""
  119. idx = self.container_idx + 1
  120. if idx < len(self.container):
  121. return self.container.structure[idx]
  122. else:
  123. return None
  124. @property
  125. def previous_block(self) -> Optional["Block"]:
  126. """Previous block in the current container"""
  127. idx = self.container_idx - 1
  128. if idx >= 0:
  129. return self.container.structure[idx]
  130. else:
  131. return None
  132. def detach(self: B) -> B:
  133. """Remove and return this block from container"""
  134. self.container._remove_block(self.container_idx)
  135. self._container = None
  136. return self
  137. def has_container(self) -> bool:
  138. """Checks if this block has a container attached"""
  139. return not (self._container is None)
  140. def attach(self: B, container: "Container") -> B:
  141. """PRIVATE: Don't use this as a user!
  142. Rather use `add_*` or the bracket notation
  143. """
  144. if self._container is not None and self._container is not container:
  145. raise AlreadyAttachedError(self)
  146. self._container = container
  147. return self
  148. class Comment(Block):
  149. """Comment block"""
  150. class Space(Block):
  151. """Vertical space block of new lines"""