diff --git a/src/utils/adv_configparser.py b/src/utils/adv_configparser.py index b1ddbc8..7c9a5fd 100644 --- a/src/utils/adv_configparser.py +++ b/src/utils/adv_configparser.py @@ -6,31 +6,42 @@ from shutil import copy class Advanced_ConfigParser(configparser.ConfigParser): """Utility class to ease the use of the configparser libary""" - VERSION = "2.0" + VERSION = "2.5" number_of_instances = 0 - def __init__(self, path:str, allow_template:bool = True) -> None: + def __init__(self, path:str, allow_template:bool = True, allow_update:bool = True) -> None: + """ + Initializes the Advanced_ConfigParser instance. + + Args: + path (str): The path to the configuration file. + allow_template (bool, optional): Determines if an existing template can be used when the config file is missing. Default is True. + allow_update (bool, optional): Specifies if the config file should be updated based on a template. Default is True. + + This constructor loads the configuration file if it exists, optionally creates one from a template if allowed, + and can update the configuration based on the template. It also initializes logging and tracks pending changes. + """ super().__init__() self.__instance_number = self.__class__.number_of_instances self.__class__.number_of_instances += 1 self.__from_template = False - self.logger = logging.getLogger(f"utils.config.{self.__instance_number}") - self.logger.debug(f"New instance {self.__instance_number} of class created") + self.__logger = logging.getLogger(f"utils.config.{self.__instance_number}") + self.__logger.debug(f"New instance {self.__instance_number} of class created") # Check existence of provided file self.__template_path = self.__get_template_path(path) if isfile(path): - self.logger.debug(f"Opening existing file at path: '{path}'") + self.__logger.debug(f"Opening existing file at path: '{path}'") else: # Check if it is allowed to use an existing template if allow_template: if isfile(self.__template_path): - self.logger.info(f"Using existing template file {self.__template_path}") + self.__logger.info(f"Using existing template file {self.__template_path}") copy(self.__template_path, path) self.__from_template = True else: - self.logger.warning("No potential template found, starting with empty file") + self.__logger.warning("No potential template found, starting with empty file") # Load the configfile self.__file_path = path @@ -41,7 +52,11 @@ class Advanced_ConfigParser(configparser.ConfigParser): except: pass else: - self.logger.info(f"Successfully opened config file at path {path}") + self.__logger.info(f"Successfully opened config file at path {path}") + + # Check for update if allowed + if allow_update: + self.check_for_update() def __get_template_path(self, path:str) -> str | None: """Looks for a existing template and returns the path to it @@ -67,10 +82,11 @@ class Advanced_ConfigParser(configparser.ConfigParser): def set(self, section: str, option: str, value: str | None = None) -> None: old_value = self.get(section, option, fallback=None) if old_value: - self.logger.debug(f"New value in section '{section}' at option '{option}' added: '{value}'") + self.__logger.debug(f"New value in section '{section}' at option '{option}' added: '{value}'") self.__pending_changes += 1 else: - self.logger.debug(f"Value in section '{section}' at option '{option}' changed: '{self[section][option]}' -> '{value}'") + if self.has_option(section, option): + self.__logger.debug(f"Value in section '{section}' at option '{option}' changed: '{old_value}' -> '{value}'") if old_value != value: self.__pending_changes += 1 @@ -79,14 +95,14 @@ class Advanced_ConfigParser(configparser.ConfigParser): def remove_option(self, section, option): if self.has_option(section, option): value = self.get(section, option) - self.logger.debug(f"Option removed in section '{section}' for '{option}': '{value}'") + self.__logger.debug(f"Option removed in section '{section}' for '{option}': '{value}'") self.__pending_changes += 1 return super().remove_option(section, option) def remove_section(self, section): if self.has_section(section): - self.logger.info(f"Section removed: '{section}'") + self.__logger.info(f"Section removed: '{section}'") self.__pending_changes += 1 return super().remove_section(section) @@ -95,7 +111,7 @@ class Advanced_ConfigParser(configparser.ConfigParser): """Saves changes to disk""" with open(self.__file_path, 'w') as configfile: self.write(configfile) - self.logger.debug(f"Contents ({self.__pending_changes} changes) have been saved to disk") + self.__logger.debug(f"Contents ({self.__pending_changes} changes) have been saved to disk") def get_config_file_path(self) -> str: """Returns the path to the config file""" @@ -115,60 +131,104 @@ class Advanced_ConfigParser(configparser.ConfigParser): +-----------------+--------------------------------------------------------------------------------------------------------------------------+ | not_found | No template file found | | equal | Both files contain the same sections and options | - | config_plus | Config contains additional sections and/or options compared to template | + | config_plus | Config contains additional sections and/or options compared to template | | config_options | Config contains additional options but same sections | - | config_base | Same as "config_plus" but additionally config contains all sections and options from the template | + | config_base | Same as "config_plus" but additionally config contains all sections and options from the template | | config_minus | Config contains fewer sections and/or options as template | +-----------------+--------------------------------------------------------------------------------------------------------------------------+""" # Check existence of template - if self.__template_path is None: + if self.__template_path == None: return "not_found" - - # Open template file + template_config = configparser.ConfigParser() template_config.read_file(open(self.__template_path)) - - # Get sections of both config and template config_sections = set(self.sections()) template_sections = set(template_config.sections()) - # Compare sections - if config_sections == template_sections: - # Same sections, now compare options within each section - config_plus_found = False # For tracking additional options in config - config_minus_found = False # For tracking missing options in config - - for section in config_sections: - config_options = set(self.options(section)) - template_options = set(template_config.options(section)) + # Check if config contains all the same (or more) sections than template + same_or_more_sections = config_sections >= template_sections - if config_options > template_options: - config_plus_found = True # More options in config than template - elif config_options < template_options: - config_minus_found = True # Fewer options in config than template - - if config_plus_found and config_minus_found: - return "config_plus" - elif config_plus_found: - return "config_options" - elif config_minus_found: - return "config_minus" - else: - return "equal" + # Check if both files have the same amount of sections + same_amount_of_sections = len(config_sections) == len(template_sections) - elif config_sections > template_sections: - # Config has additional sections - return "config_plus" + # Consolidate the options of both files + all_config_options = set() + all_template_options = set() + all_sections = config_sections.union(template_sections) + config_all_present = True + template_all_present = True + for section in all_sections: + try: + for option in self.options(section): + all_config_options.add(f"{section}.{option}") + except configparser.NoSectionError: + config_all_present = False + + try: + for option in template_config.options(section): + all_template_options.add(f"{section}.{option}") + except configparser.NoSectionError: + template_all_present = False - elif config_sections < template_sections: - # Config has fewer sections + # Check if both have the same options + same_or_more_options = all_config_options >= all_template_options + + # Check if both have the same amount of options + same_amount_of_options = len(all_config_options) == len(all_template_options) + + # Check for different cases + # Since both files have the same (or more) options (and the options contain the section name) and contain the same amount, they must be equal + if same_or_more_options and same_amount_of_options: + return "equal" + + # Since the config contains not even the same sections as present in the template, it doesn't contain the same base options + if not same_or_more_sections: return "config_minus" - + + # If the config contains more sections or options than the template + if same_or_more_sections: + if same_or_more_options: + # If config contains all sections and options from template but also has extras + return "config_base" + else: + # Config has extra sections and/or options, but not necessarily all from template + return "config_plus" + + # If the config contains the same sections but additional options within those sections + if same_amount_of_sections and not same_amount_of_options: + return "config_options" + + return "config_plus" def created_from_template(self) -> bool: """Will ONLY return `True` if the config file has been created from template at the first run""" return self.__from_template + + def check_for_update(self, omit_save:bool = False): + """Updates the already created config to the same level as the template, + + no options are deleted or their values changed.""" + if self.__template_path == None: + raise FileNotFoundError + template_config = configparser.ConfigParser() + template_config.read_file(open(self.__template_path)) + + updated_options = 0 + for section in template_config.sections(): + options = template_config.options(section) + + for option in options: + if not self.has_option(section, option): + self.set(section, option, template_config.get(section, option)) + updated_options += 1 + + if updated_options: + _, file_name = split(self.__file_path) + self.__logger.error(f"The confg updated at {updated_options} locations, you might want check the {file_name} for unchanged placeholders") + + if not omit_save: + self.save() if __name__ == "__main__": # Dummy logger