mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 07:31:38 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			346 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			346 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import io
 | 
						|
import os
 | 
						|
import sys
 | 
						|
 | 
						|
from collections.abc import Callable, Iterator, Mapping
 | 
						|
from dataclasses import dataclass, field, Field
 | 
						|
 | 
						|
COLORIZE = True
 | 
						|
 | 
						|
 | 
						|
# types
 | 
						|
if False:
 | 
						|
    from typing import IO, Self, ClassVar
 | 
						|
    _theme: Theme
 | 
						|
 | 
						|
 | 
						|
class ANSIColors:
 | 
						|
    RESET = "\x1b[0m"
 | 
						|
 | 
						|
    BLACK = "\x1b[30m"
 | 
						|
    BLUE = "\x1b[34m"
 | 
						|
    CYAN = "\x1b[36m"
 | 
						|
    GREEN = "\x1b[32m"
 | 
						|
    GREY = "\x1b[90m"
 | 
						|
    MAGENTA = "\x1b[35m"
 | 
						|
    RED = "\x1b[31m"
 | 
						|
    WHITE = "\x1b[37m"  # more like LIGHT GRAY
 | 
						|
    YELLOW = "\x1b[33m"
 | 
						|
 | 
						|
    BOLD = "\x1b[1m"
 | 
						|
    BOLD_BLACK = "\x1b[1;30m"  # DARK GRAY
 | 
						|
    BOLD_BLUE = "\x1b[1;34m"
 | 
						|
    BOLD_CYAN = "\x1b[1;36m"
 | 
						|
    BOLD_GREEN = "\x1b[1;32m"
 | 
						|
    BOLD_MAGENTA = "\x1b[1;35m"
 | 
						|
    BOLD_RED = "\x1b[1;31m"
 | 
						|
    BOLD_WHITE = "\x1b[1;37m"  # actual WHITE
 | 
						|
    BOLD_YELLOW = "\x1b[1;33m"
 | 
						|
 | 
						|
    # intense = like bold but without being bold
 | 
						|
    INTENSE_BLACK = "\x1b[90m"
 | 
						|
    INTENSE_BLUE = "\x1b[94m"
 | 
						|
    INTENSE_CYAN = "\x1b[96m"
 | 
						|
    INTENSE_GREEN = "\x1b[92m"
 | 
						|
    INTENSE_MAGENTA = "\x1b[95m"
 | 
						|
    INTENSE_RED = "\x1b[91m"
 | 
						|
    INTENSE_WHITE = "\x1b[97m"
 | 
						|
    INTENSE_YELLOW = "\x1b[93m"
 | 
						|
 | 
						|
    BACKGROUND_BLACK = "\x1b[40m"
 | 
						|
    BACKGROUND_BLUE = "\x1b[44m"
 | 
						|
    BACKGROUND_CYAN = "\x1b[46m"
 | 
						|
    BACKGROUND_GREEN = "\x1b[42m"
 | 
						|
    BACKGROUND_MAGENTA = "\x1b[45m"
 | 
						|
    BACKGROUND_RED = "\x1b[41m"
 | 
						|
    BACKGROUND_WHITE = "\x1b[47m"
 | 
						|
    BACKGROUND_YELLOW = "\x1b[43m"
 | 
						|
 | 
						|
    INTENSE_BACKGROUND_BLACK = "\x1b[100m"
 | 
						|
    INTENSE_BACKGROUND_BLUE = "\x1b[104m"
 | 
						|
    INTENSE_BACKGROUND_CYAN = "\x1b[106m"
 | 
						|
    INTENSE_BACKGROUND_GREEN = "\x1b[102m"
 | 
						|
    INTENSE_BACKGROUND_MAGENTA = "\x1b[105m"
 | 
						|
    INTENSE_BACKGROUND_RED = "\x1b[101m"
 | 
						|
    INTENSE_BACKGROUND_WHITE = "\x1b[107m"
 | 
						|
    INTENSE_BACKGROUND_YELLOW = "\x1b[103m"
 | 
						|
 | 
						|
 | 
						|
ColorCodes = set()
 | 
						|
NoColors = ANSIColors()
 | 
						|
 | 
						|
for attr, code in ANSIColors.__dict__.items():
 | 
						|
    if not attr.startswith("__"):
 | 
						|
        ColorCodes.add(code)
 | 
						|
        setattr(NoColors, attr, "")
 | 
						|
 | 
						|
 | 
						|
#
 | 
						|
# Experimental theming support (see gh-133346)
 | 
						|
#
 | 
						|
 | 
						|
# - Create a theme by copying an existing `Theme` with one or more sections
 | 
						|
#   replaced, using `default_theme.copy_with()`;
 | 
						|
# - create a theme section by copying an existing `ThemeSection` with one or
 | 
						|
#   more colors replaced, using for example `default_theme.syntax.copy_with()`;
 | 
						|
# - create a theme from scratch by instantiating a `Theme` data class with
 | 
						|
#   the required sections (which are also dataclass instances).
 | 
						|
#
 | 
						|
# Then call `_colorize.set_theme(your_theme)` to set it.
 | 
						|
#
 | 
						|
# Put your theme configuration in $PYTHONSTARTUP for the interactive shell,
 | 
						|
# or sitecustomize.py in your virtual environment or Python installation for
 | 
						|
# other uses.  Your applications can call `_colorize.set_theme()` too.
 | 
						|
#
 | 
						|
# Note that thanks to the dataclasses providing default values for all fields,
 | 
						|
# creating a new theme or theme section from scratch is possible without
 | 
						|
# specifying all keys.
 | 
						|
#
 | 
						|
# For example, here's a theme that makes punctuation and operators less prominent:
 | 
						|
#
 | 
						|
#   try:
 | 
						|
#       from _colorize import set_theme, default_theme, Syntax, ANSIColors
 | 
						|
#   except ImportError:
 | 
						|
#       pass
 | 
						|
#   else:
 | 
						|
#       theme_with_dim_operators = default_theme.copy_with(
 | 
						|
#           syntax=Syntax(op=ANSIColors.INTENSE_BLACK),
 | 
						|
#       )
 | 
						|
#       set_theme(theme_with_dim_operators)
 | 
						|
#       del set_theme, default_theme, Syntax, ANSIColors, theme_with_dim_operators
 | 
						|
#
 | 
						|
# Guarding the import ensures that your .pythonstartup file will still work in
 | 
						|
# Python 3.13 and older. Deleting the variables ensures they don't remain in your
 | 
						|
# interactive shell's global scope.
 | 
						|
 | 
						|
class ThemeSection(Mapping[str, str]):
 | 
						|
    """A mixin/base class for theme sections.
 | 
						|
 | 
						|
    It enables dictionary access to a section, as well as implements convenience
 | 
						|
    methods.
 | 
						|
    """
 | 
						|
 | 
						|
    # The two types below are just that: types to inform the type checker that the
 | 
						|
    # mixin will work in context of those fields existing
 | 
						|
    __dataclass_fields__: ClassVar[dict[str, Field[str]]]
 | 
						|
    _name_to_value: Callable[[str], str]
 | 
						|
 | 
						|
    def __post_init__(self) -> None:
 | 
						|
        name_to_value = {}
 | 
						|
        for color_name in self.__dataclass_fields__:
 | 
						|
            name_to_value[color_name] = getattr(self, color_name)
 | 
						|
        super().__setattr__('_name_to_value', name_to_value.__getitem__)
 | 
						|
 | 
						|
    def copy_with(self, **kwargs: str) -> Self:
 | 
						|
        color_state: dict[str, str] = {}
 | 
						|
        for color_name in self.__dataclass_fields__:
 | 
						|
            color_state[color_name] = getattr(self, color_name)
 | 
						|
        color_state.update(kwargs)
 | 
						|
        return type(self)(**color_state)
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def no_colors(cls) -> Self:
 | 
						|
        color_state: dict[str, str] = {}
 | 
						|
        for color_name in cls.__dataclass_fields__:
 | 
						|
            color_state[color_name] = ""
 | 
						|
        return cls(**color_state)
 | 
						|
 | 
						|
    def __getitem__(self, key: str) -> str:
 | 
						|
        return self._name_to_value(key)
 | 
						|
 | 
						|
    def __len__(self) -> int:
 | 
						|
        return len(self.__dataclass_fields__)
 | 
						|
 | 
						|
    def __iter__(self) -> Iterator[str]:
 | 
						|
        return iter(self.__dataclass_fields__)
 | 
						|
 | 
						|
 | 
						|
@dataclass(frozen=True)
 | 
						|
class Argparse(ThemeSection):
 | 
						|
    usage: str = ANSIColors.BOLD_BLUE
 | 
						|
    prog: str = ANSIColors.BOLD_MAGENTA
 | 
						|
    prog_extra: str = ANSIColors.MAGENTA
 | 
						|
    heading: str = ANSIColors.BOLD_BLUE
 | 
						|
    summary_long_option: str = ANSIColors.CYAN
 | 
						|
    summary_short_option: str = ANSIColors.GREEN
 | 
						|
    summary_label: str = ANSIColors.YELLOW
 | 
						|
    summary_action: str = ANSIColors.GREEN
 | 
						|
    long_option: str = ANSIColors.BOLD_CYAN
 | 
						|
    short_option: str = ANSIColors.BOLD_GREEN
 | 
						|
    label: str = ANSIColors.BOLD_YELLOW
 | 
						|
    action: str = ANSIColors.BOLD_GREEN
 | 
						|
    reset: str = ANSIColors.RESET
 | 
						|
 | 
						|
 | 
						|
@dataclass(frozen=True)
 | 
						|
class Syntax(ThemeSection):
 | 
						|
    prompt: str = ANSIColors.BOLD_MAGENTA
 | 
						|
    keyword: str = ANSIColors.BOLD_BLUE
 | 
						|
    builtin: str = ANSIColors.CYAN
 | 
						|
    comment: str = ANSIColors.RED
 | 
						|
    string: str = ANSIColors.GREEN
 | 
						|
    number: str = ANSIColors.YELLOW
 | 
						|
    op: str = ANSIColors.RESET
 | 
						|
    definition: str = ANSIColors.BOLD
 | 
						|
    soft_keyword: str = ANSIColors.BOLD_BLUE
 | 
						|
    reset: str = ANSIColors.RESET
 | 
						|
 | 
						|
 | 
						|
@dataclass(frozen=True)
 | 
						|
class Traceback(ThemeSection):
 | 
						|
    type: str = ANSIColors.BOLD_MAGENTA
 | 
						|
    message: str = ANSIColors.MAGENTA
 | 
						|
    filename: str = ANSIColors.MAGENTA
 | 
						|
    line_no: str = ANSIColors.MAGENTA
 | 
						|
    frame: str = ANSIColors.MAGENTA
 | 
						|
    error_highlight: str = ANSIColors.BOLD_RED
 | 
						|
    error_range: str = ANSIColors.RED
 | 
						|
    reset: str = ANSIColors.RESET
 | 
						|
 | 
						|
 | 
						|
@dataclass(frozen=True)
 | 
						|
class Unittest(ThemeSection):
 | 
						|
    passed: str = ANSIColors.GREEN
 | 
						|
    warn: str = ANSIColors.YELLOW
 | 
						|
    fail: str = ANSIColors.RED
 | 
						|
    fail_info: str = ANSIColors.BOLD_RED
 | 
						|
    reset: str = ANSIColors.RESET
 | 
						|
 | 
						|
 | 
						|
@dataclass(frozen=True)
 | 
						|
class Theme:
 | 
						|
    """A suite of themes for all sections of Python.
 | 
						|
 | 
						|
    When adding a new one, remember to also modify `copy_with` and `no_colors`
 | 
						|
    below.
 | 
						|
    """
 | 
						|
    argparse: Argparse = field(default_factory=Argparse)
 | 
						|
    syntax: Syntax = field(default_factory=Syntax)
 | 
						|
    traceback: Traceback = field(default_factory=Traceback)
 | 
						|
    unittest: Unittest = field(default_factory=Unittest)
 | 
						|
 | 
						|
    def copy_with(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        argparse: Argparse | None = None,
 | 
						|
        syntax: Syntax | None = None,
 | 
						|
        traceback: Traceback | None = None,
 | 
						|
        unittest: Unittest | None = None,
 | 
						|
    ) -> Self:
 | 
						|
        """Return a new Theme based on this instance with some sections replaced.
 | 
						|
 | 
						|
        Themes are immutable to protect against accidental modifications that
 | 
						|
        could lead to invalid terminal states.
 | 
						|
        """
 | 
						|
        return type(self)(
 | 
						|
            argparse=argparse or self.argparse,
 | 
						|
            syntax=syntax or self.syntax,
 | 
						|
            traceback=traceback or self.traceback,
 | 
						|
            unittest=unittest or self.unittest,
 | 
						|
        )
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def no_colors(cls) -> Self:
 | 
						|
        """Return a new Theme where colors in all sections are empty strings.
 | 
						|
 | 
						|
        This allows writing user code as if colors are always used. The color
 | 
						|
        fields will be ANSI color code strings when colorization is desired
 | 
						|
        and possible, and empty strings otherwise.
 | 
						|
        """
 | 
						|
        return cls(
 | 
						|
            argparse=Argparse.no_colors(),
 | 
						|
            syntax=Syntax.no_colors(),
 | 
						|
            traceback=Traceback.no_colors(),
 | 
						|
            unittest=Unittest.no_colors(),
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
def get_colors(
 | 
						|
    colorize: bool = False, *, file: IO[str] | IO[bytes] | None = None
 | 
						|
) -> ANSIColors:
 | 
						|
    if colorize or can_colorize(file=file):
 | 
						|
        return ANSIColors()
 | 
						|
    else:
 | 
						|
        return NoColors
 | 
						|
 | 
						|
 | 
						|
def decolor(text: str) -> str:
 | 
						|
    """Remove ANSI color codes from a string."""
 | 
						|
    for code in ColorCodes:
 | 
						|
        text = text.replace(code, "")
 | 
						|
    return text
 | 
						|
 | 
						|
 | 
						|
def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool:
 | 
						|
    if file is None:
 | 
						|
        file = sys.stdout
 | 
						|
 | 
						|
    if not sys.flags.ignore_environment:
 | 
						|
        if os.environ.get("PYTHON_COLORS") == "0":
 | 
						|
            return False
 | 
						|
        if os.environ.get("PYTHON_COLORS") == "1":
 | 
						|
            return True
 | 
						|
    if os.environ.get("NO_COLOR"):
 | 
						|
        return False
 | 
						|
    if not COLORIZE:
 | 
						|
        return False
 | 
						|
    if os.environ.get("FORCE_COLOR"):
 | 
						|
        return True
 | 
						|
    if os.environ.get("TERM") == "dumb":
 | 
						|
        return False
 | 
						|
 | 
						|
    if not hasattr(file, "fileno"):
 | 
						|
        return False
 | 
						|
 | 
						|
    if sys.platform == "win32":
 | 
						|
        try:
 | 
						|
            import nt
 | 
						|
 | 
						|
            if not nt._supports_virtual_terminal():
 | 
						|
                return False
 | 
						|
        except (ImportError, AttributeError):
 | 
						|
            return False
 | 
						|
 | 
						|
    try:
 | 
						|
        return os.isatty(file.fileno())
 | 
						|
    except io.UnsupportedOperation:
 | 
						|
        return hasattr(file, "isatty") and file.isatty()
 | 
						|
 | 
						|
 | 
						|
default_theme = Theme()
 | 
						|
theme_no_color = default_theme.no_colors()
 | 
						|
 | 
						|
 | 
						|
def get_theme(
 | 
						|
    *,
 | 
						|
    tty_file: IO[str] | IO[bytes] | None = None,
 | 
						|
    force_color: bool = False,
 | 
						|
    force_no_color: bool = False,
 | 
						|
) -> Theme:
 | 
						|
    """Returns the currently set theme, potentially in a zero-color variant.
 | 
						|
 | 
						|
    In cases where colorizing is not possible (see `can_colorize`), the returned
 | 
						|
    theme contains all empty strings in all color definitions.
 | 
						|
    See `Theme.no_colors()` for more information.
 | 
						|
 | 
						|
    It is recommended not to cache the result of this function for extended
 | 
						|
    periods of time because the user might influence theme selection by
 | 
						|
    the interactive shell, a debugger, or application-specific code. The
 | 
						|
    environment (including environment variable state and console configuration
 | 
						|
    on Windows) can also change in the course of the application life cycle.
 | 
						|
    """
 | 
						|
    if force_color or (not force_no_color and can_colorize(file=tty_file)):
 | 
						|
        return _theme
 | 
						|
    return theme_no_color
 | 
						|
 | 
						|
 | 
						|
def set_theme(t: Theme) -> None:
 | 
						|
    global _theme
 | 
						|
 | 
						|
    if not isinstance(t, Theme):
 | 
						|
        raise ValueError(f"Expected Theme object, found {t}")
 | 
						|
 | 
						|
    _theme = t
 | 
						|
 | 
						|
 | 
						|
set_theme(default_theme)
 |