From 9f50f6fdd7161cc7abc06d4e996736762bda3457 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 1 Sep 2024 18:31:15 +0200 Subject: [PATCH 1/2] fix: y and n are invalid selections in KlipperFlashOverviewMenu (#508) Signed-off-by: Dominik Willner --- .../menus/klipper_build_menu.py | 1 + .../menus/klipper_flash_menu.py | 53 ++++++++++++------- kiauh/core/menus/__init__.py | 5 +- kiauh/core/menus/base_menu.py | 47 ++++------------ kiauh/utils/input_utils.py | 2 +- 5 files changed, 52 insertions(+), 56 deletions(-) diff --git a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py index cd92e4f..6a3ab5b 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py @@ -81,6 +81,7 @@ class KlipperBuildFirmwareMenu(BaseMenu): line = f"{COLOR_RED}Dependencies are missing!{RESET_FORMAT}" menu += f"║ {line:<62} ║\n" + menu += "╟───────────────────────────────────────────────────────╢\n" print(menu, end="") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index a32ccac..9c0c8b1 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -249,7 +249,7 @@ class KlipperSelectMcuIdMenu(BaseMenu): self.flash_options = FlashOptions() self.mcu_list = self.flash_options.mcu_list self.input_label_txt = "Select MCU to flash" - self.footer_type = FooterType.BACK_HELP + self.footer_type = FooterType.BACK def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: self.previous_menu = ( @@ -265,7 +265,7 @@ class KlipperSelectMcuIdMenu(BaseMenu): def print_menu(self) -> None: header = "!!! ATTENTION !!!" - header2 = f"[{COLOR_CYAN}List of available MCUs{RESET_FORMAT}]" + header2 = f"[{COLOR_CYAN}List of detected MCUs{RESET_FORMAT}]" color = COLOR_RED count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( @@ -277,15 +277,21 @@ class KlipperSelectMcuIdMenu(BaseMenu): ║ ONLY flash a firmware created for the respective MCU! ║ ║ ║ ╟{header2:─^64}╢ + ║ ║ """ )[1:] for i, mcu in enumerate(self.mcu_list): mcu = mcu.split("/")[-1] - menu += f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n" - menu += "╟───────────────────────────┬───────────────────────────╢" + menu += f"║ {i}) {COLOR_CYAN}{mcu:<51}{RESET_FORMAT}║\n" - print(menu, end="\n") + menu += textwrap.dedent( + """ + ║ ║ + ╟───────────────────────────────────────────────────────╢ + """ + )[1:] + print(menu, end="") def flash_mcu(self, **kwargs): try: @@ -343,8 +349,8 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu): for i, board in enumerate(self.available_boards): line = f" {i}) {board}" - menu += f"|{line:<55}|\n" - + menu += f"║{line:<55}║\n" + menu += "╟───────────────────────────────────────────────────────╢" print(menu, end="") def board_select(self, **kwargs): @@ -392,8 +398,8 @@ class KlipperFlashOverviewMenu(BaseMenu): def set_options(self) -> None: self.options = { - "Y": Option(self.execute_flash), - "N": Option(self.abort_process), + "y": Option(self.execute_flash), + "n": Option(self.abort_process), } self.default_option = Option(self.execute_flash) @@ -406,7 +412,7 @@ class KlipperFlashOverviewMenu(BaseMenu): method = self.flash_options.flash_method.value command = self.flash_options.flash_command.value conn_type = self.flash_options.connection_type.value - mcu = self.flash_options.selected_mcu + mcu = self.flash_options.selected_mcu.split("/")[-1] board = self.flash_options.selected_board baudrate = self.flash_options.selected_baudrate subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]" @@ -420,26 +426,37 @@ class KlipperFlashOverviewMenu(BaseMenu): ║ sure everything is correct, start the process. If any ║ ║ parameter needs to be changed, you can go back (B) ║ ║ step by step or abort and start from the beginning. ║ - ║{subheader:-^64}║ + ║{subheader:─^64}║ + ║ ║ """ )[1:] - menu += f" ● MCU: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n" - menu += f" ● Connection: {COLOR_CYAN}{conn_type}{RESET_FORMAT}\n" - menu += f" ● Flash method: {COLOR_CYAN}{method}{RESET_FORMAT}\n" - menu += f" ● Flash command: {COLOR_CYAN}{command}{RESET_FORMAT}\n" + menu += textwrap.dedent( + f""" + ║ MCU: {COLOR_CYAN}{mcu:<48}{RESET_FORMAT} ║ + ║ Connection: {COLOR_CYAN}{conn_type:<41}{RESET_FORMAT} ║ + ║ Flash method: {COLOR_CYAN}{method:<39}{RESET_FORMAT} ║ + ║ Flash command: {COLOR_CYAN}{command:<38}{RESET_FORMAT} ║ + """ + )[1:] if self.flash_options.flash_method is FlashMethod.SD_CARD: - menu += f" ● Board type: {COLOR_CYAN}{board}{RESET_FORMAT}\n" - menu += f" ● Baudrate: {COLOR_CYAN}{baudrate}{RESET_FORMAT}\n" + menu += textwrap.dedent( + f""" + ║ Board type: {COLOR_CYAN}{board:<41}{RESET_FORMAT} ║ + ║ Baudrate: {COLOR_CYAN}{baudrate:<43}{RESET_FORMAT} ║ + """ + )[1:] menu += textwrap.dedent( """ + ║ ║ ╟───────────────────────────────────────────────────────╢ ║ Y) Start flash process ║ ║ N) Abort - Return to Advanced Menu ║ + ╟───────────────────────────────────────────────────────╢ """ - ) + )[1:] print(menu, end="") def execute_flash(self, **kwargs): diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py index 1166127..3fdc671 100644 --- a/kiauh/core/menus/__init__.py +++ b/kiauh/core/menus/__init__.py @@ -22,7 +22,10 @@ class Option: :param opt_data: Can be used to pass any additional data to the menu option """ - method: Type[Callable] | None = None + def __repr__(self): + return f"Option(method={self.method.__name__}, opt_index={self.opt_index}, opt_data={self.opt_data})" + + method: Type[Callable] opt_index: str = "" opt_data: Any = None diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index d4eb30e..4963d93 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -25,6 +25,7 @@ from core.constants import ( ) from core.logger import Logger from core.menus import FooterType, Option +from utils.input_utils import get_selection_input def clear() -> None: @@ -141,7 +142,7 @@ class BaseMenu(metaclass=PostInitCaller): def __go_to_help(self, **kwargs) -> None: if self.help_menu is None: return - self.help_menu(previous_menu=self).run() + self.help_menu(previous_menu=self.__class__).run() def __exit(self, **kwargs) -> None: Logger.print_ok("###### Happy printing!", False) @@ -177,46 +178,20 @@ class BaseMenu(metaclass=PostInitCaller): self.print_menu() self.print_footer() - def validate_user_input(self, usr_input: str) -> Option: - """ - Validate the user input and either return an Option, a string or None - :param usr_input: The user input in form of a string - :return: Option, str or None - """ - usr_input = usr_input.lower() - option = self.options.get( - usr_input, - Option(method=None, opt_index="", opt_data=None), - ) - - # if option/usr_input is None/empty string, we execute the menus default option if specified - if (option is None or usr_input == "") and self.default_option is not None: - self.default_option.opt_index = usr_input - return self.default_option - - # user selected a regular option - option.opt_index = usr_input - return option - - def handle_user_input(self) -> Option: - """Handle the user input, return the validated input or print an error.""" - while True: - print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="") - usr_input = input().lower() - validated_input = self.validate_user_input(usr_input) - - if validated_input.method is not None: - return validated_input - else: - Logger.print_error("Invalid input!", False) - def run(self) -> None: """Start the menu lifecycle. When this function returns, the lifecycle of the menu ends.""" try: self.display_menu() - option = self.handle_user_input() - option.method(opt_index=option.opt_index, opt_data=option.opt_data) + option = get_selection_input(self.input_label_txt, self.options) + selected_option: Option = self.options.get(option) + + selected_option.method( + opt_index=selected_option.opt_index, + opt_data=selected_option.opt_data, + ) + self.run() + except Exception as e: Logger.print_error( f"An unexpected error occured:\n{e}\n{traceback.format_exc()}" diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index 3fa5783..75af939 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -137,7 +137,7 @@ def get_selection_input(question: str, option_list: List | Dict, default=None) - else: raise ValueError("Invalid option_list type") - Logger.print_error(INVALID_CHOICE) + Logger.print_error("Invalid option! Please select a valid option.", False) def format_question(question: str, default=None) -> str: From e438081c3561adbb64764b7f6fc0e62f4c9f4fc9 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 1 Sep 2024 18:51:25 +0200 Subject: [PATCH 2/2] fix: update SimpleConfigParser submodule (#510) --- .../simple_config_parser.py | 42 +++++++++++++++++-- .../line_parsing/data/case_parse_option.py | 4 ++ ...parsing.py => test_single_line_parsing.py} | 2 +- .../data/case_line_is_multiline_option.py | 10 +++++ .../data/case_line_is_option.py | 1 + 5 files changed, 55 insertions(+), 4 deletions(-) rename kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/{test_line_parsing.py => test_single_line_parsing.py} (99%) 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 b3f10c6..b2246bb 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 @@ -85,10 +85,46 @@ class DuplicateOptionError(Exception): class SimpleConfigParser: """A customized config parser targeted at handling Klipper style config files""" - _SECTION_RE = re.compile(r"\s*\[(\w+\s?.+)]\s*([#;].*)?$") - _OPTION_RE = re.compile(r"^\s*(\w+)\s*[:=]\s*([^=:].*)\s*([#;].*)?$") - _MLOPTION_RE = re.compile(r"^\s*(\w+)\s*[:=]\s*([#;].*)?$") + # definition of section line: + # - then line MUST start with an opening square bracket - it is the first section marker + # - the section marker MUST be followed by at least one character - it is the section name + # - the section name MUST be followed by a closing square bracket - it is the second section marker + # - the second section marker MAY be followed by any amount of whitespace characters + # - the second section marker MAY be followed by a # or ; - it is the comment marker + # - the inline comment MAY be of any length and character + _SECTION_RE = re.compile(r"\[(.+)]\s*([#;].*)?$") + + # definition of option line: + # - the line MUST start with a word - it is the option name + # - the option name MUST be followed by a colon or an equal sign - it is the separator + # - the separator MUST be followed by a value + # - the separator MAY have any amount of leading or trailing whitespaces + # - the separator MUST NOT be directly followed by a colon or equal sign + # - the value MAY be of any length and character + # - the value MAY contain any amount of trailing whitespaces + # - the value MAY be followed by a # or ; - it is the comment marker + # - the inline comment MAY be of any length and character + _OPTION_RE = re.compile(r"^([^:=\s]+)\s?[:=]\s*([^=:].*)\s*([#;].*)?$") + + # definition of multiline option line: + # - the line MUST start with a word - it is the option name + # - the option name MUST be followed by a colon or an equal sign - it is the separator + # - the separator MUST NOT be followed by a value + # - the separator MAY have any amount of leading or trailing whitespaces + # - the separator MUST NOT be directly followed by a colon or equal sign + # - the separator MAY be followed by a # or ; - it is the comment marker + # - the inline comment MAY be of any length and character + _MLOPTION_RE = re.compile(r"^([^:=\s]+)\s*[:=]\s*([#;].*)?$") + + # definition of comment line: + # - the line MAY start with any amount of whitespace characters + # - the line MUST contain a # or ; - it is the comment marker + # - the comment marker MAY be followed by any amount of whitespace characters + # - the comment MAY be of any length and character _COMMENT_RE = re.compile(r"^\s*([#;].*)?$") + + # definition of empty line: + # - the line MUST contain only whitespace characters _EMPTY_LINE_RE = re.compile(r"^\s*$") BOOLEAN_STATES = { diff --git a/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_option.py b/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_option.py index fbe9001..df849be 100644 --- a/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_option.py +++ b/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_option.py @@ -21,4 +21,8 @@ testcases = [ "serial", "/dev/serial/by-id/", ), + ("parameter_temperature_(°C): 155", "parameter_temperature_(°C)", "155"), + ("parameter_humidity_(%_RH): 45", "parameter_humidity_(%_RH)", "45"), + ("parameter_spool_weight_(%): 10", "parameter_spool_weight_(%)", "10"), + ("path: /dev/shm/drying_box.json", "path", "/dev/shm/drying_box.json"), ] diff --git a/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/test_line_parsing.py b/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/test_single_line_parsing.py similarity index 99% rename from kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/test_line_parsing.py rename to kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/test_single_line_parsing.py index 96366e3..fd2e4a1 100644 --- a/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/test_line_parsing.py +++ b/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/test_single_line_parsing.py @@ -14,7 +14,7 @@ def parser(): return SimpleConfigParser() -class TestLineParsing: +class TestSingleLineParsing: @pytest.mark.parametrize("given, expected", [*case_parse_section]) def test_parse_section(self, parser, given, expected): parser._parse_section(given) diff --git a/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_multiline_option.py b/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_multiline_option.py index ed93f87..ddf74d5 100644 --- a/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_multiline_option.py +++ b/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_multiline_option.py @@ -14,4 +14,14 @@ testcases = [ ("", False), ("# that's a comment", False), ("; that's a comment", False), + ("parameter_humidity_(%_RH):", True), + ("parameter_spool_weight_(%):", True), + ("parameter_temperature_(°C):", True), + ("parameter_humidity_(%_RH): 18.123", False), + ("parameter_spool_weight_(%): 150", False), + ("parameter_temperature_(°C): 30,5", False), + ("trusted_clients:", True), + ("trusted_clients: 192.168.1.0/24", False), + ("cors_domains:", True), + ("cors_domains: http://*.lan", False), ] diff --git a/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_option.py b/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_option.py index 280e851..b33e6a8 100644 --- a/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_option.py +++ b/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_option.py @@ -26,5 +26,6 @@ testcases = [ ("description: homing!", True), ("description: inline macro :-)", True), ("path: %GCODES_DIR%", True), + ("path: /dev/shm/drying_box.json", True), ("serial = /dev/serial/by-id/", True), ]