From e79a72601b8068695de8214ee555395c0b3e3f77 Mon Sep 17 00:00:00 2001 From: Umpire2018 Date: Sun, 15 Oct 2023 14:54:37 +0800 Subject: [PATCH] feat: add theme management and testing - Implement theme setting and dynamic color function generation. - Introduce tests to validate color rendering in console. --- README.md | 14 +++- pilot/test/test_colors.py | 81 +++++++++++-------- pilot/utils/arguments.py | 6 +- pilot/utils/questionary.py | 25 ++---- pilot/utils/style.py | 162 ++++++++++++++++++++++++++++--------- 5 files changed, 191 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index da370a61..58f3fabe 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,18 @@ Erase all development steps previously done and continue working on an existing python main.py app_id= skip_until_dev_step=0 ``` -## `no-color` -Disable color ouput in terminal. +## `theme` +```bash +python main.py theme=light +``` + +![屏幕截图 2023-10-15 103907](https://github.com/Pythagora-io/gpt-pilot/assets/138990495/c3d08f21-7e3b-4ee4-981f-281d1c97149e) +```bash +python main.py theme=dark +``` +- Dark mode. +![屏幕截图 2023-10-15 104120](https://github.com/Pythagora-io/gpt-pilot/assets/138990495/942cd1c9-b774-498e-b72a-677b01be1ac3) + ## `delete_unrelated_steps` diff --git a/pilot/test/test_colors.py b/pilot/test/test_colors.py index 2b8ced67..92db31b5 100644 --- a/pilot/test/test_colors.py +++ b/pilot/test/test_colors.py @@ -1,41 +1,54 @@ -import pytest -from pilot.utils.style import Config, ColorName, color_text - -# Parameters for parametrized tests -colors = [ColorName.RED, ColorName.GREEN, ColorName.YELLOW, ColorName.BLUE, ColorName.CYAN, ColorName.WHITE] -bold_options = [True, False] -no_color_options = [True, False] +import unittest +from pilot.utils.style import style_config, Theme, ColorName, get_color_function -@pytest.fixture(params=no_color_options, ids=['no_color', 'color']) -def manage_no_color(request): - original_no_color = Config.no_color - Config.no_color = request.param - yield # This is where the test function will run. - Config.no_color = original_no_color # Restore original state after the test. +class TestColorStyle(unittest.TestCase): + def test_initialization(self): + print("\n[INFO] Testing Theme Initialization...") + style_config.set_theme(Theme.DARK) + print(f"[INFO] Set theme to: {Theme.DARK}, Current theme: {style_config.theme}") + self.assertEqual(style_config.theme, Theme.DARK) + style_config.set_theme(Theme.LIGHT) + print(f"[INFO] Set theme to: {Theme.LIGHT}, Current theme: {style_config.theme}") + self.assertEqual(style_config.theme, Theme.LIGHT) -@pytest.mark.parametrize("color", colors, ids=[c.name for c in colors]) -@pytest.mark.parametrize("bold", bold_options, ids=['bold', 'not_bold']) -def test_color_text(manage_no_color, color, bold): - """ - Test the function color_text by checking the behavior with various color and bold options, - while considering the global no_color flag. - """ - colored_text = color_text("test", color, bold) + def test_color_function(self): + dark_color_codes = { + ColorName.RED: "\x1b[31m", + ColorName.GREEN: "\x1b[32m", + # ... other colors + } + light_color_codes = { + ColorName.RED: "\x1b[91m", + ColorName.GREEN: "\x1b[92m", + # ... other colors + } - print( - f"Visual Check - expect {'color (' + color.name + ')' if not Config.no_color else 'no color'}: {colored_text}") + # Test DARK theme + print("\n[INFO] Testing DARK Theme Colors...") + style_config.set_theme(Theme.DARK) + for color_name, code in dark_color_codes.items(): + with self.subTest(color=color_name): + color_func = get_color_function(color_name, bold=False) + print(f"[INFO] Testing color: {color_name}, Expect: {code}Test, Got: {color_func('Test')}") + self.assertEqual(color_func("Test"), f"{code}Test") - # Check: if no_color is True, there should be no ANSI codes in the string. - if Config.no_color: - assert colored_text == "test" - else: - # Ensure the ANSI codes for color and (if applicable) bold styling are present in the string. - assert color.value in colored_text - if bold: - # Check for the ANSI code for bold styling. - assert "\x1b[1m" in colored_text - # Ensure the string ends with the original text. - assert colored_text.endswith("test") + color_func = get_color_function(color_name, bold=True) + print( + f"[INFO] Testing color (bold): {color_name}, Expect: {code}\x1b[1mTest, Got: {color_func('Test')}") + self.assertEqual(color_func("Test"), f"{code}\x1b[1mTest") + # Test LIGHT theme + print("\n[INFO] Testing LIGHT Theme Colors...") + style_config.set_theme(Theme.LIGHT) + for color_name, code in light_color_codes.items(): + with self.subTest(color=color_name): + color_func = get_color_function(color_name, bold=False) + print(f"[INFO] Testing color: {color_name}, Expect: {code}Test, Got: {color_func('Test')}") + self.assertEqual(color_func("Test"), f"{code}Test") + + color_func = get_color_function(color_name, bold=True) + print( + f"[INFO] Testing color (bold): {color_name}, Expect: {code}\x1b[1mTest, Got: {color_func('Test')}") + self.assertEqual(color_func("Test"), f"{code}\x1b[1mTest") diff --git a/pilot/utils/arguments.py b/pilot/utils/arguments.py index a6d9dd57..539f590d 100644 --- a/pilot/utils/arguments.py +++ b/pilot/utils/arguments.py @@ -5,7 +5,7 @@ import sys import uuid from getpass import getuser from database.database import get_app, get_app_by_user_workspace -from utils.style import color_green_bold, disable_color_output +from utils.style import color_green_bold, style_config from utils.utils import should_execute_step from const.common import STEPS @@ -26,8 +26,8 @@ def get_arguments(): else: arguments[arg] = True - if 'no-color' in arguments: - disable_color_output() + theme_mapping = {'light': style_config.theme.LIGHT, 'dark': style_config.theme.DARK} + style_config.set_theme(theme=theme_mapping.get(arguments['theme'], style_config.theme.DARK)) if 'user_id' not in arguments: arguments['user_id'] = username_to_uuid(getuser()) diff --git a/pilot/utils/questionary.py b/pilot/utils/questionary.py index 7fc2f3af..6c8cab68 100644 --- a/pilot/utils/questionary.py +++ b/pilot/utils/questionary.py @@ -1,18 +1,9 @@ import platform import questionary -from utils.style import color_yellow_bold import re import sys -from prompt_toolkit.styles import Style from database.database import save_user_input, get_saved_user_input - -custom_style = Style.from_dict({ - 'question': '#FFFFFF bold', # the color and style of the question - 'answer': '#FF910A bold', # the color and style of the answer - 'pointer': '#FF4500 bold', # the color and style of the selection pointer - 'highlighted': '#63CD91 bold', # the color and style of the highlighted choice - 'instruction': '#FFFF00 bold' # the color and style of the question mark -}) +from utils.style import color_yellow_bold, style_config def remove_ansi_codes(s: str) -> str: @@ -21,7 +12,7 @@ def remove_ansi_codes(s: str) -> str: def styled_select(*args, **kwargs): - kwargs["style"] = custom_style + kwargs["style"] = style_config.get_style() return questionary.select(*args, **kwargs).unsafe_ask() # .ask() is included here @@ -37,12 +28,10 @@ def styled_text(project, question, ignore_user_input_count=False, style=None): return user_input.user_input if project.ipc_client_instance is None or project.ipc_client_instance.client is None: - config = { - 'style': style if style is not None else custom_style, - } + used_style = style if style is not None else style_config.get_style() question = remove_ansi_codes(question) # Colorama and questionary are not compatible and styling doesn't work flush_input() - response = questionary.text(question, **config).unsafe_ask() # .ask() is included here + response = questionary.text(question, style=used_style).unsafe_ask() # .ask() is included here else: response = print(question, type='user_input_request') print(response) @@ -55,11 +44,9 @@ def styled_text(project, question, ignore_user_input_count=False, style=None): def get_user_feedback(): - config = { - 'style': custom_style, - } return questionary.text('How did GPT Pilot do? Were you able to create any app that works? ' - 'Please write any feedback you have or just press ENTER to exit: ', **config).unsafe_ask() + 'Please write any feedback you have or just press ENTER to exit: ', + style=style_config.get_style()).unsafe_ask() def flush_input(): diff --git a/pilot/utils/style.py b/pilot/utils/style.py index 72649ea3..569234d5 100644 --- a/pilot/utils/style.py +++ b/pilot/utils/style.py @@ -1,65 +1,149 @@ +from colorama import Fore, Style as ColoramaStyle, init from enum import Enum -from colorama import Fore, Style, init +from questionary import Style -# Initialize colorama +# Initialize colorama. Ensures that ANSI codes work on Windows systems. init(autoreset=True) -class Config: - no_color: bool = False - - -def disable_color_output(): - Config.no_color = True +class Theme(Enum): + """ + Enum representing themes, which can be either DARK or LIGHT. + """ + DARK = 'dark' + LIGHT = 'light' class ColorName(Enum): - RED = Fore.RED - GREEN = Fore.GREEN - YELLOW = Fore.YELLOW - BLUE = Fore.BLUE - CYAN = Fore.CYAN - WHITE = Fore.WHITE - - -def color_text(text: str, color_name: ColorName, bold: bool = False) -> str: """ - Returns text with a specified color and optional style. - - Args: - text (str): The text to colorize. - color_name (ColorName): The color of the text. Should be a member of the ColorName enum. - bold (bool, optional): If True, the text will be displayed in bold. Defaults to False. - - Returns: - str: The text with applied color and optional style. + Enum representing color names and their corresponding ANSI color codes. + Each color has a normal and a light version, indicated by the two elements in the tuple. """ - if Config.no_color: - return text + RED = (Fore.RED, Fore.LIGHTRED_EX) + GREEN = (Fore.GREEN, Fore.LIGHTGREEN_EX) + YELLOW = (Fore.YELLOW, Fore.LIGHTYELLOW_EX) + BLUE = (Fore.BLUE, Fore.LIGHTBLUE_EX) + CYAN = (Fore.CYAN, Fore.LIGHTCYAN_EX) + WHITE = (Fore.WHITE, Fore.LIGHTWHITE_EX) - color = color_name.value - style = Style.BRIGHT if bold else "" - return f'{color}{style}{text}' + +class ThemeStyle: + """ + Class that provides style configurations for DARK and LIGHT themes. + """ + # Style configurations for DARK theme + DARK_STYLE = Style.from_dict({ + 'question': '#FFFFFF bold', # the color and style of the question - White + 'answer': '#FF910A bold', # the color and style of the answer - Dark Orange / Pumpkin + 'pointer': '#FF4500 bold', # the color and style of the pointer - Orange Red + 'highlighted': '#63CD91 bold', # the color and style of the highlighted option - Medium Aquamarine + 'instruction': '#FFFF00 bold' # the color and style of the instruction - Yellow + }) + + # Style configurations for LIGHT theme + LIGHT_STYLE = Style.from_dict({ + 'question': '#000000 bold', # the color and style of the question - Black + 'answer': '#FFB74D bold', # the color and style of the answer - Light Orange + 'pointer': '#FF7043 bold', # the color and style of the pointer - Light Red + 'highlighted': '#AED581 bold', # the color and style of the highlighted option - Light Green + 'instruction': '#757575 bold' # the color and style of the instruction - Grey + }) + + def __init__(self, theme): + """ + Initializes a ThemeStyle instance. + + Args: + theme (Theme): An enum member indicating the theme to use. + """ + self.theme = theme + + def get_style(self): + """ + Returns the Style configuration for the current theme. + + Returns: + questionary.Style: The Style instance for the current theme. + """ + return self.DARK_STYLE if self.theme == Theme.DARK else self.LIGHT_STYLE + + +class StyleConfig: + """ + Class to manage the application's style and color configurations. + """ + def __init__(self, theme: Theme = Theme.DARK): + """ + Initializes a StyleConfig instance. + + Args: + theme (Theme, optional): The initial theme to use. Defaults to Theme.DARK. + """ + self.theme_style = ThemeStyle(theme) + self.theme = theme + + def get_style(self): + """ + Retrieves the Style configuration from the theme_style instance. + + Returns: + questionary.Style: The Style configuration. + """ + return self.theme_style.get_style() + + def get_color(self, color_name: ColorName): + """ + Retrieves the ANSI color code for the provided color_name, taking into account the current theme. + + Args: + color_name (ColorName): Enum member indicating the desired color. + + Returns: + str: The ANSI color code. + """ + return color_name.value[self.theme == Theme.LIGHT] + + def set_theme(self, theme: Theme): + """ + Updates the theme of both the StyleConfig and its theme_style instance. + + Args: + theme (Theme): Enum member indicating the new theme. + """ + self.theme = theme + self.theme_style.theme = theme def get_color_function(color_name: ColorName, bold: bool = False): """ - Generate and return a function that colorizes input text with the specified color and style. + Returns a function that colorizes text using the provided color_name and optionally makes it bold. - Parameters: - color_name (ColorName): Enum member specifying the text color. - bold (bool, optional): If True, generated function will produce bold text. Defaults to False. + Args: + color_name (ColorName): Enum member indicating the color to use. + bold (bool, optional): If True, the returned function will bold text. Defaults to False. Returns: - Callable[[str], str]: A function that takes a string input and returns it colorized. + Callable[[str], str]: A function that takes a string and returns it colorized. """ def color_func(text: str) -> str: - if Config.no_color: - return text - return color_text(text, color_name, bold) + """ + Colorizes the input text using the color and boldness provided when `get_color_function` was called. + + Args: + text (str): The text to colorize. + + Returns: + str: The colorized text. + """ + color = style_config.get_color(color_name) + style = ColoramaStyle.BRIGHT if bold else "" + return f'{color}{style}{text}' + return color_func +style_config = StyleConfig() + # Dynamically generate color functions color_red = get_color_function(ColorName.RED) color_red_bold = get_color_function(ColorName.RED, True)