mirror of
https://github.com/Pythagora-io/gpt-pilot.git
synced 2026-01-09 13:17:55 -05:00
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:
14
README.md
14
README.md
@@ -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
|
||||
```
|
||||
|
||||

|
||||
```bash
|
||||
python main.py theme=dark
|
||||
```
|
||||
- Dark mode.
|
||||

|
||||
|
||||
|
||||
## `delete_unrelated_steps`
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user