color.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. from __future__ import annotations
  2. import os
  3. import re
  4. import sys
  5. from enum import Enum
  6. from typing import Final
  7. # Colors are disabled in non-TTY environments such as pipes. This means if output is redirected
  8. # to a file, it won't contain color codes. Colors are enabled by default on continuous integration.
  9. IS_CI: Final[bool] = bool(os.environ.get("CI"))
  10. NO_COLOR: Final[bool] = bool(os.environ.get("NO_COLOR"))
  11. CLICOLOR_FORCE: Final[bool] = bool(os.environ.get("CLICOLOR_FORCE"))
  12. STDOUT_TTY: Final[bool] = bool(sys.stdout.isatty())
  13. STDERR_TTY: Final[bool] = bool(sys.stderr.isatty())
  14. _STDOUT_ORIGINAL: Final[bool] = False if NO_COLOR else CLICOLOR_FORCE or IS_CI or STDOUT_TTY
  15. _STDERR_ORIGINAL: Final[bool] = False if NO_COLOR else CLICOLOR_FORCE or IS_CI or STDERR_TTY
  16. _stdout_override: bool = _STDOUT_ORIGINAL
  17. _stderr_override: bool = _STDERR_ORIGINAL
  18. def is_stdout_color() -> bool:
  19. return _stdout_override
  20. def is_stderr_color() -> bool:
  21. return _stderr_override
  22. def force_stdout_color(value: bool) -> None:
  23. """
  24. Explicitly set `stdout` support for ANSI escape codes.
  25. If environment overrides exist, does nothing.
  26. """
  27. if not NO_COLOR or not CLICOLOR_FORCE:
  28. global _stdout_override
  29. _stdout_override = value
  30. def force_stderr_color(value: bool) -> None:
  31. """
  32. Explicitly set `stderr` support for ANSI escape codes.
  33. If environment overrides exist, does nothing.
  34. """
  35. if not NO_COLOR or not CLICOLOR_FORCE:
  36. global _stderr_override
  37. _stderr_override = value
  38. class Ansi(Enum):
  39. """
  40. Enum class for adding ANSI codepoints directly into strings. Automatically converts values to
  41. strings representing their internal value.
  42. """
  43. RESET = "\x1b[0m"
  44. BOLD = "\x1b[1m"
  45. DIM = "\x1b[2m"
  46. ITALIC = "\x1b[3m"
  47. UNDERLINE = "\x1b[4m"
  48. STRIKETHROUGH = "\x1b[9m"
  49. REGULAR = "\x1b[22;23;24;29m"
  50. BLACK = "\x1b[30m"
  51. RED = "\x1b[31m"
  52. GREEN = "\x1b[32m"
  53. YELLOW = "\x1b[33m"
  54. BLUE = "\x1b[34m"
  55. MAGENTA = "\x1b[35m"
  56. CYAN = "\x1b[36m"
  57. WHITE = "\x1b[37m"
  58. GRAY = "\x1b[90m"
  59. def __str__(self) -> str:
  60. return self.value
  61. RE_ANSI = re.compile(r"\x1b\[[=\?]?[;\d]+[a-zA-Z]")
  62. def color_print(*values: object, sep: str | None = " ", end: str | None = "\n", flush: bool = False) -> None:
  63. """Prints a colored message to `stdout`. If disabled, ANSI codes are automatically stripped."""
  64. if is_stdout_color():
  65. print(*values, sep=sep, end=f"{Ansi.RESET}{end}", flush=flush)
  66. else:
  67. print(RE_ANSI.sub("", (sep or " ").join(map(str, values))), sep="", end=end, flush=flush)
  68. def color_printerr(*values: object, sep: str | None = " ", end: str | None = "\n", flush: bool = False) -> None:
  69. """Prints a colored message to `stderr`. If disabled, ANSI codes are automatically stripped."""
  70. if is_stderr_color():
  71. print(*values, sep=sep, end=f"{Ansi.RESET}{end}", flush=flush, file=sys.stderr)
  72. else:
  73. print(RE_ANSI.sub("", (sep or " ").join(map(str, values))), sep="", end=end, flush=flush, file=sys.stderr)
  74. def print_info(*values: object) -> None:
  75. """Prints a informational message with formatting."""
  76. color_print(f"{Ansi.GRAY}{Ansi.BOLD}INFO:{Ansi.REGULAR}", *values)
  77. def print_warning(*values: object) -> None:
  78. """Prints a warning message with formatting."""
  79. color_printerr(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR}", *values)
  80. def print_error(*values: object) -> None:
  81. """Prints an error message with formatting."""
  82. color_printerr(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR}", *values)
  83. if sys.platform == "win32":
  84. def _win_color_fix():
  85. """Attempts to enable ANSI escape code support on Windows 10 and later."""
  86. from ctypes import POINTER, WINFUNCTYPE, WinError, windll
  87. from ctypes.wintypes import BOOL, DWORD, HANDLE
  88. STDOUT_HANDLE = -11
  89. STDERR_HANDLE = -12
  90. ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
  91. def err_handler(result, func, args):
  92. if not result:
  93. raise WinError()
  94. return args
  95. GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),))
  96. GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
  97. ("GetConsoleMode", windll.kernel32),
  98. ((1, "hConsoleHandle"), (2, "lpMode")),
  99. )
  100. GetConsoleMode.errcheck = err_handler
  101. SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
  102. ("SetConsoleMode", windll.kernel32),
  103. ((1, "hConsoleHandle"), (1, "dwMode")),
  104. )
  105. SetConsoleMode.errcheck = err_handler
  106. for handle_id in [STDOUT_HANDLE, STDERR_HANDLE]:
  107. try:
  108. handle = GetStdHandle(handle_id)
  109. flags = GetConsoleMode(handle)
  110. SetConsoleMode(handle, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
  111. except OSError:
  112. pass
  113. _win_color_fix()