diff --git a/sphinx/_cli/util/colour.py b/sphinx/_cli/util/colour.py index 122b836129f..5ad8c279607 100644 --- a/sphinx/_cli/util/colour.py +++ b/sphinx/_cli/util/colour.py @@ -2,27 +2,31 @@ from __future__ import annotations -import os import sys -from collections.abc import Callable # NoQA: TC003 +from os import environ as _environ + +if False: + from collections.abc import Callable if sys.platform == 'win32': import colorama + colorama.just_fix_windows_console() + del colorama + _COLOURING_DISABLED = False def terminal_supports_colour() -> bool: """Return True if coloured terminal output is supported.""" - if 'NO_COLOUR' in os.environ or 'NO_COLOR' in os.environ: + if 'NO_COLOUR' in _environ or 'NO_COLOR' in _environ: return False if sys.platform == 'win32': - colorama.just_fix_windows_console() return True - if 'FORCE_COLOUR' in os.environ or 'FORCE_COLOR' in os.environ: + if 'FORCE_COLOUR' in _environ or 'FORCE_COLOR' in _environ: return True - if os.environ.get('CI', '') in {'true', '1'}: + if _environ.get('CI', '').lower() in {'true', '1'}: return True try: @@ -34,7 +38,7 @@ def terminal_supports_colour() -> bool: return False # Do not colour output if on a dumb terminal - return os.environ.get('TERM', 'unknown').lower() not in {'dumb', 'unknown'} + return _environ.get('TERM', 'unknown').lower() not in {'dumb', 'unknown'} def disable_colour() -> None: @@ -50,7 +54,21 @@ def enable_colour() -> None: def colourise(colour_name: str, text: str, /) -> str: if _COLOURING_DISABLED: return text - return globals()[colour_name](text) + if colour_name.startswith('_') or colour_name in { + 'annotations', + 'sys', + 'terminal_supports_colour', + 'disable_colour', + 'enable_colour', + 'colourise', + }: + msg = f'Invalid colour name: {colour_name!r}' + raise ValueError(msg) + try: + return globals()[colour_name](text) + except KeyError: + msg = f'Invalid colour name: {colour_name!r}' + raise ValueError(msg) from None def _create_colour_func(escape_code: str, /) -> Callable[[str], str]: diff --git a/sphinx/cmd/quickstart.py b/sphinx/cmd/quickstart.py index 06a89b995e4..7bdd5304343 100644 --- a/sphinx/cmd/quickstart.py +++ b/sphinx/cmd/quickstart.py @@ -10,35 +10,18 @@ import time from typing import TYPE_CHECKING -# try to import readline, unix specific enhancement -try: - import readline - - if TYPE_CHECKING and sys.platform == 'win32': # always false, for type checking - raise ImportError # NoQA: TRY301 - READLINE_AVAILABLE = True - if readline.__doc__ and 'libedit' in readline.__doc__: - readline.parse_and_bind('bind ^I rl_complete') - USE_LIBEDIT = True - else: - readline.parse_and_bind('tab: complete') - USE_LIBEDIT = False -except ImportError: - READLINE_AVAILABLE = False - USE_LIBEDIT = False - from docutils.utils import column_width import sphinx.locale from sphinx import __display_version__, package_dir from sphinx._cli.util.colour import ( + _create_input_mode_colour_func, bold, disable_colour, red, terminal_supports_colour, ) from sphinx.locale import __ -from sphinx.util.console import colorize from sphinx.util.osutil import ensuredir from sphinx.util.template import SphinxRenderer @@ -46,6 +29,25 @@ from collections.abc import Callable, Sequence from typing import Any +# try to import readline, unix specific enhancement +try: + import readline + + if TYPE_CHECKING and sys.platform == 'win32': + # MyPy doesn't realise that this raises a ModuleNotFoundError + # on Windows, and complains that 'parse_and_bind' is not defined. + # This condition is always False at runtime, but tricks type checkers. + raise ImportError # NoQA: TRY301 +except ImportError: + READLINE_AVAILABLE = USE_LIBEDIT = False +else: + READLINE_AVAILABLE = True + USE_LIBEDIT = 'libedit' in getattr(readline, '__doc__', '') + if USE_LIBEDIT: + readline.parse_and_bind('bind ^I rl_complete') + else: + readline.parse_and_bind('tab: complete') + EXTENSIONS = { 'autodoc': __('automatically insert docstrings from modules'), 'doctest': __('automatically test code snippets in doctest blocks'), @@ -73,10 +75,17 @@ PROMPT_PREFIX = '> ' if sys.platform == 'win32': - # On Windows, show questions as bold because of color scheme of PowerShell (refs: #5294). - COLOR_QUESTION = 'bold' + # On Windows, show questions as bold because of PowerShell's colour scheme + # (xref: https://github.com/sphinx-doc/sphinx/issues/5294). + from sphinx._cli.util.colour import bold as _question_colour else: - COLOR_QUESTION = 'purple' + from sphinx._cli.util.colour import purple as _question_colour + + if READLINE_AVAILABLE: + # Use an input-mode colour function if readline is available + if escape_code := getattr(_question_colour, '__escape_code', ''): + _question_colour = _create_input_mode_colour_func(escape_code) + del escape_code # function to get input from terminal -- overridden by the test suite @@ -158,11 +167,8 @@ def do_prompt( # sequence (see #5335). To avoid the problem, all prompts are not colored # on libedit. pass - elif READLINE_AVAILABLE: - # pass input_mode=True if readline available - prompt = colorize(COLOR_QUESTION, prompt, input_mode=True) else: - prompt = colorize(COLOR_QUESTION, prompt, input_mode=False) + prompt = _question_colour(prompt) x = term_input(prompt).strip() if default and not x: x = default diff --git a/sphinx/util/logging.py b/sphinx/util/logging.py index 9a66932d96a..f30e6e7b311 100644 --- a/sphinx/util/logging.py +++ b/sphinx/util/logging.py @@ -12,8 +12,8 @@ from docutils import nodes from docutils.utils import get_source_line +from sphinx._cli.util.colour import colourise from sphinx.errors import SphinxWarning -from sphinx.util.console import colorize if TYPE_CHECKING: from collections.abc import Iterator, Sequence, Set @@ -49,14 +49,11 @@ }, ) -COLOR_MAP: defaultdict[int, str] = defaultdict( - lambda: 'blue', - { - logging.ERROR: 'darkred', - logging.WARNING: 'red', - logging.DEBUG: 'darkgray', - }, -) +COLOR_MAP: dict[int, str] = { + logging.ERROR: 'darkred', + logging.WARNING: 'red', + logging.DEBUG: 'darkgray', +} def getLogger(name: str) -> SphinxLoggerAdapter: @@ -566,13 +563,14 @@ def get_node_location(node: Node) -> str | None: class ColorizeFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: message = super().format(record) - color = getattr(record, 'color', None) - if color is None: - color = COLOR_MAP.get(record.levelno) - - if color: - return colorize(color, message) - else: + colour_name = getattr(record, 'color', '') + if not colour_name: + colour_name = COLOR_MAP.get(record.levelno, '') + if not colour_name: + return message + try: + return colourise(colour_name, message) + except ValueError: return message