From 89b48168f40db73042fcd090293fc102a5454fd3 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 6 Sep 2025 13:12:20 +0200 Subject: [PATCH] fix: do not drop SAVE_CONFIG block when editing and writing config files (#723) Squashed 'kiauh/core/submodules/simple_config_parser/' changes from 4a6e5f2..f5eee99 f5eee99 feat: add support for parsing and handling `SAVE_CONFIG` blocks (#4) 8170583 refactor(api)!: `getval` now returns a string, `getvals` returns list of strings git-subtree-dir: kiauh/core/submodules/simple_config_parser git-subtree-split: f5eee99b0f04717c6bbf30c1256d70ad019223d5 --- .../submodules/simple_config_parser/README.md | 1 + .../src/simple_config_parser/constants.py | 3 + .../simple_config_parser.py | 32 ++++- .../tests/assets/test_config_4.cfg | 116 ++++++++++++++++++ .../test_data/matching_data.txt | 22 ++++ .../test_data/non_matching_data.txt | 6 + .../test_match_save_config_content.py | 37 ++++++ .../test_data/matching_data.txt | 6 + .../test_data/non_matching_data.txt | 13 ++ .../test_match_save_config_start.py | 37 ++++++ .../tests/public_api/conftest.py | 2 +- 11 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/test_config_4.cfg create mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/matching_data.txt create mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/non_matching_data.txt create mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_match_save_config_content.py create mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/matching_data.txt create mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/non_matching_data.txt create mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_match_save_config_start.py diff --git a/kiauh/core/submodules/simple_config_parser/README.md b/kiauh/core/submodules/simple_config_parser/README.md index c04af42..b84b6c7 100644 --- a/kiauh/core/submodules/simple_config_parser/README.md +++ b/kiauh/core/submodules/simple_config_parser/README.md @@ -12,6 +12,7 @@ Specialized for handling Klipper style config files. - Option Block: A line starting with a word, followed by a `:` or `=` and a newline - Comment: A line starting with a `#` or `;` - Blank: A line containing only whitespace characters +- SaveConfig: Klippers auto-generated SAVE_CONFIG section that can be found at the very end of the config file --- diff --git a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/constants.py b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/constants.py index 5afe9af..db9ecb3 100644 --- a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/constants.py +++ b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/constants.py @@ -49,6 +49,9 @@ LINE_COMMENT_RE = re.compile(r"^\s*[#;].*") # - the line MUST contain only whitespace characters EMPTY_LINE_RE = re.compile(r"^\s*$") +SAVE_CONFIG_START_RE = re.compile(r"^#\*# <-+ SAVE_CONFIG -+>$") +SAVE_CONFIG_CONTENT_RE = re.compile(r"^#\*#.*$") + BOOLEAN_STATES = { "1": True, "yes": True, diff --git a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py index 01d9374..4084779 100644 --- a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py +++ b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py @@ -18,7 +18,7 @@ from ..simple_config_parser.constants import ( LINE_COMMENT_RE, OPTION_RE, OPTIONS_BLOCK_START_RE, - SECTION_RE, LineType, INDENT, + SECTION_RE, LineType, INDENT, SAVE_CONFIG_START_RE, SAVE_CONFIG_CONTENT_RE, ) _UNSET = object() @@ -61,25 +61,34 @@ class SimpleConfigParser: def __init__(self) -> None: self.header: List[str] = [] + self.save_config_block: List[str] = [] self.config: Dict = {} self.current_section: str | None = None self.current_opt_block: str | None = None self.in_option_block: bool = False def _match_section(self, line: str) -> bool: - """Wheter or not the given line matches the definition of a section""" + """Whether the given line matches the definition of a section""" return SECTION_RE.match(line) is not None def _match_option(self, line: str) -> bool: - """Wheter or not the given line matches the definition of an option""" + """Whether the given line matches the definition of an option""" return OPTION_RE.match(line) is not None def _match_options_block_start(self, line: str) -> bool: - """Wheter or not the given line matches the definition of a multiline option""" + """Whether the given line matches the definition of a multiline option""" return OPTIONS_BLOCK_START_RE.match(line) is not None + def _match_save_config_start(self, line: str) -> bool: + """Whether the given line matches the definition of a save config start""" + return SAVE_CONFIG_START_RE.match(line) is not None + + def _match_save_config_content(self, line: str) -> bool: + """Whether the given line matches the definition of a save config content""" + return SAVE_CONFIG_CONTENT_RE.match(line) is not None + def _match_line_comment(self, line: str) -> bool: - """Wheter or not the given line matches the definition of a comment""" + """Whether the given line matches the definition of a comment""" return LINE_COMMENT_RE.match(line) is not None def _match_empty_line(self, line: str) -> bool: @@ -124,6 +133,14 @@ class SimpleConfigParser: element["value"].append(line.strip()) # indentation is removed break + elif self._match_save_config_start(line): + self.current_opt_block = None + self.save_config_block.append(line) + + elif self._match_save_config_content(line): + self.current_opt_block = None + self.save_config_block.append(line) + elif self._match_empty_line(line) or self._match_line_comment(line): self.current_opt_block = None @@ -185,6 +202,11 @@ class SimpleConfigParser: if not last_line.endswith("\n"): f.write("\n") + if self.save_config_block: + for line in self.save_config_block: + f.write(line) + f.write("\n") + def get_sections(self) -> List[str]: """Return a list of all section names, but exclude any section starting with '#_'""" return list( diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_4.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_4.cfg new file mode 100644 index 0000000..04cf0c7 --- /dev/null +++ b/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_4.cfg @@ -0,0 +1,116 @@ +# a comment at the very top +# should be treated as the file header + +# up to the first section, including all blank lines + +[section_1] +option_1: value_1 +option_1_1: True # this is a boolean +option_1_2: 5 ; this is an integer +option_1_3: 1.123 #;this is a float + +[section_2] ; comment +option_2: value_2 + +; comment + +[section_3] +option_3: value_3 # comment + +[section_4] +# comment +option_4: value_4 + +[section number 5] +#option_5: value_5 +option_5 = this.is.value-5 +multi_option: + # these are multi-line values + value_5_1 + value_5_2 ; here is a comment + value_5_3 +option_5_1: value_5_1 + +[gcode_macro M117] +rename_existing: M117.1 +gcode: + {% if rawparams %} + {% set escaped_msg = rawparams.split(';', 1)[0].split('\x23', 1)[0]|replace('"', '\\"') %} + SET_DISPLAY_TEXT MSG="{escaped_msg}" + RESPOND TYPE=command MSG="{escaped_msg}" + {% else %} + SET_DISPLAY_TEXT + {% endif %} + +# SDCard 'looping' (aka Marlin M808 commands) support +# +# Support SDCard looping +[sdcard_loop] +[gcode_macro M486] +gcode: + # Parameters known to M486 are as follows: + # [C] Cancel the current object + # [P] Cancel the object with the given index + # [S] Set the index of the current object. + # If the object with the given index has been canceled, this will cause + # the firmware to skip to the next object. The value -1 is used to + # indicate something that isn’t an object and shouldn’t be skipped. + # [T] Reset the state and set the number of objects + # [U] Un-cancel the object with the given index. This command will be + # ignored if the object has already been skipped + + {% if 'exclude_object' not in printer %} + {action_raise_error("[exclude_object] is not enabled")} + {% endif %} + + {% if 'T' in params %} + EXCLUDE_OBJECT RESET=1 + + {% for i in range(params.T | int) %} + EXCLUDE_OBJECT_DEFINE NAME={i} + {% endfor %} + {% endif %} + + {% if 'C' in params %} + EXCLUDE_OBJECT CURRENT=1 + {% endif %} + + {% if 'P' in params %} + EXCLUDE_OBJECT NAME={params.P} + {% endif %} + + {% if 'S' in params %} + {% if params.S == '-1' %} + {% if printer.exclude_object.current_object %} + EXCLUDE_OBJECT_END NAME={printer.exclude_object.current_object} + {% endif %} + {% else %} + EXCLUDE_OBJECT_START NAME={params.S} + {% endif %} + {% endif %} + + {% if 'U' in params %} + EXCLUDE_OBJECT RESET=1 NAME={params.U} + {% endif %} + +#*# <---------------------- SAVE_CONFIG ----------------------> +#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated. +#*# +#*# [bed_mesh default] +#*# version = 1 +#*# points = +#*# -0.152500, -0.133125, -0.113125, -0.159375, -0.232500 +#*# -0.095000, -0.078750, -0.068125, -0.133125, -0.235000 +#*# -0.092500, -0.040625, 0.004375, -0.077500, -0.193125 +#*# -0.073750, 0.023750, 0.085625, 0.026875, -0.085000 +#*# -0.140625, 0.038125, 0.126250, 0.097500, 0.003750 +#*# tension = 0.2 +#*# min_x = 26.0 +#*# algo = bicubic +#*# y_count = 5 +#*# mesh_y_pps = 2 +#*# min_y = 5.0 +#*# x_count = 5 +#*# max_y = 174.0 +#*# mesh_x_pps = 2 +#*# max_x = 194.0 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/matching_data.txt new file mode 100644 index 0000000..0839d60 --- /dev/null +++ b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/matching_data.txt @@ -0,0 +1,22 @@ +#*# any content +#*# +#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated. +#*# +#*# [bed_mesh default] +#*# version = 1 +#*# points = +#*# -0.152500, -0.133125, -0.113125, -0.159375, -0.232500 +#*# -0.095000, -0.078750, -0.068125, -0.133125, -0.235000 +#*# -0.092500, -0.040625, 0.004375, -0.077500, -0.193125 +#*# -0.073750, 0.023750, 0.085625, 0.026875, -0.085000 +#*# -0.140625, 0.038125, 0.126250, 0.097500, 0.003750 +#*# tension = 0.2 +#*# min_x = 26.0 +#*# algo = bicubic +#*# y_count = 5 +#*# mesh_y_pps = 2 +#*# min_y = 5.0 +#*# x_count = 5 +#*# max_y = 174.0 +#*# mesh_x_pps = 2 +#*# max_x = 194.0 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/non_matching_data.txt new file mode 100644 index 0000000..a97d893 --- /dev/null +++ b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/non_matching_data.txt @@ -0,0 +1,6 @@ + #*# leading space prevents match +random +*# not starting with hash-star-hash +# *# spaced out +<- SAVE_CONFIG -> +;#*# semicolon first diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_match_save_config_content.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_match_save_config_content.py new file mode 100644 index 0000000..7677266 --- /dev/null +++ b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_match_save_config_content.py @@ -0,0 +1,37 @@ +# ======================================================================= # +# Copyright (C) 2024 Dominik Willner # +# # +# https://github.com/dw-0/simple-config-parser # +# # +# This file may be distributed under the terms of the GNU GPLv3 license # +# ======================================================================= # + +from pathlib import Path + +import pytest + +from src.simple_config_parser.simple_config_parser import SimpleConfigParser +from tests.utils import load_testdata_from_file + +BASE_DIR = Path(__file__).parent.joinpath("test_data") +MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt") +NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt") + + +@pytest.fixture +def parser(): + return SimpleConfigParser() + + +def test_matching_lines(parser): + """Alle Zeilen in matching_data.txt sollen als Save-Config-Content erkannt werden.""" + matching_lines = load_testdata_from_file(MATCHING_TEST_DATA_PATH) + for line in matching_lines: + assert parser._match_save_config_content(line) is True, f"Line should be a save config content: {line!r}" + + +def test_non_matching_lines(parser): + """Alle Zeilen in non_matching_data.txt sollen NICHT als Save-Config-Content erkannt werden.""" + non_matching_lines = load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH) + for line in non_matching_lines: + assert parser._match_save_config_content(line) is False, f"Line should not be a save config content: {line!r}" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/matching_data.txt new file mode 100644 index 0000000..b2516e3 --- /dev/null +++ b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/matching_data.txt @@ -0,0 +1,6 @@ +#*# <- SAVE_CONFIG -> +#*# <---- SAVE_CONFIG ----> +#*# <------------------- SAVE_CONFIG -------------------> +#*# <---------------------- SAVE_CONFIG ----------------------> +#*# <----- SAVE_CONFIG -> +#*# <- SAVE_CONFIG -----------------> diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/non_matching_data.txt new file mode 100644 index 0000000..59b2841 --- /dev/null +++ b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/non_matching_data.txt @@ -0,0 +1,13 @@ +#*#<- SAVE_CONFIG -> +#*# <-SAVE_CONFIG -> +#*# <- SAVE_CONFIG-> +#*# <- SAVE_CONFIG -> extra +#*# SAVE_CONFIG ----> +#*# < SAVE_CONFIG > +# *# <- SAVE_CONFIG -> +<- SAVE_CONFIG -> +random text + #*# <---------------------- SAVE_CONFIG ----------------------> +#*# <---------------------- SAVE_CONFIG ----------------------> #*# +#*# <--------------------------------------------> +#*# SAVE_CONFIG diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_match_save_config_start.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_match_save_config_start.py new file mode 100644 index 0000000..3a85237 --- /dev/null +++ b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_match_save_config_start.py @@ -0,0 +1,37 @@ +# ======================================================================= # +# Copyright (C) 2024 Dominik Willner # +# # +# https://github.com/dw-0/simple-config-parser # +# # +# This file may be distributed under the terms of the GNU GPLv3 license # +# ======================================================================= # + +from pathlib import Path + +import pytest + +from src.simple_config_parser.simple_config_parser import SimpleConfigParser +from tests.utils import load_testdata_from_file + +BASE_DIR = Path(__file__).parent.joinpath("test_data") +MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt") +NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt") + + +@pytest.fixture +def parser(): + return SimpleConfigParser() + + +def test_matching_lines(parser): + """Test that all lines in the matching data file are correctly identified as save config start lines.""" + matching_lines = load_testdata_from_file(MATCHING_TEST_DATA_PATH) + for line in matching_lines: + assert parser._match_save_config_start(line) is True, f"Line should be a save config start: {line!r}" + + +def test_non_matching_lines(parser): + """Test that all lines in the non-matching data file are correctly identified as not save config start lines.""" + non_matching_lines = load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH) + for line in non_matching_lines: + assert parser._match_save_config_start(line) is False, f"Line should not be a save config start: {line!r}" diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/conftest.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/conftest.py index 7c932c6..fdd77fa 100644 --- a/kiauh/core/submodules/simple_config_parser/tests/public_api/conftest.py +++ b/kiauh/core/submodules/simple_config_parser/tests/public_api/conftest.py @@ -13,7 +13,7 @@ from src.simple_config_parser.simple_config_parser import SimpleConfigParser from tests.utils import load_testdata_from_file BASE_DIR = Path(__file__).parent.parent.joinpath("assets") -CONFIG_FILES = ["test_config_1.cfg", "test_config_2.cfg", "test_config_3.cfg"] +CONFIG_FILES = ["test_config_1.cfg", "test_config_2.cfg", "test_config_3.cfg", "test_config_4.cfg"] @pytest.fixture(params=CONFIG_FILES)