SXXXXXXX_GitUtility/gitutility/config/config_manager.py
2025-05-05 10:28:19 +02:00

473 lines
22 KiB
Python

# --- FILE: gitsync_tool/config/config_manager.py ---
import configparser
import os
import sys # Per fallback print
from typing import Optional, Dict, List, Any # Aggiunti type hints
# ---<<< MODIFICA IMPORT >>>---
# Importa usando il percorso assoluto dal pacchetto gitsync_tool
from gitutility.logging_setup import log_handler
# ---<<< FINE MODIFICA IMPORT >>>---
# --- Constants ---
CONFIG_FILE: str = "git_svn_sync.ini" # Nome file configurazione
DEFAULT_PROFILE: str = "default" # Nome del profilo di default
# Percorso di backup di default (usa os.path.join per compatibilità OS)
DEFAULT_BACKUP_DIR: str = os.path.join(
os.path.expanduser("~"), "git_sync_backups"
) # Nome cartella cambiato per chiarezza
# Nome di default per il remote Git
DEFAULT_REMOTE_NAME: str = "origin"
class ConfigManager:
"""
Manages the application's configuration file (git_svn_sync.ini).
Handles loading, saving, validating, and accessing profile settings.
Uses the centralized log_handler for logging operations.
Ensures a default profile exists and all expected keys are present
across all profiles, adding defaults where necessary.
"""
def __init__(
self, logger_ignored: Optional[Any] = None
): # Accetta argomento ma lo ignora
"""Initializes the ConfigManager and loads the configuration."""
# self.logger non è più usato
# Initialize the config parser instance
self.config: configparser.ConfigParser = configparser.ConfigParser()
# Load and validate the configuration during initialization
# Use try-except to handle critical errors during the initial load
try:
self.load_config() # This method now handles creation/validation
log_handler.log_debug(
"ConfigManager initialized and configuration loaded/validated.",
func_name="__init__",
)
except Exception as e:
# Log critical failure if initial load/validation fails completely
# Use print as a fallback since logging itself might depend on config/paths
print(
f"CRITICAL ERROR: Failed to initialize configuration: {e}",
file=sys.stderr,
)
log_handler.log_critical(
f"Failed to load/validate initial configuration: {e}",
func_name="__init__",
)
# Initialize with an empty config parser to allow the app to potentially
# start in a degraded state or prompt the user.
self.config = configparser.ConfigParser()
# Depending on app design, might want to raise RuntimeError here
# raise RuntimeError("Failed to initialize configuration manager") from e
def _get_expected_keys_with_defaults(self) -> Dict[str, str]:
"""
Returns a dictionary defining all expected config keys for a profile
and their default string values. This serves as the single source of truth.
"""
# Define all expected keys and their default values here
return {
# Core Paths & Names
"svn_working_copy_path": "", # Default to empty, user must provide
"usb_drive_path": "", # Default to empty
"bundle_name": "repository.bundle",
"bundle_name_updated": "repository_update.bundle",
# Backup Settings
"autobackup": "False", # Use string "True"/"False" for ini compatibility
"backup_dir": DEFAULT_BACKUP_DIR, # Use the defined constant
"backup_exclude_extensions": ".log, .tmp, .bak, ~*", # Example defaults
"backup_exclude_dirs": "__pycache__, .venv, .vscode, build, dist, node_modules, .git, .svn", # Extended defaults
# Commit Settings
"autocommit": "False",
"commit_message": "", # Default empty commit message
# Remote Settings
"remote_url": "", # Default to empty, user needs to configure
"remote_name": DEFAULT_REMOTE_NAME, # Use the defined constant 'origin'
}
def load_config(self) -> None:
"""
Loads configuration from CONFIG_FILE. Creates the file and/or default
profile with all expected keys if they don't exist. Validates existing
profiles to ensure all expected keys are present, adding defaults if missing.
Uses the centralized log_handler for logging.
"""
func_name: str = "load_config"
log_handler.log_debug(
f"Attempting to load configuration from '{CONFIG_FILE}'...",
func_name=func_name,
)
config_needs_saving: bool = False # Flag to track if changes require saving
try:
config_exists: bool = os.path.exists(CONFIG_FILE)
files_read: List[str] = []
if config_exists:
try:
# Attempt to read the configuration file using UTF-8
files_read = self.config.read(CONFIG_FILE, encoding="utf-8")
# Handle case where file exists but is empty or unreadable by configparser
if not files_read and config_exists:
log_handler.log_warning(
f"Configuration file '{CONFIG_FILE}' exists but is empty or could not be parsed. Will recreate with defaults.",
func_name=func_name,
)
# Reset config object to start fresh
self.config = configparser.ConfigParser()
config_exists = False # Treat as if file didn't exist
except configparser.Error as read_err:
# Handle parsing errors (e.g., invalid INI format)
log_handler.log_error(
f"Error parsing configuration file '{CONFIG_FILE}': {read_err}. Recreating file with defaults.",
func_name=func_name,
)
self.config = configparser.ConfigParser()
config_exists = False # Treat as if file didn't exist
# If file didn't exist or was invalid, create default structure
if not config_exists:
log_handler.log_info(
f"Configuration file '{CONFIG_FILE}' not found or invalid. Creating a new one with the default profile.",
func_name=func_name,
)
# Ensure config object is clean (already done above if parsing failed)
if self.config.sections(): # Should be empty, but check just in case
self.config = configparser.ConfigParser()
# Create the default profile with all expected keys
self._create_default_profile() # Helper function to add section and keys
config_needs_saving = True # Mark that the new config needs to be saved
else:
# Config file was read successfully, proceed with validation
log_handler.log_debug(
f"Successfully read configuration from '{CONFIG_FILE}'. Validating content...",
func_name=func_name,
)
# --- Content Validation ---
expected_keys_defaults: Dict[str, str] = (
self._get_expected_keys_with_defaults()
)
all_current_profiles: List[str] = list(
self.config.sections()
) # Get copy of section names
# 1. Ensure the default profile exists
if DEFAULT_PROFILE not in all_current_profiles:
log_handler.log_warning(
f"Default profile '{DEFAULT_PROFILE}' is missing in the loaded config. Adding it with default values.",
func_name=func_name,
)
self._create_default_profile() # Create the default profile
# Refresh the list of profiles if default was just added
all_current_profiles = list(self.config.sections())
config_needs_saving = True # Mark for saving
# 2. Validate keys for ALL existing profiles
log_handler.log_debug(
f"Validating keys for profiles: {all_current_profiles}",
func_name=func_name,
)
for profile_name in all_current_profiles:
for key, default_value in expected_keys_defaults.items():
# Check if the profile section is missing the expected key
if not self.config.has_option(profile_name, key):
# Use the default value defined in _get_expected_keys_with_defaults
log_handler.log_warning(
f"Key '{key}' missing in profile '{profile_name}'. Adding it with default value: '{default_value}'.",
func_name=func_name,
)
# Add the missing key with its default value
self.config.set(profile_name, key, default_value)
config_needs_saving = True # Mark for saving
# Save configuration if any changes were made during load/validation
if config_needs_saving:
log_handler.log_info(
f"Configuration was updated during load/validation. Saving changes to '{CONFIG_FILE}'.",
func_name=func_name,
)
self.save_config() # Call save method
except Exception as e:
# Catch any unexpected errors during the load/validation process
log_handler.log_exception(
f"Unexpected error loading or validating configuration: {e}",
func_name=func_name,
)
# Optionally reset config to a safe state or re-raise
self.config = configparser.ConfigParser()
# raise RuntimeError("Critical configuration error during load") from e
def _create_default_profile(self) -> None:
"""
Internal helper method to create the default profile section
and populate it with all expected keys and their default values.
"""
func_name = "_create_default_profile"
# Ensure the section exists
if DEFAULT_PROFILE not in self.config.sections():
log_handler.log_debug(
f"Adding default profile section: '{DEFAULT_PROFILE}'.",
func_name=func_name,
)
self.config.add_section(DEFAULT_PROFILE)
# Get the standard keys and defaults
expected_keys_defaults: Dict[str, str] = self._get_expected_keys_with_defaults()
log_handler.log_debug(
f"Setting default keys/values for profile '{DEFAULT_PROFILE}'.",
func_name=func_name,
)
# Set each expected key with its default value in the default profile
for key, default_value in expected_keys_defaults.items():
# configparser's set() method handles adding/updating the option
self.config.set(DEFAULT_PROFILE, key, default_value)
log_handler.log_info(
f"Ensured default profile '{DEFAULT_PROFILE}' exists with all expected keys.",
func_name=func_name,
)
def save_config(self) -> None:
"""
Saves the current configuration state (self.config) to the CONFIG_FILE.
Uses UTF-8 encoding. Uses log_handler.
"""
func_name: str = "save_config"
log_handler.log_debug(
f"Attempting to save configuration to '{CONFIG_FILE}'...",
func_name=func_name,
)
# Optional: Check/Create directory if config file is specified with a path
config_dir: str = os.path.dirname(CONFIG_FILE)
# Ensure directory exists only if CONFIG_FILE includes a directory part
if config_dir and not os.path.exists(config_dir):
try:
os.makedirs(config_dir)
log_handler.log_info(
f"Created directory for config file: {config_dir}",
func_name=func_name,
)
except OSError as e:
# Log error but proceed with save attempt (might succeed in current dir)
log_handler.log_error(
f"Could not create directory '{config_dir}': {e}. Save attempt might fail.",
func_name=func_name,
)
try:
# Write the config object to the file using 'w' mode and UTF-8 encoding
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
self.config.write(configfile)
log_handler.log_info(
f"Configuration saved successfully to '{CONFIG_FILE}'.",
func_name=func_name,
)
except IOError as e:
# Log specific I/O errors during save
log_handler.log_error(
f"IOError writing config file '{CONFIG_FILE}': {e}", func_name=func_name
)
# Optionally re-raise depending on desired application behavior
# raise IOError(f"Failed to save config: {e}") from e
except Exception as e:
# Log any other unexpected errors during save
log_handler.log_exception(
f"Unexpected error saving configuration: {e}", func_name=func_name
)
# raise RuntimeError(f"Unexpected error saving config: {e}") from e
def get_profile_sections(self) -> List[str]:
"""
Returns a sorted list of all profile section names present in the configuration.
"""
# Sort alphabetically for consistent display order in UI elements
return sorted(self.config.sections())
def get_profile_option(
self, profile: str, option: str, fallback: Optional[str] = None
) -> Optional[str]:
"""
Retrieves a specific option's value from a profile section as a string.
Handles missing sections or options gracefully using the fallback. Uses log_handler.
Args:
profile (str): Name of the profile section (e.g., 'default').
option (str): Name of the option key (e.g., 'remote_url').
fallback (Optional[str]): Value to return if the option or section is not found.
Returns:
Optional[str]: The option value as a string if found, otherwise the fallback value.
Returns None if fallback is None and option is missing.
"""
func_name: str = "get_profile_option"
try:
# Check if the profile section exists first for better logging
if not self.config.has_section(profile):
log_handler.log_warning(
f"Profile '{profile}' not found. Cannot get option '{option}'. Returning fallback: {repr(fallback)}",
func_name=func_name,
)
return fallback # Return the original fallback value
# Use configparser's get method with its built-in fallback mechanism
# Configparser always returns strings or the fallback if the fallback itself is provided.
value_str: Optional[str] = self.config.get(
profile, option, fallback=fallback
)
# Log if fallback was potentially used (more informative debug log)
# Note: This check isn't perfect if fallback could be the actual value,
# but it helps in debugging missing keys.
if value_str == fallback and not self.config.has_option(profile, option):
log_handler.log_debug(
f"Option '{option}' not found in profile '{profile}'. Used fallback value: {repr(fallback)}",
func_name=func_name,
)
# Return the value obtained (which will be a string or the original fallback)
return value_str
except Exception as e:
# Log unexpected errors during option retrieval
log_handler.log_exception(
f"Unexpected error getting option '{option}' for profile '{profile}': {e}",
func_name=func_name,
)
# Return the fallback value in case of any error
return fallback
def set_profile_option(self, profile: str, option: str, value: Any) -> None:
"""
Sets an option in a specific profile section. Creates the section if needed.
The provided value is automatically converted to a string for storage in the INI file.
Uses log_handler.
Args:
profile (str): Name of the profile section.
option (str): Name of the option key.
value (Any): Value to set (will be converted to its string representation).
None will be stored as an empty string.
"""
func_name: str = "set_profile_option"
try:
# Ensure the profile section exists, creating it if necessary
if not self.config.has_section(profile):
self.config.add_section(profile)
log_handler.log_info(
f"Created new profile section '{profile}' while setting option '{option}'.",
func_name=func_name,
)
# Convert the value to string for storage (handle None case)
value_str: str = str(value) if value is not None else ""
# Set the option using configparser's set method
self.config.set(profile, option, value_str)
log_handler.log_debug(
f"Set option in profile '{profile}': {option} = '{value_str}'",
func_name=func_name,
)
except configparser.Error as e:
# Log specific configparser errors (e.g., invalid section/option names)
log_handler.log_error(
f"ConfigParser error setting option '{option}' in profile '{profile}': {e}",
func_name=func_name,
)
except Exception as e:
# Log other unexpected errors
log_handler.log_exception(
f"Unexpected error setting option '{option}' in profile '{profile}': {e}",
func_name=func_name,
)
def remove_profile_section(self, profile: str) -> bool:
"""
Removes a profile section entirely from the configuration.
Prevents removal of the default profile. Uses log_handler.
Args:
profile (str): Name of the profile section to remove.
Returns:
bool: True if the section was successfully removed, False otherwise
(e.g., profile not found or is the default profile).
"""
func_name: str = "remove_profile_section"
# Prevent removal of the required default profile
if profile == DEFAULT_PROFILE:
log_handler.log_warning(
f"Attempt to remove the default profile ('{DEFAULT_PROFILE}') was denied.",
func_name=func_name,
)
return False # Indicate failure
# Check if the profile section actually exists
if self.config.has_section(profile):
try:
# Attempt to remove the section using configparser
removed: bool = self.config.remove_section(profile)
if removed:
log_handler.log_info(
f"Successfully removed profile section: '{profile}'",
func_name=func_name,
)
return True # Indicate success
else:
# This case should be rare if has_section was True, but log it
log_handler.log_warning(
f"ConfigParser reported failure trying to remove existing section '{profile}'.",
func_name=func_name,
)
return False # Indicate failure
except Exception as e:
# Log any errors that occur during the removal process
log_handler.log_exception(
f"Error removing profile section '{profile}': {e}",
func_name=func_name,
)
return False # Indicate failure
else:
# Profile section was not found
log_handler.log_warning(
f"Profile section '{profile}' not found. Cannot remove.",
func_name=func_name,
)
return False # Indicate failure
def add_section(self, section_name: str) -> bool:
"""
Adds a new section (profile) to the configuration if it doesn't already exist.
Args:
section_name (str): The name of the section (profile) to add.
Returns:
bool: True if the section was added, False if it already existed.
"""
# Helper function, mainly used internally by add_profile logic in controller
if not self.config.has_section(section_name):
self.config.add_section(section_name)
log_handler.log_info(
f"Added new configuration section: '{section_name}'",
func_name="add_section",
)
return True # Section was added
else:
log_handler.log_debug(
f"Section '{section_name}' already exists. No action taken.",
func_name="add_section",
)
return False # Section already existed
# --- END OF FILE gitsync_tool/config/config_manager.py ---