"""Parser for configuration files (normally ``*.cfg/*.ini``) A configuration file consists of sections, lead by a "[section]" header, and followed by "name: value" entries, with continuations and such in the style of RFC 822. The basic idea of **ConfigUpdater** is that a configuration file consists of three kinds of building blocks: sections, comments and spaces for separation. A section itself consists of three kinds of blocks: options, comments and spaces. This gives us the corresponding data structures to describe a configuration file. A general block object contains the lines which were parsed and make up the block. If a block object was not changed then during writing the same lines that were parsed will be used to express the block. In case a block, e.g. an option, was changed, it is marked as `updated` and its values will be transformed into a corresponding string during an update of a configuration file. .. note:: ConfigUpdater is based on Python's ConfigParser source code, specially regarding the ``parser`` module. The main parsing rules and algorithm are preserved, however ConfigUpdater implements its own modified version of the abstract syntax tree to support retaining comments and whitespace in an attempt to provide format-preserving document manipulation. The copyright and license of the original ConfigParser code is included as an attachment to ConfigUpdater's own license, at the root of the source code repository; see the file LICENSE for details. """ import io import os import re import sys from configparser import ( DuplicateOptionError, DuplicateSectionError, MissingSectionHeaderError, NoOptionError, NoSectionError, ParsingError, ) from types import MappingProxyType as ReadOnlyMapping from typing import Callable, Optional, Tuple, Type, TypeVar, Union, cast, overload if sys.version_info[:2] >= (3, 9): # pragma: no cover from collections.abc import Iterable, Mapping List = list Dict = dict else: # pragma: no cover from typing import Iterable, List, Dict, Mapping from .block import Comment, Space from .document import Document from .option import Option from .section import Section __all__ = [ "NoSectionError", "DuplicateOptionError", "DuplicateSectionError", "NoOptionError", "ParsingError", "MissingSectionHeaderError", "InconsistentStateError", "Parser", ] T = TypeVar("T") E = TypeVar("E", bound=Exception) D = TypeVar("D", bound=Document) if sys.version_info[:2] >= (3, 7): # pragma: no cover PathLike = Union[str, bytes, os.PathLike] else: # pragma: no cover PathLike = Union[str, os.PathLike] ConfigContent = Union["Section", "Comment", "Space"] class InconsistentStateError(Exception): # pragma: no cover (not expected to happen) """Internal parser error, some of the parsing algorithm assumptions was violated, and the internal state machine ended up in an unpredicted state. """ def __init__(self, msg, fpname="", lineno: int = -1, line: str = "???"): super().__init__(msg) self.args = (msg, fpname, lineno, line) def __str__(self): (msg, fpname, lineno, line) = self.args return f"{msg}\n{fpname}({lineno}): {line!r}" class Parser: """Parser for updating configuration files. ConfigUpdater's parser follows ConfigParser with some differences: * inline comments are treated as part of a key's value, * only a single config file can be updated at a time, * the original case of sections and keys are kept, * control over the position of a new section/key. Following features are **deliberately not** implemented: * interpolation of values, * propagation of parameters from the default section, * conversions of values, * passing key/value-pairs with ``default`` argument, * non-strict mode allowing duplicate sections and keys. """ # Regular expressions for parsing section headers and options _SECT_TMPL: str = r""" \[ # [ (?P
.+) # very permissive! \] # ] (?P.*) # match any suffix """ _OPT_TMPL: str = r""" (?P