Source code for configupdater.option

"""Options are the ultimate mean of configuration inside a configuration value.

They are always associated with a :attr:`~Option.key` (or the name of the configuration
parameter) and a :attr:`~Option.value`.

Options can also have multi-line values that are usually interpreted as a list of
values.

When editing configuration files with ConfigUpdater, a handy way of setting a multi-line
(or comma separated value) for an specific option is to use the
:meth:`~Option.set_values` method.
"""
import sys
import warnings
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union, cast

if sys.version_info[:2] >= (3, 9):  # pragma: no cover
    from collections.abc import Iterable

    List = list
    Dict = dict
else:  # pragma: no cover
    from typing import Iterable, List

if TYPE_CHECKING:
    from .section import Section
    from .document import Document

from .block import AssignMultilineValueError, Block

Value = Union["Option", str]
T = TypeVar("T", bound="Option")


def is_multi_line(value: Any) -> bool:
    """Checks if a given value has multiple lines"""
    if isinstance(value, str):
        return "\n" in value
    else:
        return False


[docs] class NoneValueDisallowed(SyntaxWarning): """Cannot represent <{option} = None>, it will be converted to <{option} = ''>. Please use ``allow_no_value=True`` with ``ConfigUpdater``. """ @classmethod def warn(cls, option): warnings.warn(cls.__doc__.format(option=option), cls, stacklevel=2)
[docs] class Option(Block): """Option block holding a key/value pair.""" def __init__( self, key: str, value: Optional[str] = None, container: Optional["Section"] = None, delimiter: str = "=", space_around_delimiters: bool = True, line: Optional[str] = None, ): super().__init__(container=container) self._key = key self._values: List[Optional[str]] = [] if value is None else [value] self._value_is_none = value is None self._delimiter = delimiter self._value: Optional[str] = None # will be filled after join_multiline_value self._updated = False self._multiline_value_joined = False self._space_around_delimiters = space_around_delimiters if line: super().add_line(line) if value is not None: self._set_value(value)
[docs] def add_value(self, value: Optional[str]): """PRIVATE: this function is not part of the public API of Option. It is only used internally by other classes of the package during parsing. """ self._value_is_none = value is None self._values.append(value)
[docs] def add_line(self, line: str): """PRIVATE: this function is not part of the public API of Option. It is only used internally by other classes of the package during parsing. """ super().add_line(line) self.add_value(line.strip())
def _join_multiline_value(self): if not self._multiline_value_joined and not self._value_is_none: # do what `_join_multiline_value` in ConfigParser would do self._value = "\n".join(self._values).rstrip() self._multiline_value_joined = True def _get_delim(self, determine_suffix=True) -> str: document = self._document() opts = getattr(document, "syntax_options", None) or {} value = self._value space = self._space_around_delimiters or opts.get("space_around_delimiters") if determine_suffix and str(value).startswith("\n"): suffix = "" # no space is needed if we use multi-line arguments else: suffix = " " delim = f" {self._delimiter}{suffix}" if space else self._delimiter return delim
[docs] def value_start_idx(self) -> int: """Index where the value of the option starts, good for indentation""" delim = self._get_delim(determine_suffix=False) return len(f"{self._key}{delim}")
def __str__(self) -> str: if not self.updated: return super().__str__() document = self._document() opts = getattr(document, "syntax_options", None) or {} value = self._value if value is None: if document is None or opts.get("allow_no_value"): return f"{self._key}\n" NoneValueDisallowed.warn(self._key) return "" delim = self._get_delim() return f"{self._key}{delim}{value}\n" def __repr__(self) -> str: return f"<Option: {self._key} = {self.value!r}>" def _instantiate_copy(self: T) -> T: """Will be called by :meth:`Block.__deepcopy__`""" self._join_multiline_value() return self.__class__( self._key, self._value, container=None, delimiter=self._delimiter, space_around_delimiters=self._space_around_delimiters, ) def _document(self) -> Optional["Document"]: if self._container is None: return None return self._container._container # type: ignore @property def section(self) -> "Section": return cast("Section", self.container) @property def key(self) -> str: """Key string associated with the option. Please notice that the option key is normalized with :meth:`~configupdater.document.Document.optionxform`. When the option object is :obj:`detached <configupdater.block.Block.detach>`, this method will raise a :obj:`NotAttachedError`. """ return self.section.document.optionxform(self._key) @key.setter def key(self, value: str): self._join_multiline_value() self._key = value self._updated = True @property def raw_key(self) -> str: """Equivalent to :obj:`key`, but before applying :meth:`optionxform`.""" return self._key @property def value(self) -> Optional[str]: """Value associated with the given option.""" self._join_multiline_value() return self._value @value.setter def value(self, value: str): if is_multi_line(value): raise AssignMultilineValueError(self) self._set_value(value) def _set_value(self, value: str): self._updated = True self._multiline_value_joined = True self._value = value self._values = [value]
[docs] def as_list(self, separator="\n") -> List[str]: """Returns the (multi-line/element) value as a list Empty list if value is None, single-element list for a one-element value and an element for each line in a multi-element value. Args: separator (str): separator for values, default: line separator """ if self._value_is_none: return [] else: return [v.strip() for v in cast(str, self.value).strip().split(separator)]
[docs] def append(self, value: str, **kwargs) -> "Option": """Append a value to a mult-line value Args: value (str): value kwargs: keyword arguments for `set_values` """ sep = kwargs.get("separator", None) if sep is None: new_values = self.as_list() else: new_values = self.as_list(separator=sep) new_values.append(value) self.set_values(new_values, **kwargs) return self
[docs] def set_values( self, values: Iterable[str], separator="\n", indent: Optional[str] = None, prepend_newline=True, ): """Sets the value to a given list of options, e.g. multi-line values Args: values (iterable): sequence of values separator (str): separator for values, default: line separator indent (optional str): indentation in case of line separator. If prepend_newline is `True` 4 whitespaces by default, otherwise determine automatically if `None`. prepend_newline (bool): start with a new line or not, resp. """ values = list(values).copy() self._updated = True self._multiline_value_joined = True self._values = cast(List[Optional[str]], values) if indent is None: if prepend_newline: indent = 4 * " " else: indent = self.value_start_idx() * " " # The most common case of multiline values being preceded by a new line if prepend_newline and "\n" in separator: values = [""] + values separator = separator + indent elif "\n" in separator: separator = separator + indent self._value = separator.join(values)