"""INI format converter.""" from configparser import ConfigParser, RawConfigParser from pathlib import Path from typing import Any, Dict, Union from config_converter.converters.base import BaseConverter, ConversionError def flatten_dict(data: Dict[str, Any], parent_key: str = "", sep: str = ".") -> Dict[str, Any]: """Flatten nested dictionary for INI output.""" items: Dict[str, Any] = {} for key, value in data.items(): new_key = f"{parent_key}{sep}{key}" if parent_key else key if isinstance(value, dict): items.update(flatten_dict(value, new_key, sep)) else: items[new_key] = value return items def unflatten_dict(data: Dict[str, Any], sep: str = ".") -> Dict[str, Any]: """Unflatten flat dictionary for INI input.""" result: Dict[str, Any] = {} for key, value in data.items(): parts = key.split(sep) target = result for part in parts[:-1]: if part not in target: target[part] = {} target = target[part] target[parts[-1]] = value return result class IniConverter(BaseConverter): """Converter for INI configuration files.""" FORMAT_NAME = "ini" FILE_EXTENSIONS = ["ini", "cfg"] def read(self, source: Union[str, Path]) -> Dict[str, Any]: """Read and parse an INI configuration file.""" try: config = RawConfigParser(allow_no_value=True) config.read(source, encoding="utf-8") result: Dict[str, Any] = {} for section in config.sections(): section_dict: Dict[str, Any] = {} for key, value in config.items(section): if value: try: section_dict[key] = int(value) except ValueError: try: section_dict[key] = float(value) except ValueError: if value.lower() in ("true", "yes", "on"): section_dict[key] = True elif value.lower() in ("false", "no", "off"): section_dict[key] = False else: section_dict[key] = value else: section_dict[key] = None result[section] = section_dict return result except Exception as e: raise ConversionError(f"Invalid INI in {source}: {e}") from e def write(self, data: Dict[str, Any], target: Union[str, Path]) -> None: """Write configuration data to an INI file.""" try: config = ConfigParser() for section, section_data in data.items(): if isinstance(section_data, dict): config[section] = {} for key, value in section_data.items(): config[section][key] = str(value) if value is not None else "" else: config["DEFAULT"] = {} config["DEFAULT"]["settings"] = str(section_data) with open(target, "w", encoding="utf-8") as f: config.write(f) except (OSError, TypeError) as e: raise ConversionError(f"Failed to write INI to {target}: {e}") from e def parse(self, content: str) -> Dict[str, Any]: """Parse INI content from a string.""" try: config = RawConfigParser(allow_no_value=True) config.read_string(content) result: Dict[str, Any] = {} for section in config.sections(): section_dict: Dict[str, Any] = {} for key, value in config.items(section): if value: try: section_dict[key] = int(value) except ValueError: try: section_dict[key] = float(value) except ValueError: if value.lower() in ("true", "yes", "on"): section_dict[key] = True elif value.lower() in ("false", "no", "off"): section_dict[key] = False else: section_dict[key] = value else: section_dict[key] = None result[section] = section_dict return result except Exception as e: raise ConversionError(f"Invalid INI content: {e}") from e def format(self, data: Dict[str, Any]) -> str: """Format configuration data to an INI string.""" try: config = ConfigParser() for section, section_data in data.items(): if isinstance(section_data, dict): config[section] = {} for key, value in section_data.items(): config[section][key] = str(value) if value is not None else "" from io import StringIO output = StringIO() config.write(output) return output.getvalue() except (TypeError, ValueError) as e: raise ConversionError(f"Failed to format data as INI: {e}") from e