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
+102 -42
View File
@@ -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"""
@@ -122,54 +138,98 @@ class Advanced_ConfigParser(configparser.ConfigParser):
+-----------------+--------------------------------------------------------------------------------------------------------------------------+"""
# 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
# Check if config contains all the same (or more) sections than template
same_or_more_sections = config_sections >= template_sections
for section in config_sections:
config_options = set(self.options(section))
template_options = set(template_config.options(section))
# Check if both files have the same amount of sections
same_amount_of_sections = len(config_sections) == len(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
# 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
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:
try:
for option in template_config.options(section):
all_template_options.add(f"{section}.{option}")
except configparser.NoSectionError:
template_all_present = False
# 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"
elif config_sections > template_sections:
# Config has additional sections
return "config_plus"
elif config_sections < template_sections:
# Config has fewer sections
# 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
console = logging.StreamHandler()