SXXXXXXX_GitUtility/config_manager.py

408 lines
18 KiB
Python

# --- START OF FILE config_manager.py ---
# config_manager.py
import configparser
import os
# Rimosso import logging
import sys # Per fallback print
# Importa il nuovo gestore della coda log
import log_handler
# Constants
CONFIG_FILE = "git_svn_sync.ini"
DEFAULT_PROFILE = "default"
# Single source of truth for the default backup directory path
DEFAULT_BACKUP_DIR = os.path.join(os.path.expanduser("~"), "git_svn_backup")
class ConfigManager:
"""
Manages the configuration file (git_svn_sync.ini) for the tool.
Handles loading, saving, and accessing profile settings using log_handler.
Ensures default profile and all expected keys exist across profiles.
"""
# Rimosso logger da __init__
def __init__(self, logger_ignored=None): # Accetta argomento ma lo ignora
"""Initializes the ConfigManager."""
# Non c'è più self.logger
# Inizializza il parser
self.config = configparser.ConfigParser()
# Carica la configurazione all'inizializzazione
# Usa try-except per gestire errori critici durante il caricamento iniziale
try:
self.load_config() # Ensure config is loaded and validated
log_handler.log_debug(
"ConfigManager initialized and config loaded.", func_name="__init__"
)
except Exception as e:
# Log critico se il caricamento iniziale fallisce completamente
# Usa print perché il logging potrebbe non essere ancora pronto
print(
f"CRITICAL ERROR: Failed to load initial configuration: {e}",
file=sys.stderr,
)
log_handler.log_critical(
f"Failed to load initial configuration: {e}", func_name="__init__"
)
# Inizializza con un config vuoto per permettere all'app di partire (forse)
self.config = configparser.ConfigParser()
# Potrebbe essere necessario sollevare l'eccezione qui per fermare l'app
# raise RuntimeError("Failed to initialize configuration manager") from e
def _get_expected_keys_with_defaults(self):
"""
Returns a dictionary of expected config keys and their default values.
This is the single source of truth for profile keys.
"""
return {
"svn_working_copy_path": "/path/to/your/repo", # Aggiornato default
"usb_drive_path": "/path/to/your/usb", # Aggiornato default
"bundle_name": "my_repo.bundle",
"bundle_name_updated": "my_repo_update.bundle",
"autobackup": "False",
"backup_dir": DEFAULT_BACKUP_DIR, # Usa costante definita sopra
"autocommit": "False",
"commit_message": "",
"backup_exclude_extensions": ".log,.tmp,.bak,~*", # Esempio default
"backup_exclude_dirs": "__pycache__,.venv,.vscode,build,dist,node_modules,.git,.svn", # Default esteso
}
def load_config(self):
"""
Loads config from CONFIG_FILE, creates/validates defaults and keys.
Uses log_handler for logging.
"""
func_name = "load_config"
log_handler.log_debug(
f"Attempting to load configuration from '{CONFIG_FILE}'...",
func_name=func_name,
)
needs_save = False # Flag to track if config needs saving after load/check
try:
config_exists = os.path.exists(CONFIG_FILE)
files_read = []
if config_exists:
# Try reading the existing file
try:
# Specify UTF-8 encoding for consistency
files_read = self.config.read(CONFIG_FILE, encoding="utf-8")
if not files_read and config_exists:
# File exists but configparser couldn't read it (e.g., empty, corrupted format)
log_handler.log_warning(
f"Config file '{CONFIG_FILE}' exists but is empty or unreadable. Will recreate.",
func_name=func_name,
)
self.config = configparser.ConfigParser() # Reset parser
config_exists = False # Force default creation logic
except configparser.Error as read_err:
log_handler.log_error(
f"Error parsing config file '{CONFIG_FILE}': {read_err}. Recreating with defaults.",
func_name=func_name,
)
self.config = configparser.ConfigParser() # Reset parser
config_exists = False # Force default creation logic
# --- Create or Validate Configuration ---
if not config_exists:
# File doesn't exist or failed to parse
log_handler.log_info(
f"Config file '{CONFIG_FILE}' not found or invalid. Creating with default profile.",
func_name=func_name,
)
# Ensure clean parser state if recreating
self.config = configparser.ConfigParser()
self._create_default_profile() # Create the default section
needs_save = True # Mark for saving the new default profile
else:
# File exists and was read successfully
log_handler.log_debug(
f"Successfully read from '{CONFIG_FILE}'. Validating content...",
func_name=func_name,
)
# --- Ensure Default Profile Exists and Has All Keys ---
if DEFAULT_PROFILE not in self.config.sections():
log_handler.log_warning(
f"Default profile '{DEFAULT_PROFILE}' missing. Adding with defaults.",
func_name=func_name,
)
self._create_default_profile() # Creates section and adds all keys
needs_save = True
else:
# Default profile exists, check/add missing keys
expected_keys = self._get_expected_keys_with_defaults()
for key, default_value in expected_keys.items():
if not self.config.has_option(DEFAULT_PROFILE, key):
log_handler.log_warning(
f"Key '{key}' missing in profile '{DEFAULT_PROFILE}'. Adding default: '{default_value}'.",
func_name=func_name,
)
self.config.set(DEFAULT_PROFILE, key, default_value)
needs_save = True
# --- Ensure ALL Profiles Have ALL Expected Keys ---
expected_keys = self._get_expected_keys_with_defaults()
all_current_profiles = (
self.config.sections()
) # Get potentially updated list
for profile_name in all_current_profiles:
for key, default_value in expected_keys.items():
if not self.config.has_option(profile_name, key):
log_handler.log_warning(
f"Key '{key}' missing in profile '{profile_name}'. Adding default: '{default_value}'.",
func_name=func_name,
)
self.config.set(profile_name, key, default_value)
needs_save = True # Mark for saving
# Save configuration only if changes were made during loading/validation
if needs_save:
log_handler.log_info(
f"Configuration updated. Saving changes to '{CONFIG_FILE}'.",
func_name=func_name,
)
self.save_config() # Call save method
except Exception as e:
# Catch unexpected errors during the load/validation process
log_handler.log_exception(
f"Unexpected error loading/validating config: {e}", func_name=func_name
)
# Reset to a safe state (empty config) in case of severe issues
self.config = configparser.ConfigParser()
# Optionally re-raise or handle this critical failure
def _create_default_profile(self):
"""Internal helper: Creates the default profile section with default keys/values."""
func_name = "_create_default_profile"
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 expected keys and set them for the default profile
expected_keys = self._get_expected_keys_with_defaults()
log_handler.log_debug(
f"Setting default keys for profile '{DEFAULT_PROFILE}'.",
func_name=func_name,
)
for key, default_value in expected_keys.items():
# configparser's set() handles both adding and updating options
self.config.set(DEFAULT_PROFILE, key, default_value)
log_handler.log_info(
f"Ensured default profile '{DEFAULT_PROFILE}' exists with default keys.",
func_name=func_name,
)
def save_config(self):
"""Saves the current configuration state to the CONFIG_FILE."""
func_name = "save_config"
log_handler.log_debug(
f"Attempting to save configuration to '{CONFIG_FILE}'...",
func_name=func_name,
)
# Check/Create directory if config file is in a subdirectory (optional)
config_dir = os.path.dirname(CONFIG_FILE)
# Ensure directory exists only if CONFIG_FILE specifies one
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 saving attempt (might work in current dir)
log_handler.log_error(
f"Could not create directory '{config_dir}': {e}. Save might fail.",
func_name=func_name,
)
try:
# Write the config object to the file using 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 IO 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
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
)
# Optionally re-raise
# raise
def get_profile_sections(self):
"""Returns a sorted list of all profile section names."""
# Sort alphabetically for consistent display order in UI
return sorted(self.config.sections())
def get_profile_option(self, profile, option, fallback=None):
"""
Retrieves a specific option from a profile section, handling errors.
Args:
profile (str): Name of the profile section.
option (str): Name of the option key.
fallback (any, optional): Value to return if option/section not found.
Returns:
str | any: The option value as a string if found, otherwise the original fallback.
"""
func_name = "get_profile_option"
try:
# Check section existence first for clearer logging
if not self.config.has_section(profile):
log_handler.log_warning(
f"Profile '{profile}' not found. Cannot get option '{option}'. Using fallback: {fallback}",
func_name=func_name,
)
return fallback # Return original fallback type
# Use configparser's built-in fallback mechanism if option missing
# Convert fallback to string for configparser, handling None
str_fallback = str(fallback) if fallback is not None else None
value_str = self.config.get(profile, option, fallback=str_fallback)
# Check if the fallback was actually used because the option was missing
option_was_missing = not self.config.has_option(profile, option)
# Careful comparison needed if fallback could be None or empty string
if value_str == str_fallback and option_was_missing:
log_handler.log_debug(
f"Option '{option}' not found in profile '{profile}'. Used fallback value.",
func_name=func_name,
)
return fallback # Return original fallback type if option was truly missing
# Return the value obtained (always a string from configparser)
return value_str
except Exception as e:
# Log unexpected errors during option retrieval
log_handler.log_exception(
f"Error getting option '{option}' for profile '{profile}': {e}",
func_name=func_name,
)
# Return the fallback value on any error
return fallback
def set_profile_option(self, profile, option, value):
"""
Sets an option in a profile section, creating the section if needed.
Value is converted to string for storage. 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 string).
"""
func_name = "set_profile_option"
try:
# Create section if it doesn't exist
if not self.config.has_section(profile):
self.config.add_section(profile)
log_handler.log_info(
f"Created new profile section during set: '{profile}'",
func_name=func_name,
)
# Convert value to string for storage (handle None)
value_str = str(value) if value is not None else ""
# Set the option using configparser
self.config.set(profile, option, value_str)
log_handler.log_debug(
f"Set option '{option}' = '{value_str}' in profile '{profile}'.",
func_name=func_name,
)
except configparser.Error as e:
# Log specific configparser errors
log_handler.log_error(
f"ConfigParser error setting option '{option}' in '{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}': {e}",
func_name=func_name,
)
def remove_profile_section(self, profile):
"""
Removes a profile section (cannot remove the default profile). Uses log_handler.
Args:
profile (str): Name of the profile section to remove.
Returns:
bool: True if removed successfully, False otherwise.
"""
func_name = "remove_profile_section"
# Prevent removal of the default profile
if profile == DEFAULT_PROFILE:
log_handler.log_warning(
f"Attempt to remove default profile '{DEFAULT_PROFILE}' denied.",
func_name=func_name,
)
return False
# Check if the profile section exists
if self.config.has_section(profile):
try:
# Attempt to remove the section
removed = self.config.remove_section(profile)
if removed:
log_handler.log_info(
f"Removed profile section: '{profile}'", func_name=func_name
)
return True
else:
# Should not happen if has_section was True, but log anyway
log_handler.log_warning(
f"ConfigParser failed to remove existing section '{profile}'.",
func_name=func_name,
)
return False
except Exception as e:
# Log errors during removal
log_handler.log_exception(
f"Error removing profile section '{profile}': {e}",
func_name=func_name,
)
return False
else:
# Profile section not found
log_handler.log_warning(
f"Profile section '{profile}' not found, cannot remove.",
func_name=func_name,
)
return False
def add_section(self, section_name):
"""Adds a new section if it doesn't exist."""
# Helper function, might be useful for add_profile logic
if not self.config.has_section(section_name):
self.config.add_section(section_name)
log_handler.log_info(
f"Added new config section: '{section_name}'", func_name="add_section"
)
return True
return False # Already existed
# --- END OF FILE config_manager.py ---