|
4 | 4 |
|
5 | 5 | import os |
6 | 6 | import sys |
| 7 | +from typing import Callable |
7 | 8 |
|
8 | | -try: |
9 | | - # check if colorama is installed to support color on Windows |
| 9 | +if sys.platform == 'win32': |
10 | 10 | import colorama |
11 | | -except ImportError: |
12 | | - colorama = None |
13 | 11 |
|
14 | 12 |
|
15 | | -codes: dict[str, str] = {} |
| 13 | +_COLOURING_DISABLED = False |
16 | 14 |
|
17 | 15 |
|
18 | 16 | def color_terminal() -> bool: |
| 17 | + """Return True if coloured terminal output is supported.""" |
19 | 18 | if 'NO_COLOR' in os.environ: |
20 | 19 | return False |
21 | | - if sys.platform == 'win32' and colorama is not None: |
22 | | - colorama.init() |
23 | | - return True |
24 | 20 | if 'FORCE_COLOR' in os.environ: |
25 | 21 | return True |
26 | | - if not hasattr(sys.stdout, 'isatty'): |
| 22 | + try: |
| 23 | + if not sys.stdout.isatty(): |
| 24 | + return False |
| 25 | + except (AttributeError, ValueError): |
| 26 | + # Handle cases where .isatty() is not defined, or where e.g. |
| 27 | + # "ValueError: I/O operation on closed file" is raised |
27 | 28 | return False |
28 | | - if not sys.stdout.isatty(): |
| 29 | + if os.environ.get("TERM", "").lower() in {"dumb", "unknown"}: |
| 30 | + # Do not colour output if on a dumb terminal |
29 | 31 | return False |
30 | | - if 'COLORTERM' in os.environ: |
31 | | - return True |
32 | | - term = os.environ.get('TERM', 'dumb').lower() |
33 | | - if term in ('xterm', 'linux') or 'color' in term: |
34 | | - return True |
35 | | - return False |
| 32 | + if sys.platform == 'win32': |
| 33 | + colorama.init() |
| 34 | + return True |
36 | 35 |
|
37 | 36 |
|
38 | 37 | def nocolor() -> None: |
39 | | - if sys.platform == 'win32' and colorama is not None: |
| 38 | + global _COLOURING_DISABLED |
| 39 | + _COLOURING_DISABLED = True |
| 40 | + if sys.platform == 'win32': |
40 | 41 | colorama.deinit() |
41 | | - codes.clear() |
42 | 42 |
|
43 | 43 |
|
44 | 44 | def coloron() -> None: |
45 | | - codes.update(_orig_codes) |
| 45 | + global _COLOURING_DISABLED |
| 46 | + _COLOURING_DISABLED = False |
| 47 | + if sys.platform == 'win32': |
| 48 | + colorama.init() |
46 | 49 |
|
47 | 50 |
|
48 | 51 | def colorize(name: str, text: str, input_mode: bool = False) -> str: |
49 | | - def escseq(name: str) -> str: |
50 | | - # Wrap escape sequence with ``\1`` and ``\2`` to let readline know |
51 | | - # it is non-printable characters |
52 | | - # ref: https://tiswww.case.edu/php/chet/readline/readline.html |
53 | | - # |
54 | | - # Note: This hack does not work well in Windows (see #5059) |
55 | | - escape = codes.get(name, '') |
56 | | - if input_mode and escape and sys.platform != 'win32': |
57 | | - return '\1' + escape + '\2' |
58 | | - else: |
59 | | - return escape |
60 | | - |
61 | | - return escseq(name) + text + escseq('reset') |
62 | | - |
63 | | - |
64 | | -def create_color_func(name: str) -> None: |
| 52 | + if _COLOURING_DISABLED: |
| 53 | + return text |
| 54 | + |
| 55 | + if sys.platform == 'win32' or not input_mode: |
| 56 | + return globals()[name](text) |
| 57 | + |
| 58 | + # Wrap escape sequence with ``\1`` and ``\2`` to let readline know |
| 59 | + # it is non-printable characters |
| 60 | + # ref: https://tiswww.case.edu/php/chet/readline/readline.html |
| 61 | + # |
| 62 | + # Note: This does not work well in Windows (see |
| 63 | + # https://github.com/sphinx-doc/sphinx/pull/5059) |
| 64 | + escape_code = getattr(globals()[name], '__escape_code', '39;49;00') |
| 65 | + return f'\1\x1b[{escape_code}m\2{text}\1\x1b[39;49;00m\2' |
| 66 | + |
| 67 | + |
| 68 | +def _create_colour_func( |
| 69 | + __escape_code: str, |
| 70 | +) -> Callable[[str], str]: |
65 | 71 | def inner(text: str) -> str: |
66 | | - return colorize(name, text) |
67 | | - globals()[name] = inner |
68 | | - |
69 | | - |
70 | | -_attrs = { |
71 | | - 'reset': '39;49;00m', |
72 | | - 'bold': '01m', |
73 | | - 'faint': '02m', |
74 | | - 'standout': '03m', |
75 | | - 'underline': '04m', |
76 | | - 'blink': '05m', |
77 | | -} |
78 | | - |
79 | | -for _name, _value in _attrs.items(): |
80 | | - codes[_name] = '\x1b[' + _value |
81 | | - |
82 | | -_colors = [ |
83 | | - ('black', 'darkgray'), |
84 | | - ('darkred', 'red'), |
85 | | - ('darkgreen', 'green'), |
86 | | - ('brown', 'yellow'), |
87 | | - ('darkblue', 'blue'), |
88 | | - ('purple', 'fuchsia'), |
89 | | - ('turquoise', 'teal'), |
90 | | - ('lightgray', 'white'), |
91 | | -] |
92 | | - |
93 | | -for i, (dark, light) in enumerate(_colors, 30): |
94 | | - codes[dark] = '\x1b[%im' % i |
95 | | - codes[light] = '\x1b[%im' % (i + 60) |
96 | | - |
97 | | -_orig_codes = codes.copy() |
98 | | - |
99 | | -for _name in codes: |
100 | | - create_color_func(_name) |
| 72 | + if _COLOURING_DISABLED: |
| 73 | + return text |
| 74 | + return f'\x1b[{__escape_code}m{text}\x1b[39;49;00m' |
| 75 | + # private attribute, only for ``colorize()`` |
| 76 | + inner.__escape_code = __escape_code |
| 77 | + return inner |
| 78 | + |
| 79 | + |
| 80 | +reset = _create_colour_func('39;49;00') |
| 81 | +bold = _create_colour_func('01') |
| 82 | +# faint = _create_colour_func('02') |
| 83 | +# standout = _create_colour_func('03') |
| 84 | +# underline = _create_colour_func('04') |
| 85 | +# blink = _create_colour_func('05') |
| 86 | + |
| 87 | +black = _create_colour_func('30') |
| 88 | +darkgray = _create_colour_func('90') |
| 89 | +darkred = _create_colour_func('31') |
| 90 | +red = _create_colour_func('91') |
| 91 | +darkgreen = _create_colour_func('32') |
| 92 | +green = _create_colour_func('92') |
| 93 | +brown = _create_colour_func('33') |
| 94 | +yellow = _create_colour_func('93') |
| 95 | +darkblue = _create_colour_func('34') |
| 96 | +blue = _create_colour_func('94') |
| 97 | +purple = _create_colour_func('35') |
| 98 | +fuchsia = _create_colour_func('95') |
| 99 | +turquoise = _create_colour_func('36') |
| 100 | +teal = _create_colour_func('96') |
| 101 | +lightgray = _create_colour_func('37') |
| 102 | +white = _create_colour_func('97') |
0 commit comments