option.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. """Options are the ultimate mean of configuration inside a configuration value.
  2. They are always associated with a :attr:`~Option.key` (or the name of the configuration
  3. parameter) and a :attr:`~Option.value`.
  4. Options can also have multi-line values that are usually interpreted as a list of
  5. values.
  6. When editing configuration files with ConfigUpdater, a handy way of setting a multi-line
  7. (or comma separated value) for an specific option is to use the
  8. :meth:`~Option.set_values` method.
  9. """
  10. import sys
  11. import warnings
  12. from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union, cast
  13. if sys.version_info[:2] >= (3, 9): # pragma: no cover
  14. from collections.abc import Iterable
  15. List = list
  16. Dict = dict
  17. else: # pragma: no cover
  18. from typing import Iterable, List
  19. if TYPE_CHECKING:
  20. from .section import Section
  21. from .document import Document
  22. from .block import AssignMultilineValueError, Block
  23. Value = Union["Option", str]
  24. T = TypeVar("T", bound="Option")
  25. def is_multi_line(value: Any) -> bool:
  26. """Checks if a given value has multiple lines"""
  27. if isinstance(value, str):
  28. return "\n" in value
  29. else:
  30. return False
  31. class NoneValueDisallowed(SyntaxWarning):
  32. """Cannot represent <{option} = None>, it will be converted to <{option} = ''>.
  33. Please use ``allow_no_value=True`` with ``ConfigUpdater``.
  34. """
  35. @classmethod
  36. def warn(cls, option):
  37. warnings.warn(cls.__doc__.format(option=option), cls, stacklevel=2)
  38. class Option(Block):
  39. """Option block holding a key/value pair."""
  40. def __init__(
  41. self,
  42. key: str,
  43. value: Optional[str] = None,
  44. container: Optional["Section"] = None,
  45. delimiter: str = "=",
  46. space_around_delimiters: bool = True,
  47. line: Optional[str] = None,
  48. ):
  49. super().__init__(container=container)
  50. self._key = key
  51. self._values: List[Optional[str]] = [] if value is None else [value]
  52. self._value_is_none = value is None
  53. self._delimiter = delimiter
  54. self._value: Optional[str] = None # will be filled after join_multiline_value
  55. self._updated = False
  56. self._multiline_value_joined = False
  57. self._space_around_delimiters = space_around_delimiters
  58. if line:
  59. super().add_line(line)
  60. if value is not None:
  61. self._set_value(value)
  62. def add_value(self, value: Optional[str]):
  63. """PRIVATE: this function is not part of the public API of Option.
  64. It is only used internally by other classes of the package during parsing.
  65. """
  66. self._value_is_none = value is None
  67. self._values.append(value)
  68. def add_line(self, line: str):
  69. """PRIVATE: this function is not part of the public API of Option.
  70. It is only used internally by other classes of the package during parsing.
  71. """
  72. super().add_line(line)
  73. self.add_value(line.strip())
  74. def _join_multiline_value(self):
  75. if not self._multiline_value_joined and not self._value_is_none:
  76. # do what `_join_multiline_value` in ConfigParser would do
  77. self._value = "\n".join(self._values).rstrip()
  78. self._multiline_value_joined = True
  79. def _get_delim(self, determine_suffix=True) -> str:
  80. document = self._document()
  81. opts = getattr(document, "syntax_options", None) or {}
  82. value = self._value
  83. space = self._space_around_delimiters or opts.get("space_around_delimiters")
  84. if determine_suffix and str(value).startswith("\n"):
  85. suffix = "" # no space is needed if we use multi-line arguments
  86. else:
  87. suffix = " "
  88. delim = f" {self._delimiter}{suffix}" if space else self._delimiter
  89. return delim
  90. def value_start_idx(self) -> int:
  91. """Index where the value of the option starts, good for indentation"""
  92. delim = self._get_delim(determine_suffix=False)
  93. return len(f"{self._key}{delim}")
  94. def __str__(self) -> str:
  95. if not self.updated:
  96. return super().__str__()
  97. document = self._document()
  98. opts = getattr(document, "syntax_options", None) or {}
  99. value = self._value
  100. if value is None:
  101. if document is None or opts.get("allow_no_value"):
  102. return f"{self._key}\n"
  103. NoneValueDisallowed.warn(self._key)
  104. return ""
  105. delim = self._get_delim()
  106. return f"{self._key}{delim}{value}\n"
  107. def __repr__(self) -> str:
  108. return f"<Option: {self._key} = {self.value!r}>"
  109. def _instantiate_copy(self: T) -> T:
  110. """Will be called by :meth:`Block.__deepcopy__`"""
  111. self._join_multiline_value()
  112. return self.__class__(
  113. self._key,
  114. self._value,
  115. container=None,
  116. delimiter=self._delimiter,
  117. space_around_delimiters=self._space_around_delimiters,
  118. )
  119. def _document(self) -> Optional["Document"]:
  120. if self._container is None:
  121. return None
  122. return self._container._container # type: ignore
  123. @property
  124. def section(self) -> "Section":
  125. return cast("Section", self.container)
  126. @property
  127. def key(self) -> str:
  128. """Key string associated with the option.
  129. Please notice that the option key is normalized with
  130. :meth:`~configupdater.document.Document.optionxform`.
  131. When the option object is :obj:`detached <configupdater.block.Block.detach>`,
  132. this method will raise a :obj:`NotAttachedError`.
  133. """
  134. return self.section.document.optionxform(self._key)
  135. @key.setter
  136. def key(self, value: str):
  137. self._join_multiline_value()
  138. self._key = value
  139. self._updated = True
  140. @property
  141. def raw_key(self) -> str:
  142. """Equivalent to :obj:`key`, but before applying :meth:`optionxform`."""
  143. return self._key
  144. @property
  145. def value(self) -> Optional[str]:
  146. """Value associated with the given option."""
  147. self._join_multiline_value()
  148. return self._value
  149. @value.setter
  150. def value(self, value: str):
  151. if is_multi_line(value):
  152. raise AssignMultilineValueError(self)
  153. self._set_value(value)
  154. def _set_value(self, value: str):
  155. self._updated = True
  156. self._multiline_value_joined = True
  157. self._value = value
  158. self._values = [value]
  159. def as_list(self, separator="\n") -> List[str]:
  160. """Returns the (multi-line/element) value as a list
  161. Empty list if value is None, single-element list for a one-element
  162. value and an element for each line in a multi-element value.
  163. Args:
  164. separator (str): separator for values, default: line separator
  165. """
  166. if self._value_is_none:
  167. return []
  168. else:
  169. return [v.strip() for v in cast(str, self.value).strip().split(separator)]
  170. def append(self, value: str, **kwargs) -> "Option":
  171. """Append a value to a mult-line value
  172. Args:
  173. value (str): value
  174. kwargs: keyword arguments for `set_values`
  175. """
  176. sep = kwargs.get("separator", None)
  177. if sep is None:
  178. new_values = self.as_list()
  179. else:
  180. new_values = self.as_list(separator=sep)
  181. new_values.append(value)
  182. self.set_values(new_values, **kwargs)
  183. return self
  184. def set_values(
  185. self,
  186. values: Iterable[str],
  187. separator="\n",
  188. indent: Optional[str] = None,
  189. prepend_newline=True,
  190. ):
  191. """Sets the value to a given list of options, e.g. multi-line values
  192. Args:
  193. values (iterable): sequence of values
  194. separator (str): separator for values, default: line separator
  195. indent (optional str): indentation in case of line separator.
  196. If prepend_newline is `True` 4 whitespaces by default, otherwise
  197. determine automatically if `None`.
  198. prepend_newline (bool): start with a new line or not, resp.
  199. """
  200. values = list(values).copy()
  201. self._updated = True
  202. self._multiline_value_joined = True
  203. self._values = cast(List[Optional[str]], values)
  204. if indent is None:
  205. if prepend_newline:
  206. indent = 4 * " "
  207. else:
  208. indent = self.value_start_idx() * " "
  209. # The most common case of multiline values being preceded by a new line
  210. if prepend_newline and "\n" in separator:
  211. values = [""] + values
  212. separator = separator + indent
  213. elif "\n" in separator:
  214. separator = separator + indent
  215. self._value = separator.join(values)