From d0ab3257445f2370cefb374442cff867dba850d0 Mon Sep 17 00:00:00 2001
From: alfrix <alfredomonclus@gmail.com>
Date: Sun, 9 Jul 2023 14:02:47 -0300
Subject: [PATCH] config: changes in how the config is validated

now it validates in steps
1 defaults
2 user settings
3 user includes
4 auto-generated section

unknown keys will be removed from the auto-generated section if no other errors are found
detect and warn about missing newlines in headers
---
 ks_includes/config.py | 59 ++++++++++++++++++++++++++++---------------
 1 file changed, 38 insertions(+), 21 deletions(-)

diff --git a/ks_includes/config.py b/ks_includes/config.py
index e7def458..b3dd72e2 100644
--- a/ks_includes/config.py
+++ b/ks_includes/config.py
@@ -47,6 +47,8 @@ class KlipperScreenConfig:
 
         try:
             self.config.read(self.default_config_path)
+            # In case a user altered defaults.conf
+            self.validate_config(self.config)
             if self.config_path != self.default_config_path:
                 user_def, saved_def = self.separate_saved_config(self.config_path)
                 self.defined_config = configparser.ConfigParser()
@@ -59,19 +61,16 @@ class KlipperScreenConfig:
                 self.exclude_from_config(self.defined_config)
 
                 self.log_config(self.defined_config)
-                self.config.read_string(user_def)
+                if self.validate_config(self.defined_config, string=user_def):
+                    self.config.read_string(user_def)
                 if saved_def is not None:
-                    self.config.read_string(saved_def)
-                    logging.info(f"====== Saved Def ======\n{saved_def}\n=======================")
+                    auto_gen = configparser.ConfigParser()
+                    auto_gen.read_string(saved_def)
+                    if self.validate_config(auto_gen, string=saved_def, remove=True):
+                        self.config.read_string(saved_def)
+                        logging.info(f"====== Saved Def ======\n{saved_def}\n=======================")
             # This is the final config
             # self.log_config(self.config)
-            if self.validate_config():
-                logging.info('Configuration validated succesfuly')
-            else:
-                logging.error('Invalid configuration detected !!!')
-                logging.info('Loading default config')
-                self.config = configparser.ConfigParser()
-                self.config.read(self.default_config_path)
         except KeyError as Kerror:
             msg = f"Error reading config: {self.config_path}\n{Kerror}"
             logging.exception(msg)
@@ -139,9 +138,16 @@ class KlipperScreenConfig:
         self.lang = self.langs[lang]
         self.lang.install(names=['gettext', 'ngettext'])
 
-    def validate_config(self):
+    def validate_config(self, config, string="", remove=False):
         valid = True
-        for section in self.config:
+        if string:
+            msg = "Section headers have extra information after brackets possible newline issue:"
+            for line in string.split('\n'):
+                if re.match(r".+\].", line):
+                    logging.error(line)
+                    self.errors.append(f'{msg}\n\n{line}')
+                    return False
+        for section in config:
             if section == 'DEFAULT' or section.startswith('include '):
                 # Do not validate 'DEFAULT' or 'include*' sections
                 continue
@@ -175,7 +181,7 @@ class KlipperScreenConfig:
                 )
             elif section.startswith('preheat '):
                 strs = ('gcode', '')
-                numbers = [f'{option}' for option in self.config[section] if option != 'gcode']
+                numbers = [f'{option}' for option in config[section] if option != 'gcode']
             elif section.startswith('menu '):
                 strs = ('name', 'icon', 'panel', 'method', 'params', 'enable', 'confirm', 'style')
             elif section == 'bed_screws':
@@ -183,25 +189,29 @@ class KlipperScreenConfig:
                 numbers = ('rotation', '')
                 strs = ('screw_positions', '')
             elif section.startswith('graph') or section.startswith('displayed_macros'):
-                bools = [f'{option}' for option in self.config[section]]
+                bools = [f'{option}' for option in config[section]]
             elif section.startswith('z_calibrate_position'):
                 # This section may be deprecated in favor of moving this options under the printer section
                 numbers = ('calibrate_x_position', 'calibrate_y_position')
             else:
                 self.errors.append(f'Section [{section}] not recognized')
 
-            for key in self.config[section]:
+            for key in config[section]:
                 if key not in bools and key not in strs and key not in numbers:
                     msg = f'Option "{key}" not recognized for section "[{section}]"'
-                    self.errors.append(msg)
-                    # This most probably is not a big issue, continue to load the config
-                elif key in numbers and not self.is_float(self.config[section][key]) \
-                        or key in bools and self.config[section][key] not in ["False", "false", "True", "true"]:
+                    if remove:
+                        # This should only be called for the auto-generated section
+                        self.config.remove_option(section, key)
+                    else:
+                        self.errors.append(msg)
+                elif key in numbers and not self.is_float(config[section][key]) \
+                        or key in bools and not self.is_bool(config[section][key]):
                     msg = (
                         f'Unable to parse "{key}" from [{section}]\n'
-                        f'Expected a {"number" if key in numbers else "boolean"} but got: {self.config[section][key]}'
+                        f'Expected a {"number" if key in numbers else "boolean"} but got: {config[section][key]}'
                     )
                     self.errors.append(msg)
+                    logging.error('Invalid configuration detected !!!')
                     valid = False
         return valid
 
@@ -213,6 +223,10 @@ class KlipperScreenConfig:
         except ValueError:
             return False
 
+    @staticmethod
+    def is_bool(element):
+        return element in ["False", "false", "True", "true"]
+
     def get_errors(self):
         return "".join(f'{error}\n\n' for error in self.errors)
 
@@ -347,7 +361,10 @@ class KlipperScreenConfig:
                 self._include_config("/".join(full_path.split("/")[:-1]), include)
             self.exclude_from_config(config)
             self.log_config(config)
-            self.config.read(file)
+            with open(file, 'r') as f:
+                string = f.read()
+                if self.validate_config(config, string=string):
+                    self.config.read(file)
 
     def separate_saved_config(self, config_path):
         user_def = []