# -*- coding: utf-8 -*- """ parameter.py - Zahlenwerte der Bibliotheken aus cfg/parameter.ini. Toleranzen, Schwellen, Massstaebe und Zeichnungsgroessen stehen nicht im Code, sondern in einer INI-Datei: ein Abschnitt je Modul, der Schluessel ist der Name der Konstanten in Kleinbuchstaben ([kreisel_junctions] contact_tolerance -> kreisel_junctions.CONTACT_TOLERANCE). Gelesen wird zuerst die Grunddatei des Repositorys (/cfg/ parameter.ini, versioniert, muss vollstaendig sein). Liegt in %SKEL_CFG% eine weitere parameter.ini, ueberschreibt sie einzelne Werte - so kann eine Anlage abweichende Toleranzen bekommen, ohne die Grunddatei zu aendern. Fehlt die Grunddatei oder ein Schluessel, bricht der Import mit ParameterError ab: einen stillen Ersatzwert im Code gibt es bewusst nicht, sonst stuende die Zahl doch wieder im Code. Unterste Schicht: importiert nichts aus lib/, damit jedes Modul es nutzen kann. """ from __future__ import annotations import configparser import os from functools import lru_cache from pathlib import Path PARAMETER_FILE = "parameter.ini" __all__ = ["PARAMETER_FILE", "ParameterError", "files", "get_float", "get_int"] class ParameterError(RuntimeError): """Grunddatei fehlt oder ein Wert fehlt bzw. ist keine Zahl.""" def _base_file() -> Path: """Grunddatei im Repository: /cfg/parameter.ini.""" return Path(__file__).resolve().parent.parent / "cfg" / PARAMETER_FILE def files() -> list[Path]: """Gelesene Dateien in Lesereihenfolge (Grunddatei, ggf. %SKEL_CFG%).""" result = [_base_file()] cfg = os.environ.get("SKEL_CFG") if cfg: override = Path(cfg) / PARAMETER_FILE if override.is_file() and override.resolve() != result[0].resolve(): result.append(override) return result @lru_cache(maxsize=1) def _parser() -> configparser.ConfigParser: """Alle Parameterdateien einlesen (einmal je Prozess).""" base = _base_file() if not base.is_file(): raise ParameterError(f"Parameterdatei fehlt: {base}") parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#")) for path in files(): with path.open(encoding="utf-8") as handle: parser.read_file(handle, source=str(path)) return parser def _raw(section: str, key: str) -> str: """Rohwert; ParameterError, wenn Abschnitt oder Schluessel fehlt.""" parser = _parser() if not parser.has_option(section, key): raise ParameterError( f"[{section}] {key} fehlt in {', '.join(str(p) for p in files())}") return parser.get(section, key) def get_float(section: str, key: str) -> float: """Zahl aus [section] key. Parameter: section: Abschnitt, in der Regel der Modulname (z. B. "tro_flow"). key: Schluessel, der Konstantenname in Kleinbuchstaben. Rueckgabe: Der Wert als float. """ raw = _raw(section, key) try: return float(raw) except ValueError: raise ParameterError(f"[{section}] {key} = {raw!r} ist keine Zahl") from None def get_int(section: str, key: str) -> int: """Ganze Zahl aus [section] key (Anzahlen, Schrittweiten).""" raw = _raw(section, key) try: return int(raw) except ValueError: raise ParameterError(f"[{section}] {key} = {raw!r} ist keine ganze Zahl") from None