feat: add theme management and testing

- Implement theme setting and dynamic color function generation.
- Introduce tests to validate color rendering in console.
This commit is contained in:
Umpire2018
2023-10-15 14:54:37 +08:00
parent fea7d2e78d
commit e79a72601b
5 changed files with 191 additions and 97 deletions

View File

@@ -170,8 +170,18 @@ Erase all development steps previously done and continue working on an existing
python main.py app_id=<ID_OF_THE_APP> 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`

View File

@@ -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")

View File

@@ -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())

View File

@@ -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():

View File

@@ -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)