import configparser from pathlib import Path import click from InquirerPy import inquirer from pydantic import ValidationError from ...core.config import AppConfig from ...core.constants import USER_CONFIG_PATH from ...core.exceptions import ConfigError from .generate import generate_config_ini_from_app_model from .interactive_editor import InteractiveConfigEditor class ConfigLoader: """ Handles loading the application configuration from an .ini file. It ensures a default configuration exists, reads the .ini file, and uses Pydantic to parse and validate the data into a type-safe AppConfig object. """ def __init__(self, config_path: Path = USER_CONFIG_PATH): """ Initializes the loader with the path to the configuration file. Args: config_path: The path to the user's config.ini file. """ self.config_path = config_path self.parser = configparser.ConfigParser( interpolation=None, # Allow boolean values without a corresponding value (e.g., `enabled` vs `enabled = true`) allow_no_value=True, # Behave like a dictionary, preserving case sensitivity of keys dict_type=dict, ) def _handle_first_run(self) -> AppConfig: """Handles the configuration process when no config file is found.""" click.echo( "[bold yellow]Welcome to FastAnime![/bold yellow] No configuration file found." ) choice = inquirer.select( message="How would you like to proceed?", choices=[ "Use default settings (Recommended for new users)", "Configure settings interactively", ], default="Use default settings (Recommended for new users)", ).execute() if "interactively" in choice: editor = InteractiveConfigEditor(AppConfig()) app_config = editor.run() else: app_config = AppConfig() config_ini_content = generate_config_ini_from_app_model(app_config) try: self.config_path.parent.mkdir(parents=True, exist_ok=True) self.config_path.write_text(config_ini_content, encoding="utf-8") click.echo( f"Configuration file created at: [green]{self.config_path}[/green]" ) except Exception as e: raise ConfigError( f"Could not create configuration file at {self.config_path!s}. Please check permissions. Error: {e}", ) return app_config def load(self) -> AppConfig: """ Loads the configuration and returns a populated, validated AppConfig object. Returns: An instance of AppConfig with values from the user's .ini file. Raises: click.ClickException: If the configuration file contains validation errors. """ if not self.config_path.exists(): return self._handle_first_run() try: self.parser.read(self.config_path, encoding="utf-8") except configparser.Error as e: raise ConfigError( f"Error parsing configuration file '{self.config_path}':\n{e}" ) # Convert the configparser object into a nested dictionary that mirrors # the structure of our AppConfig Pydantic model. config_dict = { section: dict(self.parser.items(section)) for section in self.parser.sections() } try: app_config = AppConfig.model_validate(config_dict) return app_config except ValidationError as e: error_message = ( f"Configuration error in '{self.config_path}'!\n" f"Please correct the following issues:\n\n{e}" ) raise ConfigError(error_message)