2013-05-23 10:19:43 +02:00
|
|
|
"""Access and/or modify INI files
|
|
|
|
|
|
|
|
* Compatiable with ConfigParser
|
|
|
|
* Preserves order of sections & options
|
|
|
|
* Preserves comments/blank lines/etc
|
|
|
|
* More conveninet access to data
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
>>> from io import StringIO
|
2013-05-23 10:19:43 +02:00
|
|
|
>>> sio = StringIO('''# configure foo-application
|
|
|
|
... [foo]
|
|
|
|
... bar1 = qualia
|
|
|
|
... bar2 = 1977
|
|
|
|
... [foo-ext]
|
|
|
|
... special = 1''')
|
|
|
|
|
|
|
|
>>> cfg = INIConfig(sio)
|
2022-04-25 10:02:43 +02:00
|
|
|
>>> print(cfg.foo.bar1)
|
2013-05-23 10:19:43 +02:00
|
|
|
qualia
|
2022-04-25 10:02:43 +02:00
|
|
|
>>> print(cfg['foo-ext'].special)
|
2013-05-23 10:19:43 +02:00
|
|
|
1
|
|
|
|
>>> cfg.foo.newopt = 'hi!'
|
|
|
|
>>> cfg.baz.enabled = 0
|
|
|
|
|
2022-04-25 10:02:43 +02:00
|
|
|
>>> print(cfg)
|
2013-05-23 10:19:43 +02:00
|
|
|
# configure foo-application
|
|
|
|
[foo]
|
|
|
|
bar1 = qualia
|
|
|
|
bar2 = 1977
|
|
|
|
newopt = hi!
|
|
|
|
[foo-ext]
|
|
|
|
special = 1
|
|
|
|
<BLANKLINE>
|
|
|
|
[baz]
|
|
|
|
enabled = 0
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
# An ini parser that supports ordered sections/options
|
|
|
|
# Also supports updates, while preserving structure
|
2024-11-29 22:54:39 +01:00
|
|
|
# Backward-compatible with ConfigParser
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
import re
|
2022-04-25 10:02:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
from typing import Any, Callable, Dict, TextIO, Iterator, List, Optional, Set, Union
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
|
|
from .configparser import DEFAULTSECT, ParsingError, MissingSectionHeaderError
|
2022-04-25 10:02:43 +02:00
|
|
|
|
|
|
|
from . import config
|
2024-11-29 22:54:39 +01:00
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
from compat import RawConfigParser
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
class LineType:
|
|
|
|
line: Optional[str] = None
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __init__(self, line: Optional[str] = None) -> None:
|
2013-05-23 10:19:43 +02:00
|
|
|
if line is not None:
|
2024-11-29 22:54:39 +01:00
|
|
|
self.line = line.strip("\n")
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
# Return the original line for unmodified objects
|
|
|
|
# Otherwise construct using the current attribute values
|
2024-11-29 22:54:39 +01:00
|
|
|
def __str__(self) -> str:
|
2013-05-23 10:19:43 +02:00
|
|
|
if self.line is not None:
|
|
|
|
return self.line
|
|
|
|
else:
|
|
|
|
return self.to_string()
|
|
|
|
|
|
|
|
# If an attribute is modified after initialization
|
|
|
|
# set line to None since it is no longer accurate.
|
2024-11-29 22:54:39 +01:00
|
|
|
def __setattr__(self, name: str, value: object) -> None:
|
|
|
|
if hasattr(self, name):
|
|
|
|
self.__dict__["line"] = None
|
2013-05-23 10:19:43 +02:00
|
|
|
self.__dict__[name] = value
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def to_string(self) -> str:
|
|
|
|
# FIXME Raise NotImplementedError instead
|
|
|
|
raise Exception("This method must be overridden in derived classes")
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
|
|
|
|
class SectionLine(LineType):
|
2024-11-29 22:54:39 +01:00
|
|
|
regex = re.compile(r"^\[" r"(?P<name>[^]]+)" r"\]\s*" r"((?P<csep>;|#)(?P<comment>.*))?$")
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
name: str,
|
|
|
|
comment: Optional[str] = None,
|
|
|
|
comment_separator: Optional[str] = None,
|
|
|
|
comment_offset: int = -1,
|
|
|
|
line: Optional[str] = None,
|
|
|
|
) -> None:
|
|
|
|
super().__init__(line)
|
|
|
|
self.name: str = name
|
|
|
|
self.comment: Optional[str] = comment
|
|
|
|
self.comment_separator: Optional[str] = comment_separator
|
|
|
|
self.comment_offset: int = comment_offset
|
|
|
|
|
|
|
|
def to_string(self) -> str:
|
|
|
|
out: str = "[" + self.name + "]"
|
2013-05-23 10:19:43 +02:00
|
|
|
if self.comment is not None:
|
|
|
|
# try to preserve indentation of comments
|
2024-11-29 22:54:39 +01:00
|
|
|
out = (out + " ").ljust(self.comment_offset)
|
2013-05-23 10:19:43 +02:00
|
|
|
out = out + self.comment_separator + self.comment
|
|
|
|
return out
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
@classmethod
|
|
|
|
def parse(cls, line: str) -> Optional["SectionLine"]:
|
|
|
|
m: Optional[re.Match] = cls.regex.match(line.rstrip())
|
2013-05-23 10:19:43 +02:00
|
|
|
if m is None:
|
|
|
|
return None
|
2024-11-29 22:54:39 +01:00
|
|
|
return cls(m.group("name"), m.group("comment"), m.group("csep"), m.start("csep"), line)
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
|
|
|
|
class OptionLine(LineType):
|
2024-11-29 22:54:39 +01:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
name: str,
|
|
|
|
value: object,
|
|
|
|
separator: str = " = ",
|
|
|
|
comment: Optional[str] = None,
|
|
|
|
comment_separator: Optional[str] = None,
|
|
|
|
comment_offset: int = -1,
|
|
|
|
line: Optional[str] = None,
|
|
|
|
) -> None:
|
|
|
|
super().__init__(line)
|
|
|
|
self.name: str = name
|
|
|
|
self.value: object = value
|
|
|
|
self.separator: str = separator
|
|
|
|
self.comment: Optional[str] = comment
|
|
|
|
self.comment_separator: Optional[str] = comment_separator
|
|
|
|
self.comment_offset: int = comment_offset
|
|
|
|
|
|
|
|
def to_string(self) -> str:
|
|
|
|
out: str = "%s%s%s" % (self.name, self.separator, self.value)
|
2013-05-23 10:19:43 +02:00
|
|
|
if self.comment is not None:
|
|
|
|
# try to preserve indentation of comments
|
2024-11-29 22:54:39 +01:00
|
|
|
out = (out + " ").ljust(self.comment_offset)
|
2013-05-23 10:19:43 +02:00
|
|
|
out = out + self.comment_separator + self.comment
|
|
|
|
return out
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
regex = re.compile(r"^(?P<name>[^:=\s[][^:=]*)" r"(?P<sep>[:=]\s*)" r"(?P<value>.*)$")
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
@classmethod
|
|
|
|
def parse(cls, line: str) -> Optional["OptionLine"]:
|
|
|
|
m: Optional[re.Match] = cls.regex.match(line.rstrip())
|
2013-05-23 10:19:43 +02:00
|
|
|
if m is None:
|
|
|
|
return None
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
name: str = m.group("name").rstrip()
|
|
|
|
value: str = m.group("value")
|
|
|
|
sep: str = m.group("name")[len(name) :] + m.group("sep")
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
# comments are not detected in the regex because
|
|
|
|
# ensuring total compatibility with ConfigParser
|
|
|
|
# requires that:
|
|
|
|
# option = value ;comment // value=='value'
|
|
|
|
# option = value;1 ;comment // value=='value;1 ;comment'
|
|
|
|
#
|
|
|
|
# Doing this in a regex would be complicated. I
|
|
|
|
# think this is a bug. The whole issue of how to
|
|
|
|
# include ';' in the value needs to be addressed.
|
|
|
|
# Also, '#' doesn't mark comments in options...
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
coff: int = value.find(";")
|
|
|
|
if coff != -1 and value[coff - 1].isspace():
|
|
|
|
comment = value[coff + 1 :]
|
2013-05-23 10:19:43 +02:00
|
|
|
csep = value[coff]
|
|
|
|
value = value[:coff].rstrip()
|
2024-11-29 22:54:39 +01:00
|
|
|
coff = m.start("value") + coff
|
2013-05-23 10:19:43 +02:00
|
|
|
else:
|
|
|
|
comment = None
|
|
|
|
csep = None
|
|
|
|
coff = -1
|
|
|
|
|
|
|
|
return cls(name, value, sep, comment, csep, coff, line)
|
|
|
|
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def change_comment_syntax(comment_chars: str = "%;#", allow_rem: bool = False) -> None:
|
|
|
|
comment_chars: str = re.sub(r"([\]\-\^])", r"\\\1", comment_chars)
|
|
|
|
regex: str = r"^(?P<csep>[%s]" % comment_chars
|
2013-05-23 10:19:43 +02:00
|
|
|
if allow_rem:
|
2024-11-29 22:54:39 +01:00
|
|
|
regex += "|[rR][eE][mM]"
|
|
|
|
regex += r")(?P<comment>.*)$"
|
2013-05-23 10:19:43 +02:00
|
|
|
CommentLine.regex = re.compile(regex)
|
|
|
|
|
2022-04-25 10:02:43 +02:00
|
|
|
|
2013-05-23 10:19:43 +02:00
|
|
|
class CommentLine(LineType):
|
2024-11-29 22:54:39 +01:00
|
|
|
regex: re.Pattern = re.compile(r"^(?P<csep>[;#]|[rR][eE][mM])" r"(?P<comment>.*)$")
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __init__(self, comment: str = "", separator: str = "#", line: Optional[str] = None) -> None:
|
|
|
|
super().__init__(line)
|
|
|
|
self.comment: str = comment
|
|
|
|
self.separator: str = separator
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def to_string(self) -> str:
|
2013-05-23 10:19:43 +02:00
|
|
|
return self.separator + self.comment
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
@classmethod
|
|
|
|
def parse(cls, line: str) -> Optional["CommentLine"]:
|
|
|
|
m: Optional[re.Match] = cls.regex.match(line.rstrip())
|
2013-05-23 10:19:43 +02:00
|
|
|
if m is None:
|
|
|
|
return None
|
2024-11-29 22:54:39 +01:00
|
|
|
return cls(m.group("comment"), m.group("csep"), line)
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
|
|
|
|
class EmptyLine(LineType):
|
|
|
|
# could make this a singleton
|
2024-11-29 22:54:39 +01:00
|
|
|
def to_string(self) -> str:
|
|
|
|
return ""
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
value = property(lambda self: "")
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
@classmethod
|
|
|
|
def parse(cls, line: str) -> Optional["EmptyLine"]:
|
2022-04-25 10:02:43 +02:00
|
|
|
if line.strip():
|
|
|
|
return None
|
2013-05-23 10:19:43 +02:00
|
|
|
return cls(line)
|
2022-04-25 10:02:43 +02:00
|
|
|
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
class ContinuationLine(LineType):
|
2024-11-29 22:54:39 +01:00
|
|
|
regex: re.Pattern = re.compile(r"^\s+(?P<value>.*)$")
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __init__(self, value: str, value_offset: Optional[int] = None, line: Optional[str] = None) -> None:
|
|
|
|
super().__init__(line)
|
2013-05-23 10:19:43 +02:00
|
|
|
self.value = value
|
|
|
|
if value_offset is None:
|
|
|
|
value_offset = 8
|
2024-11-29 22:54:39 +01:00
|
|
|
self.value_offset: int = value_offset
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def to_string(self) -> str:
|
|
|
|
return " " * self.value_offset + self.value
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
@classmethod
|
|
|
|
def parse(cls, line: str) -> Optional["ContinuationLine"]:
|
|
|
|
m: Optional[re.Match] = cls.regex.match(line.rstrip())
|
2013-05-23 10:19:43 +02:00
|
|
|
if m is None:
|
|
|
|
return None
|
2024-11-29 22:54:39 +01:00
|
|
|
return cls(m.group("value"), m.start("value"), line)
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
class LineContainer:
|
|
|
|
def __init__(self, d: Optional[Union[List[LineType], LineType]] = None) -> None:
|
2013-05-23 10:19:43 +02:00
|
|
|
self.contents = []
|
2024-11-29 22:54:39 +01:00
|
|
|
self.orgvalue: str = None
|
2013-05-23 10:19:43 +02:00
|
|
|
if d:
|
2024-11-29 22:54:39 +01:00
|
|
|
if isinstance(d, list):
|
|
|
|
self.extend(d)
|
|
|
|
else:
|
|
|
|
self.add(d)
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def add(self, x: LineType) -> None:
|
2013-05-23 10:19:43 +02:00
|
|
|
self.contents.append(x)
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def extend(self, x: List[LineType]) -> None:
|
|
|
|
for i in x:
|
|
|
|
self.add(i)
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def get_name(self) -> str:
|
2013-05-23 10:19:43 +02:00
|
|
|
return self.contents[0].name
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def set_name(self, data: str) -> None:
|
2013-05-23 10:19:43 +02:00
|
|
|
self.contents[0].name = data
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def get_value(self) -> str:
|
2013-05-23 10:19:43 +02:00
|
|
|
if self.orgvalue is not None:
|
|
|
|
return self.orgvalue
|
|
|
|
elif len(self.contents) == 1:
|
|
|
|
return self.contents[0].value
|
|
|
|
else:
|
2024-11-29 22:54:39 +01:00
|
|
|
return "\n".join([("%s" % x.value) for x in self.contents if not isinstance(x, CommentLine)])
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def set_value(self, data: object) -> None:
|
2013-05-23 10:19:43 +02:00
|
|
|
self.orgvalue = data
|
2024-11-29 22:54:39 +01:00
|
|
|
lines: List[str] = ("%s" % data).split("\n")
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
# If there is an existing ContinuationLine, use its offset
|
2024-11-29 22:54:39 +01:00
|
|
|
value_offset: Optional[int] = None
|
2013-05-23 10:19:43 +02:00
|
|
|
for v in self.contents:
|
|
|
|
if isinstance(v, ContinuationLine):
|
|
|
|
value_offset = v.value_offset
|
|
|
|
break
|
|
|
|
|
|
|
|
# Rebuild contents list, preserving initial OptionLine
|
|
|
|
self.contents = self.contents[0:1]
|
|
|
|
self.contents[0].value = lines[0]
|
|
|
|
del lines[0]
|
|
|
|
for line in lines:
|
|
|
|
if line.strip():
|
|
|
|
self.add(ContinuationLine(line, value_offset))
|
|
|
|
else:
|
|
|
|
self.add(EmptyLine())
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def get_line_number(self) -> Optional[int]:
|
|
|
|
return self.contents[0].line_number if self.contents else None
|
|
|
|
|
2013-05-23 10:19:43 +02:00
|
|
|
name = property(get_name, set_name)
|
2022-04-25 10:02:43 +02:00
|
|
|
|
2013-05-23 10:19:43 +02:00
|
|
|
value = property(get_value, set_value)
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
line_number = property(get_line_number)
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __str__(self) -> str:
|
|
|
|
s: List[str] = [x.__str__() for x in self.contents]
|
|
|
|
return "\n".join(s)
|
|
|
|
|
|
|
|
def finditer(self, key: str) -> Iterator[Union[SectionLine, OptionLine]]:
|
2013-05-23 10:19:43 +02:00
|
|
|
for x in self.contents[::-1]:
|
2024-11-29 22:54:39 +01:00
|
|
|
if hasattr(x, "name") and x.name == key:
|
2013-05-23 10:19:43 +02:00
|
|
|
yield x
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def find(self, key: str) -> Union[SectionLine, OptionLine]:
|
2013-05-23 10:19:43 +02:00
|
|
|
for x in self.finditer(key):
|
|
|
|
return x
|
|
|
|
raise KeyError(key)
|
|
|
|
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def _make_xform_property(myattrname: str, srcattrname: Optional[str] = None) -> property:
|
|
|
|
private_attrname: str = myattrname + "value"
|
|
|
|
private_srcname: str = myattrname + "source"
|
2013-05-23 10:19:43 +02:00
|
|
|
if srcattrname is None:
|
|
|
|
srcattrname = myattrname
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def getfn(self) -> Callable:
|
|
|
|
srcobj: Optional[object] = getattr(self, private_srcname)
|
2013-05-23 10:19:43 +02:00
|
|
|
if srcobj is not None:
|
|
|
|
return getattr(srcobj, srcattrname)
|
|
|
|
else:
|
|
|
|
return getattr(self, private_attrname)
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def setfn(self, value: Callable) -> None:
|
|
|
|
srcobj: Optional[object] = getattr(self, private_srcname)
|
2013-05-23 10:19:43 +02:00
|
|
|
if srcobj is not None:
|
|
|
|
setattr(srcobj, srcattrname, value)
|
|
|
|
else:
|
|
|
|
setattr(self, private_attrname, value)
|
|
|
|
|
|
|
|
return property(getfn, setfn)
|
|
|
|
|
|
|
|
|
|
|
|
class INISection(config.ConfigNamespace):
|
2024-11-29 22:54:39 +01:00
|
|
|
_lines: List[LineContainer] = None
|
|
|
|
_options: Dict[str, object] = None
|
|
|
|
_defaults: Optional["INISection"] = None
|
|
|
|
_optionxformvalue: "INIConfig" = None
|
|
|
|
_optionxformsource: "INIConfig" = None
|
|
|
|
_compat_skip_empty_lines: Set[str] = set()
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
lineobj: LineContainer,
|
|
|
|
defaults: Optional["INISection"] = None,
|
|
|
|
optionxformvalue: Optional["INIConfig"] = None,
|
|
|
|
optionxformsource: Optional["INIConfig"] = None,
|
|
|
|
) -> None:
|
2013-05-23 10:19:43 +02:00
|
|
|
self._lines = [lineobj]
|
|
|
|
self._defaults = defaults
|
|
|
|
self._optionxformvalue = optionxformvalue
|
|
|
|
self._optionxformsource = optionxformsource
|
|
|
|
self._options = {}
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
_optionxform = _make_xform_property("_optionxform")
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def _compat_get(self, key: str) -> str:
|
2013-05-23 10:19:43 +02:00
|
|
|
# identical to __getitem__ except that _compat_XXX
|
|
|
|
# is checked for backward-compatible handling
|
2024-11-29 22:54:39 +01:00
|
|
|
if key == "__name__":
|
2013-05-23 10:19:43 +02:00
|
|
|
return self._lines[-1].name
|
2024-11-29 22:54:39 +01:00
|
|
|
if self._optionxform:
|
|
|
|
key = self._optionxform(key)
|
2013-05-23 10:19:43 +02:00
|
|
|
try:
|
2024-11-29 22:54:39 +01:00
|
|
|
value: str = self._options[key].value
|
|
|
|
del_empty: bool = key in self._compat_skip_empty_lines
|
2013-05-23 10:19:43 +02:00
|
|
|
except KeyError:
|
|
|
|
if self._defaults and key in self._defaults._options:
|
|
|
|
value = self._defaults._options[key].value
|
|
|
|
del_empty = key in self._defaults._compat_skip_empty_lines
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
if del_empty:
|
2024-11-29 22:54:39 +01:00
|
|
|
value = re.sub("\n+", "\n", value)
|
2013-05-23 10:19:43 +02:00
|
|
|
return value
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def _getitem(self, key: str) -> object:
|
|
|
|
if key == "__name__":
|
2013-05-23 10:19:43 +02:00
|
|
|
return self._lines[-1].name
|
2024-11-29 22:54:39 +01:00
|
|
|
if self._optionxform:
|
|
|
|
key = self._optionxform(key)
|
2013-05-23 10:19:43 +02:00
|
|
|
try:
|
|
|
|
return self._options[key].value
|
|
|
|
except KeyError:
|
|
|
|
if self._defaults and key in self._defaults._options:
|
|
|
|
return self._defaults._options[key].value
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __setitem__(self, key: str, value: object) -> None:
|
|
|
|
if self._optionxform:
|
|
|
|
xkey = self._optionxform(key)
|
|
|
|
else:
|
|
|
|
xkey = key
|
2013-05-23 10:19:43 +02:00
|
|
|
if xkey in self._compat_skip_empty_lines:
|
|
|
|
self._compat_skip_empty_lines.remove(xkey)
|
|
|
|
if xkey not in self._options:
|
|
|
|
# create a dummy object - value may have multiple lines
|
2024-11-29 22:54:39 +01:00
|
|
|
obj = LineContainer(OptionLine(key, ""))
|
2013-05-23 10:19:43 +02:00
|
|
|
self._lines[-1].add(obj)
|
|
|
|
self._options[xkey] = obj
|
|
|
|
# the set_value() function in LineContainer
|
|
|
|
# automatically handles multi-line values
|
|
|
|
self._options[xkey].value = value
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __delitem__(self, key: str) -> None:
|
|
|
|
if self._optionxform:
|
|
|
|
key = self._optionxform(key)
|
2013-05-23 10:19:43 +02:00
|
|
|
if key in self._compat_skip_empty_lines:
|
|
|
|
self._compat_skip_empty_lines.remove(key)
|
|
|
|
for l in self._lines:
|
|
|
|
remaining = []
|
|
|
|
for o in l.contents:
|
|
|
|
if isinstance(o, LineContainer):
|
|
|
|
n = o.name
|
2024-11-29 22:54:39 +01:00
|
|
|
if self._optionxform:
|
|
|
|
n = self._optionxform(n)
|
|
|
|
if key != n:
|
|
|
|
remaining.append(o)
|
2013-05-23 10:19:43 +02:00
|
|
|
else:
|
|
|
|
remaining.append(o)
|
|
|
|
l.contents = remaining
|
|
|
|
del self._options[key]
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __iter__(self) -> Iterator[str]:
|
2013-05-23 10:19:43 +02:00
|
|
|
d = set()
|
|
|
|
for l in self._lines:
|
|
|
|
for x in l.contents:
|
|
|
|
if isinstance(x, LineContainer):
|
|
|
|
if self._optionxform:
|
|
|
|
ans = self._optionxform(x.name)
|
|
|
|
else:
|
|
|
|
ans = x.name
|
|
|
|
if ans not in d:
|
|
|
|
yield ans
|
|
|
|
d.add(ans)
|
|
|
|
if self._defaults:
|
|
|
|
for x in self._defaults:
|
|
|
|
if x not in d:
|
|
|
|
yield x
|
|
|
|
d.add(x)
|
|
|
|
|
|
|
|
def _new_namespace(self, name):
|
2024-11-29 22:54:39 +01:00
|
|
|
raise Exception("No sub-sections allowed", name)
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def make_comment(line: str) -> CommentLine:
|
|
|
|
return CommentLine(line.rstrip("\n"))
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def readline_iterator(f: TextIO) -> Iterator[str]:
|
|
|
|
"""Iterate over a file by only using the file object's readline method."""
|
|
|
|
have_newline: bool = False
|
2013-05-23 10:19:43 +02:00
|
|
|
while True:
|
2024-11-29 22:54:39 +01:00
|
|
|
line: Optional[str] = f.readline()
|
2013-05-23 10:19:43 +02:00
|
|
|
|
|
|
|
if not line:
|
|
|
|
if have_newline:
|
|
|
|
yield ""
|
|
|
|
return
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
if line.endswith("\n"):
|
2013-05-23 10:19:43 +02:00
|
|
|
have_newline = True
|
|
|
|
else:
|
|
|
|
have_newline = False
|
|
|
|
|
|
|
|
yield line
|
|
|
|
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def lower(x: str) -> str:
|
2013-05-23 10:19:43 +02:00
|
|
|
return x.lower()
|
|
|
|
|
|
|
|
|
|
|
|
class INIConfig(config.ConfigNamespace):
|
2024-11-29 22:54:39 +01:00
|
|
|
_data: LineContainer = None
|
|
|
|
_sections: Dict[str, object] = None
|
|
|
|
_defaults: INISection = None
|
|
|
|
_optionxformvalue: Callable = None
|
|
|
|
_optionxformsource: Optional["INIConfig"] = None
|
|
|
|
_sectionxformvalue: Optional["INIConfig"] = None
|
|
|
|
_sectionxformsource: Optional["INIConfig"] = None
|
2013-05-23 10:19:43 +02:00
|
|
|
_parse_exc = None
|
|
|
|
_bom = False
|
2022-04-25 10:02:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
fp: TextIO = None,
|
|
|
|
defaults: Dict[str, object] = None,
|
|
|
|
parse_exc: bool = True,
|
|
|
|
optionxformvalue: Callable = lower,
|
|
|
|
optionxformsource: Optional[Union["INIConfig", "RawConfigParser"]] = None,
|
|
|
|
sectionxformvalue: Optional["INIConfig"] = None,
|
|
|
|
sectionxformsource: Optional["INIConfig"] = None,
|
|
|
|
) -> None:
|
2013-05-23 10:19:43 +02:00
|
|
|
self._data = LineContainer()
|
|
|
|
self._parse_exc = parse_exc
|
|
|
|
self._optionxformvalue = optionxformvalue
|
|
|
|
self._optionxformsource = optionxformsource
|
|
|
|
self._sectionxformvalue = sectionxformvalue
|
|
|
|
self._sectionxformsource = sectionxformsource
|
2024-11-29 22:54:39 +01:00
|
|
|
self._sections: Dict[str, INISection] = {}
|
|
|
|
if defaults is None:
|
|
|
|
defaults = {}
|
2013-05-23 10:19:43 +02:00
|
|
|
self._defaults = INISection(LineContainer(), optionxformsource=self)
|
2022-04-25 10:02:43 +02:00
|
|
|
for name, value in defaults.items():
|
2013-05-23 10:19:43 +02:00
|
|
|
self._defaults[name] = value
|
|
|
|
if fp is not None:
|
|
|
|
self._readfp(fp)
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
_optionxform = _make_xform_property("_optionxform", "optionxform")
|
|
|
|
_sectionxform = _make_xform_property("_sectionxform", "optionxform")
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def _getitem(self, key: str) -> INISection:
|
2013-05-23 10:19:43 +02:00
|
|
|
if key == DEFAULTSECT:
|
|
|
|
return self._defaults
|
2024-11-29 22:54:39 +01:00
|
|
|
if self._sectionxform:
|
|
|
|
key = self._sectionxform(key)
|
2013-05-23 10:19:43 +02:00
|
|
|
return self._sections[key]
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __setitem__(self, key: str, value: object):
|
|
|
|
raise Exception("Values must be inside sections", key, value)
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __delitem__(self, key: str) -> None:
|
|
|
|
if self._sectionxform:
|
|
|
|
key = self._sectionxform(key)
|
2013-05-23 10:19:43 +02:00
|
|
|
for line in self._sections[key]._lines:
|
|
|
|
self._data.contents.remove(line)
|
|
|
|
del self._sections[key]
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __iter__(self) -> Iterator[str]:
|
2013-05-23 10:19:43 +02:00
|
|
|
d = set()
|
|
|
|
d.add(DEFAULTSECT)
|
|
|
|
for x in self._data.contents:
|
|
|
|
if isinstance(x, LineContainer):
|
|
|
|
if x.name not in d:
|
|
|
|
yield x.name
|
|
|
|
d.add(x.name)
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def _new_namespace(self, name: str) -> INISection:
|
2013-05-23 10:19:43 +02:00
|
|
|
if self._data.contents:
|
|
|
|
self._data.add(EmptyLine())
|
|
|
|
obj = LineContainer(SectionLine(name))
|
|
|
|
self._data.add(obj)
|
2024-11-29 22:54:39 +01:00
|
|
|
if self._sectionxform:
|
|
|
|
name = self._sectionxform(name)
|
2013-05-23 10:19:43 +02:00
|
|
|
if name in self._sections:
|
|
|
|
ns = self._sections[name]
|
|
|
|
ns._lines.append(obj)
|
|
|
|
else:
|
2024-11-29 22:54:39 +01:00
|
|
|
ns = INISection(obj, defaults=self._defaults, optionxformsource=self)
|
2013-05-23 10:19:43 +02:00
|
|
|
self._sections[name] = ns
|
|
|
|
return ns
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def __str__(self) -> str:
|
2013-05-23 10:19:43 +02:00
|
|
|
if self._bom:
|
2024-11-29 22:54:39 +01:00
|
|
|
fmt = "\ufeff%s"
|
2013-05-23 10:19:43 +02:00
|
|
|
else:
|
2024-11-29 22:54:39 +01:00
|
|
|
fmt = "%s"
|
2013-05-23 10:19:43 +02:00
|
|
|
return fmt % self._data.__str__()
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
_line_types = [EmptyLine, CommentLine, SectionLine, OptionLine, ContinuationLine]
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def _parse(self, line: str) -> Any:
|
2013-05-23 10:19:43 +02:00
|
|
|
for linetype in self._line_types:
|
|
|
|
lineobj = linetype.parse(line)
|
|
|
|
if lineobj:
|
|
|
|
return lineobj
|
|
|
|
else:
|
|
|
|
# can't parse line
|
|
|
|
return None
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
def _readfp(self, fp: TextIO) -> None:
|
2013-05-23 10:19:43 +02:00
|
|
|
cur_section = None
|
|
|
|
cur_option = None
|
|
|
|
cur_section_name = None
|
|
|
|
cur_option_name = None
|
|
|
|
pending_lines = []
|
|
|
|
pending_empty_lines = False
|
|
|
|
try:
|
|
|
|
fname = fp.name
|
|
|
|
except AttributeError:
|
2024-11-29 22:54:39 +01:00
|
|
|
fname = "<???>"
|
2022-04-25 10:02:43 +02:00
|
|
|
line_count = 0
|
2013-05-23 10:19:43 +02:00
|
|
|
exc = None
|
|
|
|
line = None
|
|
|
|
|
|
|
|
for line in readline_iterator(fp):
|
|
|
|
# Check for BOM on first line
|
2024-11-29 22:54:39 +01:00
|
|
|
if line_count == 0 and isinstance(line, str):
|
|
|
|
if line[0] == "\ufeff":
|
2013-05-23 10:19:43 +02:00
|
|
|
line = line[1:]
|
|
|
|
self._bom = True
|
|
|
|
|
2022-04-25 10:02:43 +02:00
|
|
|
line_obj = self._parse(line)
|
|
|
|
line_count += 1
|
|
|
|
if not cur_section and not isinstance(line_obj, (CommentLine, EmptyLine, SectionLine)):
|
2013-05-23 10:19:43 +02:00
|
|
|
if self._parse_exc:
|
2022-04-25 10:02:43 +02:00
|
|
|
raise MissingSectionHeaderError(fname, line_count, line)
|
2013-05-23 10:19:43 +02:00
|
|
|
else:
|
2022-04-25 10:02:43 +02:00
|
|
|
line_obj = make_comment(line)
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2022-04-25 10:02:43 +02:00
|
|
|
if line_obj is None:
|
2013-05-23 10:19:43 +02:00
|
|
|
if self._parse_exc:
|
2022-04-25 10:02:43 +02:00
|
|
|
if exc is None:
|
|
|
|
exc = ParsingError(fname)
|
|
|
|
exc.append(line_count, line)
|
|
|
|
line_obj = make_comment(line)
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2022-04-25 10:02:43 +02:00
|
|
|
if isinstance(line_obj, ContinuationLine):
|
2013-05-23 10:19:43 +02:00
|
|
|
if cur_option:
|
|
|
|
if pending_lines:
|
|
|
|
cur_option.extend(pending_lines)
|
|
|
|
pending_lines = []
|
|
|
|
if pending_empty_lines:
|
2024-11-29 22:54:39 +01:00
|
|
|
optobj._compat_skip_empty_lines.add(cur_option_name) # noqa : F821
|
2013-05-23 10:19:43 +02:00
|
|
|
pending_empty_lines = False
|
2022-04-25 10:02:43 +02:00
|
|
|
cur_option.add(line_obj)
|
2013-05-23 10:19:43 +02:00
|
|
|
else:
|
|
|
|
# illegal continuation line - convert to comment
|
|
|
|
if self._parse_exc:
|
2022-04-25 10:02:43 +02:00
|
|
|
if exc is None:
|
|
|
|
exc = ParsingError(fname)
|
|
|
|
exc.append(line_count, line)
|
|
|
|
line_obj = make_comment(line)
|
2013-05-23 10:19:43 +02:00
|
|
|
|
2022-04-25 10:02:43 +02:00
|
|
|
if isinstance(line_obj, OptionLine):
|
2013-05-23 10:19:43 +02:00
|
|
|
if pending_lines:
|
|
|
|
cur_section.extend(pending_lines)
|
|
|
|
pending_lines = []
|
|
|
|
pending_empty_lines = False
|
2022-04-25 10:02:43 +02:00
|
|
|
cur_option = LineContainer(line_obj)
|
2013-05-23 10:19:43 +02:00
|
|
|
cur_section.add(cur_option)
|
|
|
|
if self._optionxform:
|
|
|
|
cur_option_name = self._optionxform(cur_option.name)
|
|
|
|
else:
|
|
|
|
cur_option_name = cur_option.name
|
|
|
|
if cur_section_name == DEFAULTSECT:
|
|
|
|
optobj = self._defaults
|
|
|
|
else:
|
|
|
|
optobj = self._sections[cur_section_name]
|
|
|
|
optobj._options[cur_option_name] = cur_option
|
|
|
|
|
2022-04-25 10:02:43 +02:00
|
|
|
if isinstance(line_obj, SectionLine):
|
2013-05-23 10:19:43 +02:00
|
|
|
self._data.extend(pending_lines)
|
|
|
|
pending_lines = []
|
|
|
|
pending_empty_lines = False
|
2022-04-25 10:02:43 +02:00
|
|
|
cur_section = LineContainer(line_obj)
|
2013-05-23 10:19:43 +02:00
|
|
|
self._data.add(cur_section)
|
|
|
|
cur_option = None
|
|
|
|
cur_option_name = None
|
|
|
|
if cur_section.name == DEFAULTSECT:
|
|
|
|
self._defaults._lines.append(cur_section)
|
|
|
|
cur_section_name = DEFAULTSECT
|
|
|
|
else:
|
|
|
|
if self._sectionxform:
|
|
|
|
cur_section_name = self._sectionxform(cur_section.name)
|
|
|
|
else:
|
|
|
|
cur_section_name = cur_section.name
|
|
|
|
if cur_section_name not in self._sections:
|
2024-11-29 22:54:39 +01:00
|
|
|
self._sections[cur_section_name] = INISection(cur_section, defaults=self._defaults, optionxformsource=self)
|
2013-05-23 10:19:43 +02:00
|
|
|
else:
|
|
|
|
self._sections[cur_section_name]._lines.append(cur_section)
|
|
|
|
|
2022-04-25 10:02:43 +02:00
|
|
|
if isinstance(line_obj, (CommentLine, EmptyLine)):
|
|
|
|
pending_lines.append(line_obj)
|
|
|
|
if isinstance(line_obj, EmptyLine):
|
2013-05-23 10:19:43 +02:00
|
|
|
pending_empty_lines = True
|
|
|
|
|
2024-11-29 22:54:39 +01:00
|
|
|
if line_obj:
|
|
|
|
line_obj.line_number = line_count
|
|
|
|
|
2013-05-23 10:19:43 +02:00
|
|
|
self._data.extend(pending_lines)
|
2024-11-29 22:54:39 +01:00
|
|
|
if line and line[-1] == "\n":
|
2013-05-23 10:19:43 +02:00
|
|
|
self._data.add(EmptyLine())
|
|
|
|
|
|
|
|
if exc:
|
|
|
|
raise exc
|