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:
+104
-44
@@ -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,61 +131,105 @@ 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
|
||||
# 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:
|
||||
return "equal"
|
||||
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 additional sections
|
||||
return "config_plus"
|
||||
# Check if both have the same options
|
||||
same_or_more_options = all_config_options >= all_template_options
|
||||
|
||||
elif config_sections < template_sections:
|
||||
# Config has fewer sections
|
||||
# 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
|
||||
console = logging.StreamHandler()
|
||||
|
||||
Reference in New Issue
Block a user