Fixed an error in the Advanced_ConfigParser, along with the additon of missing or incorrect docstrings

If the config and the template have the same number of sections and options, but they do not have the same names, they are still “equal”

Added the `check_for_update` method to update existing configurations

Changed the `logger` attribute from public to private
This commit is contained in:
2024-10-24 18:10:26 +02:00
parent a272c5c087
commit ffdb2ef4d5
+108 -48
View File
@@ -6,31 +6,42 @@ from shutil import copy
class Advanced_ConfigParser(configparser.ConfigParser): class Advanced_ConfigParser(configparser.ConfigParser):
"""Utility class to ease the use of the configparser libary""" """Utility class to ease the use of the configparser libary"""
VERSION = "2.0" VERSION = "2.5"
number_of_instances = 0 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__() super().__init__()
self.__instance_number = self.__class__.number_of_instances self.__instance_number = self.__class__.number_of_instances
self.__class__.number_of_instances += 1 self.__class__.number_of_instances += 1
self.__from_template = False self.__from_template = False
self.logger = logging.getLogger(f"utils.config.{self.__instance_number}") self.__logger = logging.getLogger(f"utils.config.{self.__instance_number}")
self.logger.debug(f"New instance {self.__instance_number} of class created") self.__logger.debug(f"New instance {self.__instance_number} of class created")
# Check existence of provided file # Check existence of provided file
self.__template_path = self.__get_template_path(path) self.__template_path = self.__get_template_path(path)
if isfile(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: else:
# Check if it is allowed to use an existing template # Check if it is allowed to use an existing template
if allow_template: if allow_template:
if isfile(self.__template_path): 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) copy(self.__template_path, path)
self.__from_template = True self.__from_template = True
else: 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 # Load the configfile
self.__file_path = path self.__file_path = path
@@ -41,7 +52,11 @@ class Advanced_ConfigParser(configparser.ConfigParser):
except: except:
pass pass
else: 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: def __get_template_path(self, path:str) -> str | None:
"""Looks for a existing template and returns the path to it """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: def set(self, section: str, option: str, value: str | None = None) -> None:
old_value = self.get(section, option, fallback=None) old_value = self.get(section, option, fallback=None)
if old_value: 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 self.__pending_changes += 1
else: 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: if old_value != value:
self.__pending_changes += 1 self.__pending_changes += 1
@@ -79,14 +95,14 @@ class Advanced_ConfigParser(configparser.ConfigParser):
def remove_option(self, section, option): def remove_option(self, section, option):
if self.has_option(section, option): if self.has_option(section, option):
value = self.get(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 self.__pending_changes += 1
return super().remove_option(section, option) return super().remove_option(section, option)
def remove_section(self, section): def remove_section(self, section):
if self.has_section(section): if self.has_section(section):
self.logger.info(f"Section removed: '{section}'") self.__logger.info(f"Section removed: '{section}'")
self.__pending_changes += 1 self.__pending_changes += 1
return super().remove_section(section) return super().remove_section(section)
@@ -95,7 +111,7 @@ class Advanced_ConfigParser(configparser.ConfigParser):
"""Saves changes to disk""" """Saves changes to disk"""
with open(self.__file_path, 'w') as configfile: with open(self.__file_path, 'w') as configfile:
self.write(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: def get_config_file_path(self) -> str:
"""Returns the path to the config file""" """Returns the path to the config file"""
@@ -115,60 +131,104 @@ class Advanced_ConfigParser(configparser.ConfigParser):
+-----------------+--------------------------------------------------------------------------------------------------------------------------+ +-----------------+--------------------------------------------------------------------------------------------------------------------------+
| not_found | No template file found | | not_found | No template file found |
| equal | Both files contain the same sections and options | | 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_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 | | config_minus | Config contains fewer sections and/or options as template |
+-----------------+--------------------------------------------------------------------------------------------------------------------------+""" +-----------------+--------------------------------------------------------------------------------------------------------------------------+"""
# Check existence of template # Check existence of template
if self.__template_path is None: if self.__template_path == None:
return "not_found" return "not_found"
# Open template file
template_config = configparser.ConfigParser() template_config = configparser.ConfigParser()
template_config.read_file(open(self.__template_path)) template_config.read_file(open(self.__template_path))
# Get sections of both config and template
config_sections = set(self.sections()) config_sections = set(self.sections())
template_sections = set(template_config.sections()) template_sections = set(template_config.sections())
# Compare sections # Check if config contains all the same (or more) sections than template
if config_sections == template_sections: same_or_more_sections = 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))
if config_options > template_options: # Check if both files have the same amount of sections
config_plus_found = True # More options in config than template same_amount_of_sections = len(config_sections) == len(template_sections)
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"
elif config_sections > template_sections: # Consolidate the options of both files
# Config has additional sections all_config_options = set()
return "config_plus" 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: # Check if both have the same options
# Config has fewer sections 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" 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: def created_from_template(self) -> bool:
"""Will ONLY return `True` if the config file has been created from template at the first run""" """Will ONLY return `True` if the config file has been created from template at the first run"""
return self.__from_template 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__": if __name__ == "__main__":
# Dummy logger # Dummy logger