Source code for configupdater.section

"""Sections are intermediate containers in **ConfigUpdater**'s data model for
configuration files.

They are at the same time :class:`containers <Container>` that hold :mod:`options
<Option>` and :class:`blocks <Block>` nested inside the top level configuration

    Please remember that :meth:`Section.get` method is implemented to mirror the
    :meth:`ConfigParser API <configparser.ConfigParser.set>` and do not correspond to
    the more usual :meth:`` method of *dict-like* objects.
import sys
from typing import TYPE_CHECKING, Optional, Tuple, TypeVar, Union, cast, overload

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

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

    from .document import Document

from .block import Block, Comment, Space
from .builder import BlockBuilder
from .container import Container
from .option import Option

T = TypeVar("T")
S = TypeVar("S", bound="Section")

Content = Union["Option", "Comment", "Space"]
Value = Union["Option", str]

[docs]class Section(Block, Container[Content], MutableMapping[str, "Option"]): """Section block holding options""" def __init__( self, name: str, container: Optional["Document"] = None, raw_comment: str = "" ): self._container: Optional["Document"] = container self._name = name self._raw_comment = raw_comment self._structure: List[Content] = [] self._updated = False super().__init__(container=container) @property def document(self) -> "Document": return cast("Document", self.container)
[docs] def add_option(self: S, entry: "Option") -> S: """Add an Option object to the section Used during initial parsing mainly Args: entry (Option): key value pair as Option object """ entry.attach(self) self._structure.append(entry) return self
[docs] def add_comment(self: S, line: str) -> S: """Add a Comment object to the section Used during initial parsing mainly Args: line (str): one line in the comment """ if isinstance(self.last_block, Comment): comment: Comment = self.last_block else: comment = Comment(container=self) self._structure.append(comment) comment.add_line(line) return self
[docs] def add_space(self: S, line: str) -> S: """Add a Space object to the section Used during initial parsing mainly Args: line (str): one line that defines the space, maybe whitespaces """ if isinstance(self.last_block, Space): space = self.last_block else: space = Space(container=self) self._structure.append(space) space.add_line(line) return self
def _get_option_idx(self, key: str) -> int: return next( i for i, entry in enumerate(self._structure) if isinstance(entry, Option) and entry.key == key ) def __str__(self) -> str: if not self.updated: s = super().__str__() if self._structure and not s.endswith("\n"): s += "\n" else: s = "[{}]{}\n".format(self._name, self.raw_comment) for entry in self._structure: s += str(entry) return s def __repr__(self) -> str: return f"<Section: {!r} {super()._repr_blocks()}>" def _instantiate_copy(self: S) -> S: """Will be called by :meth:`Block.__deepcopy__`""" clone = self.__class__(self._name, container=None) # ^ A fresh copy should always be made detached from any container clone._raw_comment = self._raw_comment return clone def __deepcopy__(self: S, memo: dict) -> S: clone = Block.__deepcopy__(self, memo) # specific due to multi-inheritance return clone._copy_structure(self._structure, memo) def __getitem__(self, key: str) -> "Option": key = self.document.optionxform(key) try: return next(o for o in self.iter_options() if o.key == key) except StopIteration as ex: raise KeyError(f"No option `{key}` found", {"key": key}) from ex def __setitem__(self, key: str, value: Optional[Value] = None): """Set the value of an option. Please notice that this method used :meth:`~configupdater.document.Document.optionxform` to verify if the given option already exists inside the section object. """ # First we check for inconsistencies given_key = self.document.optionxform(key) if isinstance(value, Option): value_key = self.document.optionxform(value.raw_key) # ^ Calculate value_key according to the optionxform of the current # document, in the case the option is imported from a document with a # different optionxform option = value if value_key != given_key: msg = f"Set key `{given_key}` does not equal option key `{value_key}`" raise ValueError(msg) else: option = self.create_option(key, value) if given_key in self: # Replace an existing option if isinstance(value, Option): curr_value = self.__getitem__(given_key) idx = curr_value.container_idx curr_value.detach() value.attach(self) self._structure.insert(idx, value) else: option = self.__getitem__(given_key) option.value = value else: # Append a new option option.attach(self) self._structure.append(option) def __delitem__(self, key: str): try: idx = self._get_option_idx(key=key) del self._structure[idx] except StopIteration as ex: raise KeyError(f"No option `{key}` found", {"key": key}) from ex # MutableMapping[str, Option] for some reason accepts key: object # it actually doesn't matter for the implementation, so we omit the typing def __contains__(self, key) -> bool: """Returns whether the given option exists. Args: option (str): name of option Returns: bool: whether the section exists """ return next((True for o in self.iter_options() if o.key == key), False) # Omit typing so it can represent any object def __eq__(self, other) -> bool: if isinstance(other, self.__class__): return == and self._structure == other._structure else: return False def __iter__(self) -> Iterator[str]: return (b.key for b in self.iter_blocks() if isinstance(b, Option))
[docs] def iter_options(self) -> Iterator["Option"]: """Iterate only over option blocks""" return (entry for entry in self.iter_blocks() if isinstance(entry, Option))
[docs] def option_blocks(self) -> List["Option"]: """Returns option blocks Returns: list: list of :class:`~configupdater.option.Option` blocks """ return list(self.iter_options())
[docs] def options(self) -> List[str]: """Returns option names Returns: list: list of option names as strings """ return [option.key for option in self.iter_options()]
has_option = __contains__
[docs] def to_dict(self) -> Dict[str, Optional[str]]: """Transform to dictionary Returns: dict: dictionary with same content """ return {opt.key: opt.value for opt in self.iter_options()}
@property def name(self) -> str: """Name of the section""" return self._name @name.setter def name(self, value: str): self._name = str(value) self._updated = True @property def raw_comment(self): """Raw comment (includes comment mark) inline with the section header""" return self._raw_comment @raw_comment.setter def raw_comment(self, value: str): """Add/replace a single comment inline with the section header. The given value should be a raw comment, i.e. it needs to contain the comment mark. """ self._raw_comment = value self._updated = True
[docs] def set(self: S, option: str, value: Union[None, str, Iterable[str]] = None) -> S: """Set an option for chaining. Args: option: option name value: value, default None """ option = self.document.optionxform(option) if option not in self.options(): self[option] = self.create_option(option) if not isinstance(value, Iterable) or isinstance(value, str): self[option].value = value else: self[option].set_values(value) return self
[docs] def create_option(self, key: str, value: Optional[str] = None) -> "Option": """Creates an option with kwargs that respect syntax options given to the parent ConfigUpdater object (e.g. ``space_around_delimiters``). Warning: This is a low level API, not intended for public use. Prefer :meth:`set` or :meth:`__setitem__`. """ syntax_opts = getattr(self._container, "syntax_options", {}) kwargs_: dict = { "value": value, "container": self, "space_around_delimiters": syntax_opts.get("space_around_delimiters"), "delimiter": next(iter(syntax_opts.get("delimiters", [])), None), } kwargs = {k: v for k, v in kwargs_.items() if v is not None} return Option(key, **kwargs)
@overload def get(self, key: str) -> Optional["Option"]: ... @overload def get(self, key: str, default: T) -> Union["Option", T]: ...
[docs] def get(self, key, default=None): """This method works similarly to :meth:`dict.get`, and allows you to retrieve an option object by its key. """ return next((o for o in self.iter_options() if o.key == key), default)
# The following is a pragmatic violation of Liskov substitution principle # For some reason MutableMapping.items return a Set-like object # but we want to preserve ordering
[docs] def items(self) -> List[Tuple[str, "Option"]]: # type: ignore[override] """Return a list of (name, option) tuples for each option in this section. Returns: list: list of (name, :class:`Option`) tuples """ return [(opt.key, opt) for opt in self.option_blocks()]
[docs] def insert_at(self, idx: int) -> "BlockBuilder": """Returns a builder inserting a new block at the given index Args: idx (int): index where to insert """ return BlockBuilder(self, idx)
[docs] def clear(self): for block in self._structure: block.detach() self._structure.clear()