| 1 | """Dependency-free config loading (ADR-0001).
|
| 2 |
|
| 3 | One TOML-subset parser for the shapes this repo writes: dotted [section.name]
|
| 4 | tables, string/boolean/string-list values. If a config outgrows this subset,
|
| 5 | simplify the config, not the parser.
|
| 6 | """
|
| 7 |
|
| 8 | from __future__ import annotations
|
| 9 |
|
| 10 | from pathlib import Path
|
| 11 | from typing import Any
|
| 12 |
|
| 13 | try:
|
| 14 | import tomllib
|
| 15 | except ModuleNotFoundError: # Python 3.9/3.10 on stock macOS.
|
| 16 | tomllib = None # type: ignore[assignment]
|
| 17 |
|
| 18 |
|
| 19 | def load_toml(path: Path) -> dict[str, Any]:
|
| 20 | text = path.read_text()
|
| 21 | if tomllib is not None:
|
| 22 | return tomllib.loads(text)
|
| 23 |
|
| 24 | root: dict[str, Any] = {}
|
| 25 | current: dict[str, Any] | None = None
|
| 26 | for raw_line in text.splitlines():
|
| 27 | line = _strip_toml_comment(raw_line).strip()
|
| 28 | if not line:
|
| 29 | continue
|
| 30 | if line.startswith("[") and line.endswith("]"):
|
| 31 | parts = line[1:-1].split(".")
|
| 32 | current = root
|
| 33 | for part in parts:
|
| 34 | current = current.setdefault(part, {})
|
| 35 | continue
|
| 36 | if current is None or "=" not in line:
|
| 37 | continue
|
| 38 | key, value = [part.strip() for part in line.split("=", 1)]
|
| 39 | if value in {"true", "false"}:
|
| 40 | current[key] = value == "true"
|
| 41 | elif value.startswith('"') and value.endswith('"'):
|
| 42 | current[key] = value[1:-1]
|
| 43 | elif value.startswith("'") and value.endswith("'"):
|
| 44 | current[key] = value[1:-1]
|
| 45 | elif value.startswith("[") and value.endswith("]"):
|
| 46 | items = [item.strip().strip('"').strip("'") for item in value[1:-1].split(",") if item.strip()]
|
| 47 | current[key] = items
|
| 48 | else:
|
| 49 | current[key] = value
|
| 50 | return root
|
| 51 |
|
| 52 |
|
| 53 | def _strip_toml_comment(raw_line: str) -> str:
|
| 54 | quote: str | None = None
|
| 55 | escaped = False
|
| 56 | for index, char in enumerate(raw_line):
|
| 57 | if escaped:
|
| 58 | escaped = False
|
| 59 | continue
|
| 60 | if char == "\\":
|
| 61 | escaped = quote == '"'
|
| 62 | continue
|
| 63 | if quote is not None:
|
| 64 | if char == quote:
|
| 65 | quote = None
|
| 66 | continue
|
| 67 | if char in {"'", '"'}:
|
| 68 | quote = char
|
| 69 | continue
|
| 70 | if char == "#":
|
| 71 | return raw_line[:index]
|
| 72 | return raw_line
|
| 73 |
|
| 74 |
|
| 75 | def load_subject_config(path: Path) -> dict[str, Any]:
|
| 76 | data = load_toml(path)
|
| 77 | if "subjects" not in data:
|
| 78 | return {"subjects": {}}
|
| 79 | return {"subjects": data["subjects"]}
|