From 7b506ba3807c72e2fb8cd019df439c4c5a7a7626 Mon Sep 17 00:00:00 2001 From: arzzen Date: Tue, 17 Jun 2025 19:46:22 +0200 Subject: [PATCH 1/3] Add colorless menu --- README.md | 6 +- git_py_stats/menu.py | 9 ++- git_py_stats/tests/test_menu.py | 109 ++++---------------------------- 3 files changed, 23 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 006c42f..721123b 100644 --- a/README.md +++ b/README.md @@ -309,11 +309,13 @@ export _GIT_BRANCH="master" ### Color Themes -You can change to the legacy color scheme by toggling the variable -`_MENU_THEME` between `default` and `legacy` +You can change to the legacy color scheme by toggling the variable `_MENU_THEME` between `default` and `legacy`. +You can completely disable the color theme by setting the `_MENU_THEME` variable to `none`. ```bash export _MENU_THEME="legacy" +# or +export _MENU_THEME="none" ``` ## Contributing diff --git a/git_py_stats/menu.py b/git_py_stats/menu.py index 59e39b4..7bd421b 100644 --- a/git_py_stats/menu.py +++ b/git_py_stats/menu.py @@ -24,7 +24,7 @@ def interactive_menu(config: Dict[str, Union[str, int]]) -> str: WHITE = "\033[37m" CYAN = "\033[36m" - # Handle default and legacy menu + # Handle default, legacy, and colorless menu theme = config.get("menu_theme", "") if theme == "legacy": @@ -33,6 +33,13 @@ def interactive_menu(config: Dict[str, Union[str, int]]) -> str: NUMS = f"{BOLD}{YELLOW}" HELP_TXT = f"{NORMAL}{YELLOW}" EXIT_TXT = f"{BOLD}{RED}" + elif theme == "none": + TITLES = BOLD + TEXT = "" + NUMS = BOLD + HELP_TXT = "" + EXIT_TXT = BOLD + NORMAL = "" else: TITLES = f"{BOLD}{CYAN}" TEXT = f"{NORMAL}{WHITE}" diff --git a/git_py_stats/tests/test_menu.py b/git_py_stats/tests/test_menu.py index 4e41b08..401f8fc 100644 --- a/git_py_stats/tests/test_menu.py +++ b/git_py_stats/tests/test_menu.py @@ -25,6 +25,7 @@ def setUp(self): # Mock configurations for testing self.config_default = {} # Default theme self.config_legacy = {"menu_theme": "legacy"} # Legacy theme + self.config_none = {"menu_theme": "none"} # Alternate colorless theme alias @patch("builtins.input", return_value="1") @patch("sys.stdout", new_callable=StringIO) @@ -88,107 +89,19 @@ def test_legacy_theme_option_1(self, mock_stdout, mock_input): self.assertIn("Generate:", output) self.assertIn("1) Contribution stats (by author)", output) - @patch("builtins.input", return_value="2") + @patch("builtins.input", return_value="3") @patch("sys.stdout", new_callable=StringIO) - def test_legacy_theme_option_2(self, mock_stdout, mock_input): + def test_none_theme_option_3(self, mock_stdout, mock_input): """ - Test the interactive_menu with legacy theme and user selects option '2'. + Test the interactive_menu with 'none' theme (alias for colorless) and user selects option '3'. """ - choice = interactive_menu(self.config_legacy) - self.assertEqual(choice, "2") - output = strip_ansi_codes(mock_stdout.getvalue()) - self.assertIn("2) Contribution stats (by author) on a specific branch", output) - - @patch("builtins.input", return_value="") - @patch("sys.stdout", new_callable=StringIO) - def test_legacy_theme_exit(self, mock_stdout, mock_input): - """ - Test the interactive_menu with legacy theme and user presses Enter to exit. - """ - choice = interactive_menu(self.config_legacy) - self.assertEqual(choice, "") - output = strip_ansi_codes(mock_stdout.getvalue()) - self.assertIn("press Enter to exit", output) - - @patch("builtins.input", return_value="invalid") - @patch("sys.stdout", new_callable=StringIO) - def test_legacy_theme_invalid_input(self, mock_stdout, mock_input): - """ - Test the interactive_menu with legacy theme and user enters an invalid option. - """ - choice = interactive_menu(self.config_legacy) - self.assertEqual(choice, "invalid") - output = strip_ansi_codes(mock_stdout.getvalue()) - self.assertIn("Generate:", output) - - @patch("builtins.input", side_effect=["1", ""]) - @patch("sys.stdout", new_callable=StringIO) - def test_multiple_inputs(self, mock_stdout, mock_input): - """ - Test the interactive_menu with multiple inputs in sequence. - """ - choice1 = interactive_menu(self.config_default) - choice2 = interactive_menu(self.config_default) - self.assertEqual(choice1, "1") - self.assertEqual(choice2, "") - output = strip_ansi_codes(mock_stdout.getvalue()) - self.assertIn("Generate:", output) - - @patch("builtins.input", return_value=" 5 ") - @patch("sys.stdout", new_callable=StringIO) - def test_input_with_whitespace(self, mock_stdout, mock_input): - """ - Test the interactive_menu with input that includes leading/trailing whitespace. - """ - choice = interactive_menu(self.config_default) - self.assertEqual(choice, "5") - output = strip_ansi_codes(mock_stdout.getvalue()) - self.assertIn("5) My daily status", output) - - @patch("builtins.input", return_value="QUIT") - @patch("sys.stdout", new_callable=StringIO) - def test_input_quit(self, mock_stdout, mock_input): - """ - Test the interactive_menu with input 'QUIT' to simulate exit. - """ - choice = interactive_menu(self.config_default) - self.assertEqual(choice, "QUIT") - output = strip_ansi_codes(mock_stdout.getvalue()) - self.assertIn("Generate:", output) - - @patch("builtins.input", return_value="EXIT") - @patch("sys.stdout", new_callable=StringIO) - def test_input_exit(self, mock_stdout, mock_input): - """ - Test the interactive_menu with input 'EXIT' to simulate exit. - """ - choice = interactive_menu(self.config_default) - self.assertEqual(choice, "EXIT") - output = strip_ansi_codes(mock_stdout.getvalue()) - self.assertIn("Generate:", output) - - @patch("builtins.input", return_value=" ") - @patch("sys.stdout", new_callable=StringIO) - def test_input_only_whitespace(self, mock_stdout, mock_input): - """ - Test the interactive_menu with input that is only whitespace. - """ - choice = interactive_menu(self.config_default) - self.assertEqual(choice, "") - output = strip_ansi_codes(mock_stdout.getvalue()) - self.assertIn("press Enter to exit", output) - - @patch("builtins.input", side_effect=KeyboardInterrupt) - @patch("sys.stdout", new_callable=StringIO) - def test_keyboard_interrupt(self, mock_stdout, mock_input): - """ - Test the interactive_menu handles KeyboardInterrupt (Ctrl+C). - """ - with self.assertRaises(KeyboardInterrupt): - interactive_menu(self.config_default) - output = strip_ansi_codes(mock_stdout.getvalue()) - self.assertIn("Generate:", output) - + choice = interactive_menu(self.config_none) + self.assertEqual(choice, "3") + output = mock_stdout.getvalue() + self.assertNotIn("\033[31m", output) # No RED + self.assertNotIn("\033[33m", output) # No YELLOW + self.assertNotIn("\033[36m", output) # No CYAN + self.assertIn("\033[1m", output) # BOLD allowed if __name__ == "__main__": unittest.main() From 228185eb1e227be165e3f3ceda177111c62c1ff8 Mon Sep 17 00:00:00 2001 From: arzzen Date: Tue, 17 Jun 2025 19:49:16 +0200 Subject: [PATCH 2/3] tests --- git_py_stats/tests/test_menu.py | 156 +++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 4 deletions(-) diff --git a/git_py_stats/tests/test_menu.py b/git_py_stats/tests/test_menu.py index 401f8fc..558b4c7 100644 --- a/git_py_stats/tests/test_menu.py +++ b/git_py_stats/tests/test_menu.py @@ -98,10 +98,158 @@ def test_none_theme_option_3(self, mock_stdout, mock_input): choice = interactive_menu(self.config_none) self.assertEqual(choice, "3") output = mock_stdout.getvalue() - self.assertNotIn("\033[31m", output) # No RED - self.assertNotIn("\033[33m", output) # No YELLOW - self.assertNotIn("\033[36m", output) # No CYAN - self.assertIn("\033[1m", output) # BOLD allowed + self.assertNotIn("\033[31m", output) + self.assertNotIn("\033[33m", output) + self.assertNotIn("\033[36m", output) + self.assertIn("\033[1m", output) + + @patch("builtins.input", return_value="22") + @patch("sys.stdout", new_callable=StringIO) + def test_default_theme_option_22(self, mock_stdout, mock_input): + """ + Test the interactive_menu with default theme and user selects option '22'. + """ + choice = interactive_menu(self.config_default) + self.assertEqual(choice, "22") + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("Suggest:", output) + self.assertIn("22) Code reviewers (based on git history)", output) + + @patch("builtins.input", return_value="") + @patch("sys.stdout", new_callable=StringIO) + def test_default_theme_exit(self, mock_stdout, mock_input): + """ + Test the interactive_menu with default theme and user presses Enter to exit. + """ + choice = interactive_menu(self.config_default) + self.assertEqual(choice, "") + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("press Enter to exit", output) + + @patch("builtins.input", return_value="invalid") + @patch("sys.stdout", new_callable=StringIO) + def test_default_theme_invalid_input(self, mock_stdout, mock_input): + """ + Test the interactive_menu with default theme and user enters an invalid option. + """ + choice = interactive_menu(self.config_default) + self.assertEqual(choice, "invalid") + # Since interactive_menu doesn't print 'Invalid selection', we don't assert that here. + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("Generate:", output) + + @patch("builtins.input", return_value="1") + @patch("sys.stdout", new_callable=StringIO) + def test_legacy_theme_option_1(self, mock_stdout, mock_input): + """ + Test the interactive_menu with legacy theme and user selects option '1'. + """ + choice = interactive_menu(self.config_legacy) + self.assertEqual(choice, "1") + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("Generate:", output) + self.assertIn("1) Contribution stats (by author)", output) + + @patch("builtins.input", return_value="2") + @patch("sys.stdout", new_callable=StringIO) + def test_legacy_theme_option_2(self, mock_stdout, mock_input): + """ + Test the interactive_menu with legacy theme and user selects option '2'. + """ + choice = interactive_menu(self.config_legacy) + self.assertEqual(choice, "2") + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("2) Contribution stats (by author) on a specific branch", output) + + @patch("builtins.input", return_value="") + @patch("sys.stdout", new_callable=StringIO) + def test_legacy_theme_exit(self, mock_stdout, mock_input): + """ + Test the interactive_menu with legacy theme and user presses Enter to exit. + """ + choice = interactive_menu(self.config_legacy) + self.assertEqual(choice, "") + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("press Enter to exit", output) + + @patch("builtins.input", return_value="invalid") + @patch("sys.stdout", new_callable=StringIO) + def test_legacy_theme_invalid_input(self, mock_stdout, mock_input): + """ + Test the interactive_menu with legacy theme and user enters an invalid option. + """ + choice = interactive_menu(self.config_legacy) + self.assertEqual(choice, "invalid") + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("Generate:", output) + + @patch("builtins.input", side_effect=["1", ""]) + @patch("sys.stdout", new_callable=StringIO) + def test_multiple_inputs(self, mock_stdout, mock_input): + """ + Test the interactive_menu with multiple inputs in sequence. + """ + choice1 = interactive_menu(self.config_default) + choice2 = interactive_menu(self.config_default) + self.assertEqual(choice1, "1") + self.assertEqual(choice2, "") + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("Generate:", output) + + @patch("builtins.input", return_value=" 5 ") + @patch("sys.stdout", new_callable=StringIO) + def test_input_with_whitespace(self, mock_stdout, mock_input): + """ + Test the interactive_menu with input that includes leading/trailing whitespace. + """ + choice = interactive_menu(self.config_default) + self.assertEqual(choice, "5") + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("5) My daily status", output) + + @patch("builtins.input", return_value="QUIT") + @patch("sys.stdout", new_callable=StringIO) + def test_input_quit(self, mock_stdout, mock_input): + """ + Test the interactive_menu with input 'QUIT' to simulate exit. + """ + choice = interactive_menu(self.config_default) + self.assertEqual(choice, "QUIT") + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("Generate:", output) + + @patch("builtins.input", return_value="EXIT") + @patch("sys.stdout", new_callable=StringIO) + def test_input_exit(self, mock_stdout, mock_input): + """ + Test the interactive_menu with input 'EXIT' to simulate exit. + """ + choice = interactive_menu(self.config_default) + self.assertEqual(choice, "EXIT") + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("Generate:", output) + + @patch("builtins.input", return_value=" ") + @patch("sys.stdout", new_callable=StringIO) + def test_input_only_whitespace(self, mock_stdout, mock_input): + """ + Test the interactive_menu with input that is only whitespace. + """ + choice = interactive_menu(self.config_default) + self.assertEqual(choice, "") + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("press Enter to exit", output) + + @patch("builtins.input", side_effect=KeyboardInterrupt) + @patch("sys.stdout", new_callable=StringIO) + def test_keyboard_interrupt(self, mock_stdout, mock_input): + """ + Test the interactive_menu handles KeyboardInterrupt (Ctrl+C). + """ + with self.assertRaises(KeyboardInterrupt): + interactive_menu(self.config_default) + output = strip_ansi_codes(mock_stdout.getvalue()) + self.assertIn("Generate:", output) if __name__ == "__main__": unittest.main() From f4718e04de718e1d39389927a37ae2d8168a58fb Mon Sep 17 00:00:00 2001 From: arzzen Date: Thu, 19 Jun 2025 18:02:58 +0200 Subject: [PATCH 3/3] fix menu config & test case --- git_py_stats/config.py | 2 ++ git_py_stats/menu.py | 2 +- git_py_stats/tests/test_menu.py | 50 +++++++++++++++------------------ 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/git_py_stats/config.py b/git_py_stats/config.py index 97fbd83..8c05c73 100644 --- a/git_py_stats/config.py +++ b/git_py_stats/config.py @@ -120,6 +120,8 @@ def get_config() -> Dict[str, Union[str, int]]: menu_theme: Optional[str] = os.environ.get("_MENU_THEME") if menu_theme == "legacy": config["menu_theme"] = "legacy" + elif menu_theme == "none": + config["menu_theme"] = "none" else: config["menu_theme"] = "" diff --git a/git_py_stats/menu.py b/git_py_stats/menu.py index 7bd421b..30e50e8 100644 --- a/git_py_stats/menu.py +++ b/git_py_stats/menu.py @@ -39,7 +39,7 @@ def interactive_menu(config: Dict[str, Union[str, int]]) -> str: NUMS = BOLD HELP_TXT = "" EXIT_TXT = BOLD - NORMAL = "" + NORMAL = "" else: TITLES = f"{BOLD}{CYAN}" TEXT = f"{NORMAL}{WHITE}" diff --git a/git_py_stats/tests/test_menu.py b/git_py_stats/tests/test_menu.py index 558b4c7..ed0beec 100644 --- a/git_py_stats/tests/test_menu.py +++ b/git_py_stats/tests/test_menu.py @@ -27,6 +27,7 @@ def setUp(self): self.config_legacy = {"menu_theme": "legacy"} # Legacy theme self.config_none = {"menu_theme": "none"} # Alternate colorless theme alias + # Test cases for default theme @patch("builtins.input", return_value="1") @patch("sys.stdout", new_callable=StringIO) def test_default_theme_option_1(self, mock_stdout, mock_input): @@ -77,23 +78,11 @@ def test_default_theme_invalid_input(self, mock_stdout, mock_input): output = strip_ansi_codes(mock_stdout.getvalue()) self.assertIn("Generate:", output) - @patch("builtins.input", return_value="1") - @patch("sys.stdout", new_callable=StringIO) - def test_legacy_theme_option_1(self, mock_stdout, mock_input): - """ - Test the interactive_menu with legacy theme and user selects option '1'. - """ - choice = interactive_menu(self.config_legacy) - self.assertEqual(choice, "1") - output = strip_ansi_codes(mock_stdout.getvalue()) - self.assertIn("Generate:", output) - self.assertIn("1) Contribution stats (by author)", output) - @patch("builtins.input", return_value="3") @patch("sys.stdout", new_callable=StringIO) def test_none_theme_option_3(self, mock_stdout, mock_input): """ - Test the interactive_menu with 'none' theme (alias for colorless) and user selects option '3'. + Test the interactive_menu with 'none' theme and user selects option '3'. """ choice = interactive_menu(self.config_none) self.assertEqual(choice, "3") @@ -103,41 +92,43 @@ def test_none_theme_option_3(self, mock_stdout, mock_input): self.assertNotIn("\033[36m", output) self.assertIn("\033[1m", output) - @patch("builtins.input", return_value="22") + @patch("builtins.input", return_value="1") @patch("sys.stdout", new_callable=StringIO) - def test_default_theme_option_22(self, mock_stdout, mock_input): + def test_none_theme_option_1(self, mock_stdout, mock_input): """ - Test the interactive_menu with default theme and user selects option '22'. + Test the interactive_menu with 'none' theme and user selects option '1'. """ - choice = interactive_menu(self.config_default) - self.assertEqual(choice, "22") - output = strip_ansi_codes(mock_stdout.getvalue()) - self.assertIn("Suggest:", output) - self.assertIn("22) Code reviewers (based on git history)", output) + choice = interactive_menu(self.config_none) + self.assertEqual(choice, "1") + output = mock_stdout.getvalue() + self.assertNotIn("\033[31m", output) + self.assertNotIn("\033[33m", output) + self.assertNotIn("\033[36m", output) + self.assertIn("\033[1m", output) @patch("builtins.input", return_value="") @patch("sys.stdout", new_callable=StringIO) - def test_default_theme_exit(self, mock_stdout, mock_input): + def test_none_theme_exit(self, mock_stdout, mock_input): """ - Test the interactive_menu with default theme and user presses Enter to exit. + Test the interactive_menu with none theme and user presses Enter to exit. """ - choice = interactive_menu(self.config_default) + choice = interactive_menu(self.config_none) self.assertEqual(choice, "") output = strip_ansi_codes(mock_stdout.getvalue()) self.assertIn("press Enter to exit", output) @patch("builtins.input", return_value="invalid") @patch("sys.stdout", new_callable=StringIO) - def test_default_theme_invalid_input(self, mock_stdout, mock_input): + def test_none_theme_invalid_input(self, mock_stdout, mock_input): """ - Test the interactive_menu with default theme and user enters an invalid option. + Test the interactive_menu with none theme and user enters an invalid option. """ - choice = interactive_menu(self.config_default) + choice = interactive_menu(self.config_none) self.assertEqual(choice, "invalid") - # Since interactive_menu doesn't print 'Invalid selection', we don't assert that here. output = strip_ansi_codes(mock_stdout.getvalue()) self.assertIn("Generate:", output) + # Test cases for legacy theme @patch("builtins.input", return_value="1") @patch("sys.stdout", new_callable=StringIO) def test_legacy_theme_option_1(self, mock_stdout, mock_input): @@ -183,6 +174,7 @@ def test_legacy_theme_invalid_input(self, mock_stdout, mock_input): output = strip_ansi_codes(mock_stdout.getvalue()) self.assertIn("Generate:", output) + # Test cases for handling multiple inputs and edge cases @patch("builtins.input", side_effect=["1", ""]) @patch("sys.stdout", new_callable=StringIO) def test_multiple_inputs(self, mock_stdout, mock_input): @@ -207,6 +199,7 @@ def test_input_with_whitespace(self, mock_stdout, mock_input): output = strip_ansi_codes(mock_stdout.getvalue()) self.assertIn("5) My daily status", output) + # Test cases for handling exit commands @patch("builtins.input", return_value="QUIT") @patch("sys.stdout", new_callable=StringIO) def test_input_quit(self, mock_stdout, mock_input): @@ -251,5 +244,6 @@ def test_keyboard_interrupt(self, mock_stdout, mock_input): output = strip_ansi_codes(mock_stdout.getvalue()) self.assertIn("Generate:", output) + if __name__ == "__main__": unittest.main()