| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- # -*- coding: utf-8 -*-
- """
- CLI-Argument-Merging für Konfigurationen.
- Ermöglicht das Überschreiben von Konfigurationswerten
- durch Kommandozeilenargumente.
- """
- from __future__ import annotations
- import argparse
- import json
- import re
- from dataclasses import dataclass, field
- from typing import Any, Callable, Sequence, Type
- @dataclass
- class CLIOption:
- """
- Definition einer CLI-Option für Konfigurationsoverride.
- """
- name: str
- """Langname der Option (z.B. 'host')."""
- short: str | None = None
- """Kurzname (z.B. 'h')."""
- config_path: str = ""
- """Pfad in der Konfiguration (z.B. 'network.host')."""
- help: str = ""
- """Hilfetext."""
- arg_type: Type = str
- """Argumenttyp (str, int, float, bool)."""
- default: Any = None
- """Standardwert."""
- choices: Sequence[Any] | None = None
- """Erlaubte Werte."""
- required: bool = False
- """Ob erforderlich."""
- action: str | None = None
- """argparse action (z.B. 'store_true')."""
- nargs: str | int | None = None
- """argparse nargs."""
- class CLIConfigMerger:
- """
- Merged Konfiguration mit CLI-Argumenten.
- Ermöglicht das Überschreiben von Konfigurationswerten
- durch Kommandozeilenargumente mit konsistenter Syntax.
- Example:
- merger = CLIConfigMerger()
- merger.option("host", config_path="network.host")
- merger.option("port", arg_type=int, config_path="network.port")
- merger.option("debug", action="store_true")
- # Parsen und Mergen
- config = {"network": {"host": "localhost", "port": 80}}
- merged = merger.merge(config, sys.argv[1:])
- """
- def __init__(
- self,
- prefix: str = "--",
- config_prefix: str = "config.",
- parser: argparse.ArgumentParser | None = None,
- ) -> None:
- """
- Initialisiert den CLI-Config-Merger.
- Args:
- prefix: Präfix für Langoptionen (default: '--').
- config_prefix: Präfix für direkte Konfigurationspfade.
- parser: Optionaler existierender ArgumentParser.
- """
- self._prefix = prefix
- self._config_prefix = config_prefix
- self._options: list[CLIOption] = []
- self._parser = parser or argparse.ArgumentParser()
- self._generic_pattern = re.compile(
- rf"^{re.escape(config_prefix)}([a-zA-Z0-9_.]+)=(.*)$"
- )
- @property
- def parser(self) -> argparse.ArgumentParser:
- """Gibt den ArgumentParser zurück."""
- return self._parser
- def option(
- self,
- name: str,
- short: str | None = None,
- config_path: str | None = None,
- help: str = "",
- arg_type: Type = str,
- default: Any = None,
- choices: Sequence[Any] | None = None,
- required: bool = False,
- action: str | None = None,
- nargs: str | int | None = None,
- ) -> "CLIConfigMerger":
- """
- Fügt eine CLI-Option hinzu.
- Args:
- name: Optionsname.
- short: Kurzname.
- config_path: Konfigurationspfad (Default: name).
- help: Hilfetext.
- arg_type: Argumenttyp.
- default: Standardwert.
- choices: Erlaubte Werte.
- required: Ob erforderlich.
- action: argparse action.
- nargs: argparse nargs.
- Returns:
- Self für Method-Chaining.
- """
- opt = CLIOption(
- name=name,
- short=short,
- config_path=config_path or name,
- help=help,
- arg_type=arg_type,
- default=default,
- choices=choices,
- required=required,
- action=action,
- nargs=nargs,
- )
- self._options.append(opt)
- self._add_to_parser(opt)
- return self
- def _add_to_parser(self, opt: CLIOption) -> None:
- """Fügt eine Option zum ArgumentParser hinzu."""
- args = []
- if opt.short:
- args.append(f"-{opt.short}")
- args.append(f"--{opt.name}")
- kwargs: dict[str, Any] = {"help": opt.help}
- if opt.action:
- kwargs["action"] = opt.action
- else:
- kwargs["type"] = opt.arg_type
- kwargs["default"] = None # Kein Default, sonst wird immer überschrieben
- if opt.choices:
- kwargs["choices"] = opt.choices
- if opt.required:
- kwargs["required"] = opt.required
- if opt.nargs:
- kwargs["nargs"] = opt.nargs
- self._parser.add_argument(*args, **kwargs)
- def _set_nested(
- self,
- data: dict,
- path: str,
- value: Any,
- ) -> None:
- """Setzt einen verschachtelten Wert."""
- keys = path.split(".")
- current = data
- for key in keys[:-1]:
- if key not in current:
- current[key] = {}
- current = current[key]
- current[keys[-1]] = value
- def _get_nested(
- self,
- data: dict,
- path: str,
- default: Any = None,
- ) -> Any:
- """Holt einen verschachtelten Wert."""
- keys = path.split(".")
- current = data
- for key in keys:
- if isinstance(current, dict) and key in current:
- current = current[key]
- else:
- return default
- return current
- def _parse_generic_args(
- self,
- args: Sequence[str],
- ) -> tuple[list[str], dict[str, Any]]:
- """
- Parst generische config.path=value Argumente.
- Returns:
- Tupel (verbleibende Args, Konfigurationsoverrides).
- """
- remaining: list[str] = []
- overrides: dict[str, Any] = {}
- for arg in args:
- match = self._generic_pattern.match(arg)
- if match:
- path = match.group(1)
- value_str = match.group(2)
- # Wert parsen
- value = self._parse_value(value_str)
- overrides[path] = value
- else:
- remaining.append(arg)
- return remaining, overrides
- def _parse_value(self, value_str: str) -> Any:
- """Parst einen Wert-String in den entsprechenden Typ."""
- # Boolean
- if value_str.lower() in ("true", "yes", "1"):
- return True
- if value_str.lower() in ("false", "no", "0"):
- return False
- # Null
- if value_str.lower() in ("null", "none"):
- return None
- # Zahl
- try:
- if "." in value_str:
- return float(value_str)
- return int(value_str)
- except ValueError:
- pass
- # JSON für komplexe Werte
- if value_str.startswith(("{", "[")):
- try:
- return json.loads(value_str)
- except json.JSONDecodeError:
- pass
- # String
- return value_str
- def parse(
- self,
- args: Sequence[str] | None = None,
- ) -> argparse.Namespace:
- """
- Parst CLI-Argumente.
- Args:
- args: Argumente (Default: sys.argv[1:]).
- Returns:
- argparse.Namespace mit geparsten Werten.
- """
- # Generische Argumente vorverarbeiten
- if args is not None:
- remaining, _ = self._parse_generic_args(args)
- return self._parser.parse_args(remaining)
- return self._parser.parse_args()
- def merge(
- self,
- config: dict[str, Any],
- args: Sequence[str] | None = None,
- parsed: argparse.Namespace | None = None,
- ) -> dict[str, Any]:
- """
- Merged Konfiguration mit CLI-Argumenten.
- CLI-Argumente überschreiben Konfigurationswerte.
- Args:
- config: Basis-Konfiguration.
- args: CLI-Argumente (oder parsed Namespace).
- parsed: Vorab geparste Argumente.
- Returns:
- Gemergte Konfiguration.
- """
- result = dict(config)
- # Generische Argumente verarbeiten
- generic_overrides: dict[str, Any] = {}
- remaining_args = args
- if args is not None:
- remaining_args, generic_overrides = self._parse_generic_args(args)
- # Definierte Optionen parsen
- if parsed is None:
- if remaining_args is not None:
- parsed = self._parser.parse_args(remaining_args)
- else:
- parsed = self._parser.parse_args()
- # Definierte Optionen anwenden
- for opt in self._options:
- value = getattr(parsed, opt.name.replace("-", "_"), None)
- if value is not None:
- self._set_nested(result, opt.config_path, value)
- # Generische Overrides anwenden
- for path, value in generic_overrides.items():
- self._set_nested(result, path, value)
- return result
- def add_common_options(self) -> "CLIConfigMerger":
- """
- Fügt häufig verwendete Optionen hinzu.
- Returns:
- Self für Method-Chaining.
- """
- self.option(
- "config",
- short="c",
- config_path="_config_file",
- help="Pfad zur Konfigurationsdatei",
- )
- self.option(
- "debug",
- short="d",
- config_path="debug",
- action="store_true",
- help="Debug-Modus aktivieren",
- )
- self.option(
- "verbose",
- short="v",
- config_path="verbose",
- action="store_true",
- help="Ausführliche Ausgabe",
- )
- return self
- def merge_cli_config(
- config: dict[str, Any],
- args: Sequence[str] | None = None,
- options: list[CLIOption] | None = None,
- ) -> dict[str, Any]:
- """
- Convenience-Funktion zum Mergen von Config und CLI.
- Args:
- config: Basis-Konfiguration.
- args: CLI-Argumente.
- options: CLI-Optionen.
- Returns:
- Gemergte Konfiguration.
- """
- merger = CLIConfigMerger()
- if options:
- for opt in options:
- merger._options.append(opt)
- merger._add_to_parser(opt)
- return merger.merge(config, args)
|