Skip to content

Commit

Permalink
SCons: Improve colored output
Browse files Browse the repository at this point in the history
  • Loading branch information
Repiteo committed Oct 18, 2024
1 parent 4631a61 commit 9d02815
Show file tree
Hide file tree
Showing 13 changed files with 192 additions and 282 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ repos:
name: doc-status
language: python
entry: python doc/tools/doc_status.py
args: [doc/classes, modules/*/doc_classes, platform/*/doc_classes]
args: [doc/classes, modules/*/doc_classes, platform/*/doc_classes, -c]
pass_filenames: false
files: ^(doc/classes|.*/doc_classes)/.*\.xml$

Expand Down
26 changes: 4 additions & 22 deletions SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -58,31 +58,13 @@ import gles3_builders
import glsl_builders
import methods
import scu_builders
from methods import print_error, print_warning
from methods import Ansi, print_error, print_info, print_warning
from platform_methods import architecture_aliases, architectures

if ARGUMENTS.get("target", "editor") == "editor":
_helper_module("editor.editor_builders", "editor/editor_builders.py")
_helper_module("editor.template_builders", "editor/template_builders.py")

# Enable ANSI escape code support on Windows 10 and later (for colored console output).
# <https://github.com/python/cpython/issues/73245>
if sys.stdout.isatty() and sys.platform == "win32":
try:
from ctypes import WinError, byref, windll # type: ignore
from ctypes.wintypes import DWORD # type: ignore

stdout_handle = windll.kernel32.GetStdHandle(DWORD(-11))
mode = DWORD(0)
if not windll.kernel32.GetConsoleMode(stdout_handle, byref(mode)):
raise WinError()
mode = DWORD(mode.value | 4)
if not windll.kernel32.SetConsoleMode(stdout_handle, mode):
raise WinError()
except Exception as e:
methods._colorize = False
print_error(f"Failed to enable ANSI escape code support, disabling color output.\n{e}")

# Scan possible build platforms

platform_list = [] # list of platforms
Expand Down Expand Up @@ -636,7 +618,7 @@ detect.configure(env)

print(f'Building for platform "{env["platform"]}", architecture "{env["arch"]}", target "{env["target"]}".')
if env.dev_build:
print("NOTE: Developer build, with debug optimization level and debug symbols (unless overridden).")
print_info("Developer build, with debug optimization level and debug symbols (unless overridden).")

# Enforce our minimal compiler version requirements
cc_version = methods.get_compiler_version(env)
Expand Down Expand Up @@ -1126,10 +1108,10 @@ def print_elapsed_time():
time_centiseconds = round((elapsed_time_sec % 1) * 100)
print(
"{}[Time elapsed: {}.{:02}]{}".format(
methods.ANSI.GRAY,
Ansi.GRAY,
time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)),
time_centiseconds,
methods.ANSI.RESET,
Ansi.RESET,
)
)

Expand Down
52 changes: 23 additions & 29 deletions doc/tools/doc_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,26 @@
import fnmatch
import math
import os
import platform
import re
import sys
import xml.etree.ElementTree as ET
from typing import Dict, List, Set

try:
from misc.utility.colorize import COLOR_SUPPORTED, Ansi, set_color # noqa: E402
except ImportError:
root_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")
sys.path.append(root_directory)
from misc.utility.colorize import COLOR_SUPPORTED, Ansi, set_color # noqa: E402

sys.path.remove(root_directory)

################################################################################
# Config #
################################################################################

flags = {
"c": platform.platform() != "Windows", # Disable by default on windows, since we use ANSI escape codes
"c": COLOR_SUPPORTED,
"b": False,
"g": False,
"s": False,
Expand Down Expand Up @@ -85,16 +93,16 @@
"Constructors",
]
colors = {
"name": [36], # cyan
"part_big_problem": [4, 31], # underline, red
"part_problem": [31], # red
"part_mostly_good": [33], # yellow
"part_good": [32], # green
"url": [4, 34], # underline, blue
"section": [1, 4], # bold, underline
"state_off": [36], # cyan
"state_on": [1, 35], # bold, magenta/plum
"bold": [1], # bold
"name": [Ansi.CYAN], # cyan
"part_big_problem": [Ansi.RED, Ansi.UNDERLINE], # underline, red
"part_problem": [Ansi.RED], # red
"part_mostly_good": [Ansi.YELLOW], # yellow
"part_good": [Ansi.GREEN], # green
"url": [Ansi.BLUE, Ansi.UNDERLINE], # underline, blue
"section": [Ansi.BOLD, Ansi.UNDERLINE], # bold, underline
"state_off": [Ansi.CYAN], # cyan
"state_on": [Ansi.BOLD, Ansi.MAGENTA], # bold, magenta/plum
"bold": [Ansi.BOLD], # bold
}
overall_progress_description_weight = 10

Expand All @@ -111,13 +119,8 @@ def validate_tag(elem: ET.Element, tag: str) -> None:


def color(color: str, string: str) -> str:
if flags["c"] and terminal_supports_color():
color_format = ""
for code in colors[color]:
color_format += "\033[" + str(code) + "m"
return color_format + string + "\033[0m"
else:
return string
color_format = "".join([str(x) for x in colors[color]])
return f"{color_format}{string}{Ansi.RESET}"


ansi_escape = re.compile(r"\x1b[^m]*m")
Expand All @@ -127,16 +130,6 @@ def nonescape_len(s: str) -> int:
return len(ansi_escape.sub("", s))


def terminal_supports_color():
p = sys.platform
supported_platform = p != "Pocket PC" and (p != "win32" or "ANSICON" in os.environ)

is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
if not supported_platform or not is_a_tty:
return False
return True


################################################################################
# Classes #
################################################################################
Expand Down Expand Up @@ -342,6 +335,7 @@ def generate_for_class(c: ET.Element):
table_column_names.append("Docs URL")
table_columns.append("url")

set_color(flags["c"])

################################################################################
# Help #
Expand Down
51 changes: 12 additions & 39 deletions doc/tools/make_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
root_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")
sys.path.append(root_directory) # Include the root directory
import version # noqa: E402
from misc.utility.colorize import Ansi, set_color # noqa: E402

# $DOCS_URL/path/to/page.html(#fragment-tag)
GODOT_DOCS_PATTERN = re.compile(r"^\$DOCS_URL/(.*)\.html(#.*)?$")
Expand Down Expand Up @@ -90,8 +91,6 @@
]
strings_l10n: Dict[str, str] = {}

STYLES: Dict[str, str] = {}

CLASS_GROUPS: Dict[str, str] = {
"global": "Globals",
"node": "Nodes",
Expand Down Expand Up @@ -699,31 +698,7 @@ def main() -> None:
)
args = parser.parse_args()

should_color = bool(args.color or sys.stdout.isatty() or os.environ.get("CI"))

# Enable ANSI escape code support on Windows 10 and later (for colored console output).
# <https://github.com/python/cpython/issues/73245>
if should_color and sys.stdout.isatty() and sys.platform == "win32":
try:
from ctypes import WinError, byref, windll # type: ignore
from ctypes.wintypes import DWORD # type: ignore

stdout_handle = windll.kernel32.GetStdHandle(DWORD(-11))
mode = DWORD(0)
if not windll.kernel32.GetConsoleMode(stdout_handle, byref(mode)):
raise WinError()
mode = DWORD(mode.value | 4)
if not windll.kernel32.SetConsoleMode(stdout_handle, mode):
raise WinError()
except Exception:
should_color = False

STYLES["red"] = "\x1b[91m" if should_color else ""
STYLES["green"] = "\x1b[92m" if should_color else ""
STYLES["yellow"] = "\x1b[93m" if should_color else ""
STYLES["bold"] = "\x1b[1m" if should_color else ""
STYLES["regular"] = "\x1b[22m" if should_color else ""
STYLES["reset"] = "\x1b[0m" if should_color else ""
set_color(args.color)

# Retrieve heading translations for the given language.
if not args.dry_run and args.lang != "en":
Expand Down Expand Up @@ -834,16 +809,16 @@ def main() -> None:
if state.script_language_parity_check.hit_count > 0:
if not args.verbose:
print(
f'{STYLES["yellow"]}{state.script_language_parity_check.hit_count} code samples failed parity check. Use --verbose to get more information.{STYLES["reset"]}'
f"{Ansi.YELLOW}{state.script_language_parity_check.hit_count} code samples failed parity check. Use --verbose to get more information.{Ansi.RESET}"
)
else:
print(
f'{STYLES["yellow"]}{state.script_language_parity_check.hit_count} code samples failed parity check:{STYLES["reset"]}'
f"{Ansi.YELLOW}{state.script_language_parity_check.hit_count} code samples failed parity check:{Ansi.RESET}"
)

for class_name in state.script_language_parity_check.hit_map.keys():
class_hits = state.script_language_parity_check.hit_map[class_name]
print(f'{STYLES["yellow"]}- {len(class_hits)} hits in class "{class_name}"{STYLES["reset"]}')
print(f'{Ansi.YELLOW}- {len(class_hits)} hits in class "{class_name}"{Ansi.RESET}')

for context, error in class_hits:
print(f" - {error} in {format_context_name(context)}")
Expand All @@ -853,24 +828,22 @@ def main() -> None:

if state.num_warnings >= 2:
print(
f'{STYLES["yellow"]}{state.num_warnings} warnings were found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
f"{Ansi.YELLOW}{state.num_warnings} warnings were found in the class reference XML. Please check the messages above.{Ansi.RESET}"
)
elif state.num_warnings == 1:
print(
f'{STYLES["yellow"]}1 warning was found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
f"{Ansi.YELLOW}1 warning was found in the class reference XML. Please check the messages above.{Ansi.RESET}"
)

if state.num_errors >= 2:
print(
f'{STYLES["red"]}{state.num_errors} errors were found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
f"{Ansi.RED}{state.num_errors} errors were found in the class reference XML. Please check the messages above.{Ansi.RESET}"
)
elif state.num_errors == 1:
print(
f'{STYLES["red"]}1 error was found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
)
print(f"{Ansi.RED}1 error was found in the class reference XML. Please check the messages above.{Ansi.RESET}")

if state.num_warnings == 0 and state.num_errors == 0:
print(f'{STYLES["green"]}No warnings or errors found in the class reference XML.{STYLES["reset"]}')
print(f"{Ansi.GREEN}No warnings or errors found in the class reference XML.{Ansi.RESET}")
if not args.dry_run:
print(f"Wrote reStructuredText files for each class to: {args.output}")
else:
Expand All @@ -881,12 +854,12 @@ def main() -> None:


def print_error(error: str, state: State) -> None:
print(f'{STYLES["red"]}{STYLES["bold"]}ERROR:{STYLES["regular"]} {error}{STYLES["reset"]}')
print(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR} {error}{Ansi.RESET}")
state.num_errors += 1


def print_warning(warning: str, state: State) -> None:
print(f'{STYLES["yellow"]}{STYLES["bold"]}WARNING:{STYLES["regular"]} {warning}{STYLES["reset"]}')
print(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR} {warning}{Ansi.RESET}")
state.num_warnings += 1


Expand Down
59 changes: 6 additions & 53 deletions methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,72 +5,25 @@
import subprocess
import sys
from collections import OrderedDict
from enum import Enum
from io import StringIO, TextIOWrapper
from pathlib import Path
from typing import Generator, List, Optional, Union

from misc.utility.colorize import Ansi, print_error, print_info, print_warning

# Get the "Godot" folder name ahead of time
base_folder_path = str(os.path.abspath(Path(__file__).parent)) + "/"
base_folder_only = os.path.basename(os.path.normpath(base_folder_path))
# Listing all the folders we have converted
# for SCU in scu_builders.py
_scu_folders = set()
# Colors are disabled in non-TTY environments such as pipes. This means
# that if output is redirected to a file, it won't contain color codes.
# Colors are always enabled on continuous integration.
_colorize = bool(sys.stdout.isatty() or os.environ.get("CI"))


def set_scu_folders(scu_folders):
global _scu_folders
_scu_folders = scu_folders


class ANSI(Enum):
"""
Enum class for adding ansi colorcodes directly into strings.
Automatically converts values to strings representing their
internal value, or an empty string in a non-colorized scope.
"""

RESET = "\x1b[0m"

BOLD = "\x1b[1m"
ITALIC = "\x1b[3m"
UNDERLINE = "\x1b[4m"
STRIKETHROUGH = "\x1b[9m"
REGULAR = "\x1b[22;23;24;29m"

BLACK = "\x1b[30m"
RED = "\x1b[31m"
GREEN = "\x1b[32m"
YELLOW = "\x1b[33m"
BLUE = "\x1b[34m"
MAGENTA = "\x1b[35m"
CYAN = "\x1b[36m"
WHITE = "\x1b[37m"

PURPLE = "\x1b[38;5;93m"
PINK = "\x1b[38;5;206m"
ORANGE = "\x1b[38;5;214m"
GRAY = "\x1b[38;5;244m"

def __str__(self) -> str:
global _colorize
return str(self.value) if _colorize else ""


def print_warning(*values: object) -> None:
"""Prints a warning message with formatting."""
print(f"{ANSI.YELLOW}{ANSI.BOLD}WARNING:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr)


def print_error(*values: object) -> None:
"""Prints an error message with formatting."""
print(f"{ANSI.RED}{ANSI.BOLD}ERROR:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr)


def add_source_files_orig(self, sources, files, allow_gen=False):
# Convert string to list of absolute paths (including expanding wildcard)
if isinstance(files, (str, bytes)):
Expand Down Expand Up @@ -199,7 +152,7 @@ def get_version_info(module_version_string="", silent=False):
if os.getenv("BUILD_NAME") is not None:
build_name = str(os.getenv("BUILD_NAME"))
if not silent:
print(f"Using custom build name: '{build_name}'.")
print_info(f"Using custom build name: '{build_name}'.")

import version

Expand All @@ -221,7 +174,7 @@ def get_version_info(module_version_string="", silent=False):
if os.getenv("GODOT_VERSION_STATUS") is not None:
version_info["status"] = str(os.getenv("GODOT_VERSION_STATUS"))
if not silent:
print(f"Using version status '{version_info['status']}', overriding the original '{version.status}'.")
print_info(f"Using version status '{version_info['status']}', overriding the original '{version.status}'.")

# Parse Git hash if we're in a Git repo.
githash = ""
Expand Down Expand Up @@ -508,7 +461,7 @@ def mySpawn(sh, escape, cmd, args, env):


def no_verbose(env):
colors = [ANSI.BLUE, ANSI.BOLD, ANSI.REGULAR, ANSI.RESET]
colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET]

# There is a space before "..." to ensure that source file names can be
# Ctrl + clicked in the VS Code terminal.
Expand Down Expand Up @@ -1610,7 +1563,7 @@ def generate_copyright_header(filename: str) -> str:
"""
filename = filename.split("/")[-1].ljust(MARGIN)
if len(filename) > MARGIN:
print(f'WARNING: Filename "{filename}" too large for copyright header.')
print_warning(f'Filename "{filename}" too large for copyright header.')
return TEMPLATE % filename


Expand Down
Loading

0 comments on commit 9d02815

Please sign in to comment.