Source code for ini2toml.intermediate_repr

"""Intermediate representations used by ``ini2toml`` when transforming between
the INI and TOML syntaxes.
"""
from collections import UserList
from enum import Enum
from itertools import chain
from pprint import pformat
from textwrap import indent
from types import MappingProxyType
from typing import (
    Any,
    Dict,
    Generic,
    Iterable,
    List,
    Mapping,
    MutableMapping,
    Optional,
    Sequence,
    Tuple,
    TypeVar,
    Union,
    cast,
)
from uuid import uuid4

T = TypeVar("T")
S = TypeVar("S")
R = TypeVar("R", bound="IntermediateRepr")

KV = Tuple[str, T]

NotGiven = Enum("NotGiven", "NOT_GIVEN")
NOT_GIVEN = NotGiven.NOT_GIVEN

EMPTY: Mapping = MappingProxyType({})


[docs]class HiddenKey: __slots__ = ("_value",) def __init__(self): self._value = uuid4().int def __eq__(self, other): return self.__class__ is other.__class__ and self._value == other._value def __hash__(self): return hash((self.__class__.__name__, self._value)) def __str__(self): return f"{self.__class__.__name__}()" __repr__ = __str__
[docs]class WhitespaceKey(HiddenKey): pass
[docs]class CommentKey(HiddenKey): pass
Key = Union[str, HiddenKey, Tuple[Union[str, HiddenKey], ...]]
[docs]class IntermediateRepr(MutableMapping): def __init__( self, elements: Mapping[Key, Any] = EMPTY, order: Sequence[Key] = (), inline_comment: str = "", **kwargs, ): el = chain(elements.items(), kwargs.items()) self.elements: Dict[Key, Any] = {} self.order: List[Key] = [] self.inline_comment = inline_comment self.elements.update(el) self.order.extend(order or self.elements.keys()) elem_not_in_order = any(k not in self.order for k in self.elements) order_not_in_elem = any(k not in self.elements for k in self.order) if elem_not_in_order or order_not_in_elem: raise ValueError(f"{order} and {elements} need to have the same keys") def __repr__(self): inner = ",\n".join( indent(f"{k}={pformat(getattr(self, k))}", " ") for k in ("elements", "order", "inline_comment") ) return f"{self.__class__.__name__}(\n{inner}\n)" def __eq__(self, other): L = len(self) if not ( isinstance(other, self.__class__) and self.inline_comment == other.inline_comment and len(other) == L ): return False self_ = [(str(k), v) for k, v in self.items()] other_ = [(str(k), v) for k, v in other.items()] return all(self_[i] == other_[i] for i in range(L))
[docs] def rename(self, old_key: Key, new_key: Key, ignore_missing=False): """This method renames an existing key, without changing its position. Notice that ``new_key`` cannot be already present, and that trying to rename a non-pre-existing key will also result in error (unless ``ignore_missing=True``). """ if old_key == new_key: return self if new_key in self.order: raise KeyError(f"new_key={new_key!r} already exists") if old_key not in self.order and ignore_missing: return self i = self.order.index(old_key) self.order[i] = new_key self.elements[new_key] = self.elements.pop(old_key) return self
[docs] def insert(self, position: int, key: Key, value: Any): """Simulate the position-aware :meth:`collections.abc.MutableMapping.insert` method, but also require a ``key`` to be specified. """ if key in self.order: raise KeyError(f"key={key!r} already exists") self.order.insert(position, key) self.elements[key] = value
[docs] def index(self, key: Key) -> int: """Find the position of ``key``""" return self.order.index(key)
[docs] def append(self, key: Key, value: Any): """Simulate the position-aware :meth:`collections.abc.MutableMapping.append` method, but also require a ``key`` to be specified. """ self.insert(len(self.order), key, value)
[docs] def copy(self: R) -> R: return self.__class__(self.elements.copy(), self.order[:], self.inline_comment)
[docs] def replace_first_remove_others( self, existing_keys: Sequence[Key], new_key: Key, value: Any ): """Find the first key in ``existing_keys`` that existing in the intermediate representation, and replaces it with ``new_key`` (similar to :meth:`replace`). All the other keys in ``existing_keys`` are removed and the value of ``new_key`` is set to ``value``. """ idx = [self.index(k) for k in existing_keys if k in self] if not idx: i = len(self) else: i = sorted(idx)[0] for key in existing_keys: self.pop(key, None) self.insert(i, new_key, value) return i
def __getitem__(self, key: Key): return self.elements[key] def __setitem__(self, key: Key, value: Any): if key not in self.elements: self.order.append(key) self.elements[key] = value def __delitem__(self, key: Key): del self.elements[key] self.order.remove(key) def __iter__(self): return iter(self.order) def __len__(self): return len(self.order)
# These objects hold information about the processed values + comments # in such a way that we can later convert them to TOML while still preserving # the comments (if we want to).
[docs]class Commented(Generic[T]): def __init__( self, value: Union[T, NotGiven] = NOT_GIVEN, comment: Optional[str] = None, ): self.value = value self.comment = comment def __eq__(self, other): return ( self.__class__ is other.__class__ and self.value == other.value and self.comment == other.comment )
[docs] def comment_only(self): return self.value is NOT_GIVEN
[docs] def has_comment(self): return bool(self.comment)
[docs] def value_or(self, fallback: S) -> Union[T, S]: return fallback if self.value is NOT_GIVEN else self.value
[docs] def as_commented_list(self) -> "CommentedList[T]": value = [] if self.value is NOT_GIVEN else [self.value] return CommentedList([Commented(value, self.comment)])
def __repr__(self): return f"{self.__class__.__name__}({self.value!r}, {self.comment!r})" def _iter_comments(self) -> Iterable[str]: if self.comment: yield self.comment
[docs]class CommentedList(Generic[T], UserList): def __init__(self, data: Sequence[Commented[List[T]]] = ()): super().__init__(data)
[docs] def as_list(self) -> list: out = [] for entry in self: values = entry.value_or([]) for value in values: out.append(value) return out
[docs] def insert_line(self, i, values: Iterable[T], comment: Optional[str] = None): values = list(values) if values or comment: self.insert(i, Commented(values, comment))
def _iter_comments(self: Iterable[Commented]) -> Iterable[str]: return chain.from_iterable(entry._iter_comments() for entry in self)
[docs]class CommentedKV(Generic[T], UserList): def __init__(self, data: Sequence[Commented[List[KV[T]]]] = ()): super().__init__(data)
[docs] def find(self, key: str) -> Optional[Tuple[int, int]]: for i, row in enumerate(self): for j, item in enumerate(row.value_or([])): if item[0] == key: return (i, j) return None
[docs] def insert_line(self, i, values: Iterable[KV[T]], comment: Optional[str] = None): values = list(values) if values or comment: self.insert(i, Commented(values, comment)) return self
[docs] def as_dict(self) -> dict: out = {} for entry in self: values = (v for v in entry.value_or([cast(KV, ())]) if v) for k, v in values: out[k] = v return out
_iter_comments = CommentedList._iter_comments
[docs] def to_ir(self) -> IntermediateRepr: """:class:`CommentedKV` are usually intended to represent INI options, while :class:`IntermediateRepr` are usually intended to represent INI sections. Therefore this function allows "promoting" an option-equivalent to a section-equivalent representation. """ irepr = IntermediateRepr() for row in self: key, value = None, None for key, value in row.value_or([]): irepr[key] = value if row.has_comment(): if key: irepr[key] = Commented(value, row.comment) else: irepr[CommentKey()] = row.comment return irepr