Files

94 lines
2.9 KiB
Python
Raw Permalink Normal View History

"""Parser for the INI-ish text Boxsim stores inside its containers.
Shape: `#Section` headers, `key=<TAB>value` pairs, CRLF line ends, German
decimal commas, and arrays as `;`-separated values. Section names are
case sensitive: a project contains both `#AktFilter1` and `#Aktfilter1`.
"""
from __future__ import annotations
from collections.abc import Iterator
class Section(dict):
"""One `#Name` block. A plain dict of str -> str, plus the section name."""
def __init__(self, name: str):
super().__init__()
self.name = name
def __repr__(self) -> str: # pragma: no cover - debugging aid
return f"Section({self.name!r}, {len(self)} keys)"
class IniDocument:
"""Ordered collection of sections, tolerant of repeated section names."""
def __init__(self) -> None:
self.sections: list[Section] = []
def __iter__(self) -> Iterator[Section]:
return iter(self.sections)
def __contains__(self, name: str) -> bool:
return any(s.name == name for s in self.sections)
def first(self, name: str) -> Section | None:
for s in self.sections:
if s.name == name:
return s
return None
def require(self, name: str) -> Section:
s = self.first(name)
if s is None:
raise KeyError(f"missing section #{name}")
return s
def all(self, name: str) -> list[Section]:
return [s for s in self.sections if s.name == name]
def merged(self, *names: str) -> Section:
"""Merge several sections into one, for split blocks like AktFilter."""
out = Section(names[0])
for name in names:
for s in self.all(name):
out.update(s)
return out
def parse(text: str) -> IniDocument:
doc = IniDocument()
current: Section | None = None
for raw_line in text.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
line = raw_line.rstrip("\n")
if not line.strip():
continue
if line.startswith("#"):
current = Section(line[1:].strip())
doc.sections.append(current)
continue
if current is None:
# Content before any header: keep it in an unnamed section rather
# than dropping it silently.
current = Section("")
doc.sections.append(current)
key, sep, value = line.partition("=")
if not sep:
continue
current[key.strip()] = value.strip()
return doc
def number(value: str) -> float:
"""Parse a Boxsim number. German decimal comma, exponents like `1,E-5`."""
return float(value.strip().replace(",", "."))
def integer(value: str) -> int:
return int(float(value.strip().replace(",", ".")))
def number_list(value: str) -> list[float]:
"""Parse a `;`-separated array, tolerating trailing separators."""
return [number(p) for p in value.split(";") if p.strip()]