Skip to content

Commit 694e463

Browse files
masonjiansamuelcolvinclaude
authored
Respect NO_COLOR for automatic debug and pprint highlighting (#170)
Co-authored-by: Samuel Colvin <s@muelcolvin.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent c0d009c commit 694e463

6 files changed

Lines changed: 117 additions & 8 deletions

File tree

‎devtools/ansi.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from enum import IntEnum
55
from typing import Any, TextIO
66

7-
from .utils import isatty
7+
from .utils import auto_highlight
88

99
__all__ = 'sformat', 'sprint'
1010

@@ -151,7 +151,7 @@ def __call__(
151151
file: TextIO | None = None,
152152
**print_kwargs: Any,
153153
) -> None:
154-
text = sformat(input, *styles, reset=reset, apply=isatty(file))
154+
text = sformat(input, *styles, reset=reset, apply=auto_highlight(file))
155155
print(text, flush=flush, file=file, **print_kwargs)
156156

157157
def __getattr__(self, item: str) -> str:

‎devtools/prettier.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from functools import cache
77
from typing import TYPE_CHECKING, Any, TextIO, cast
88

9-
from .utils import DataClassType, LaxMapping, SQLAlchemyClassType, env_true, isatty
9+
from .utils import DataClassType, LaxMapping, SQLAlchemyClassType, auto_highlight, env_true
1010

1111
if TYPE_CHECKING:
1212
from pygments.formatters import Terminal256Formatter
@@ -316,5 +316,5 @@ def _format_fields(
316316

317317

318318
def pprint(s: Any, file: TextIO | None = None) -> None:
319-
highlight = isatty(file) if force_highlight is None else force_highlight
319+
highlight = auto_highlight(file) if force_highlight is None else force_highlight
320320
print(pformat(s, highlight=highlight), file=file, flush=True)

‎devtools/utils.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'DataClassType',
77
'LaxMapping',
88
'SQLAlchemyClassType',
9+
'auto_highlight',
910
'env_bool',
1011
'env_true',
1112
'is_literal',
@@ -99,8 +100,16 @@ def use_highlight(highlight: bool | None = None, file_: Any = None) -> bool:
99100
return highlight
100101

101102
if sys.platform == 'win32': # pragma: no cover
102-
return isatty(file_) and activate_win_color()
103-
return isatty(file_)
103+
return auto_highlight(file_) and activate_win_color()
104+
return auto_highlight(file_)
105+
106+
107+
def auto_highlight(file_: Any = None) -> bool:
108+
"""
109+
Whether to colour output when nothing has asked for it explicitly: only when writing to a terminal and
110+
`NO_COLOR` (https://no-color.org/) is unset or empty.
111+
"""
112+
return not os.getenv('NO_COLOR') and isatty(file_)
104113

105114

106115
def is_literal(s: Any) -> bool:

‎docs/usage.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ and readable way to print stuff during development. (If you know why this is, I'
1818
* each argument is printed "pretty" on a new line, see [prettier print](#prettier-print)
1919
* if `pygments` is installed the output is highlighted
2020

21+
`debug`, `pprint` and `sprint` highlight output automatically when writing to a terminal. Set
22+
[`NO_COLOR`](https://no-color.org/) to any non-empty value (including `0`) to disable this default
23+
highlighting. An empty value keeps automatic highlighting enabled. The program-specific
24+
`PY_DEVTOOLS_HIGHLIGHT` setting overrides `NO_COLOR`, and an explicit `Debug(highlight=True)` or
25+
`Debug(highlight=False)` takes precedence over both environment variables. Explicit highlighting
26+
with `pformat(value, highlight=True)` also remains available.
27+
2128
A more complex example of `debug` shows more of what it can do.
2229

2330
```py

‎tests/test_colors.py‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import io
2+
3+
import pytest
4+
5+
import devtools.prettier
6+
from devtools import Debug, pformat, pprint, sprint
7+
8+
9+
@pytest.mark.parametrize('printer', ['debug', 'pprint'])
10+
@pytest.mark.parametrize('tty', [False, True])
11+
@pytest.mark.parametrize('no_color', [None, '', '1', '0', 'false'])
12+
def test_no_color_default(monkeypatch: pytest.MonkeyPatch, printer: str, tty: bool, no_color: str | None):
13+
monkeypatch.delenv('PY_DEVTOOLS_HIGHLIGHT', raising=False)
14+
monkeypatch.setattr(devtools.prettier, 'force_highlight', None)
15+
if no_color is None:
16+
monkeypatch.delenv('NO_COLOR', raising=False)
17+
else:
18+
monkeypatch.setenv('NO_COLOR', no_color)
19+
output = io.StringIO()
20+
monkeypatch.setattr(output, 'isatty', lambda: tty)
21+
monkeypatch.setattr('devtools.utils.activate_win_color', lambda: True)
22+
23+
if printer == 'debug':
24+
Debug()(1, file_=output)
25+
else:
26+
pprint(1, file=output)
27+
28+
assert ('\x1b[' in output.getvalue()) is (tty and not no_color)
29+
30+
31+
@pytest.mark.parametrize('printer', ['debug', 'pprint'])
32+
@pytest.mark.parametrize('tty', [False, True])
33+
@pytest.mark.parametrize('highlight', [False, True])
34+
def test_no_color_config_override(monkeypatch: pytest.MonkeyPatch, printer: str, tty: bool, highlight: bool):
35+
monkeypatch.setenv('NO_COLOR', '1')
36+
monkeypatch.setenv('PY_DEVTOOLS_HIGHLIGHT', str(highlight))
37+
monkeypatch.setattr(devtools.prettier, 'force_highlight', highlight)
38+
output = io.StringIO()
39+
monkeypatch.setattr(output, 'isatty', lambda: tty)
40+
41+
if printer == 'debug':
42+
Debug()(1, file_=output)
43+
else:
44+
pprint(1, file=output)
45+
46+
assert ('\x1b[' in output.getvalue()) is highlight
47+
48+
49+
@pytest.mark.parametrize('highlight', [False, True])
50+
@pytest.mark.parametrize('tty', [False, True])
51+
def test_no_color_explicit_debug_override(monkeypatch: pytest.MonkeyPatch, highlight: bool, tty: bool):
52+
monkeypatch.setenv('NO_COLOR', '1')
53+
monkeypatch.setenv('PY_DEVTOOLS_HIGHLIGHT', str(not highlight))
54+
output = io.StringIO()
55+
monkeypatch.setattr(output, 'isatty', lambda: tty)
56+
57+
Debug(highlight=highlight)(1, file_=output)
58+
59+
assert ('\x1b[' in output.getvalue()) is highlight
60+
61+
62+
def test_no_color_explicit_pformat_override(monkeypatch: pytest.MonkeyPatch):
63+
monkeypatch.setenv('NO_COLOR', '1')
64+
assert '\x1b[' not in pformat(1)
65+
assert '\x1b[' in pformat(1, highlight=True)
66+
67+
68+
def test_pprint_highlight_config_snapshot(monkeypatch: pytest.MonkeyPatch):
69+
monkeypatch.delenv('NO_COLOR', raising=False)
70+
monkeypatch.setenv('PY_DEVTOOLS_HIGHLIGHT', 'True')
71+
monkeypatch.setattr(devtools.prettier, 'force_highlight', None)
72+
output = io.StringIO()
73+
monkeypatch.setattr(output, 'isatty', lambda: False)
74+
75+
pprint(1, file=output)
76+
77+
assert output.getvalue() == '1\n'
78+
79+
80+
@pytest.mark.parametrize('no_color', [None, '', '1'])
81+
def test_sprint_no_color(monkeypatch: pytest.MonkeyPatch, no_color: str | None):
82+
if no_color is None:
83+
monkeypatch.delenv('NO_COLOR', raising=False)
84+
else:
85+
monkeypatch.setenv('NO_COLOR', no_color)
86+
output = io.StringIO()
87+
monkeypatch.setattr(output, 'isatty', lambda: True)
88+
89+
sprint('hello', sprint.green, file=output)
90+
91+
assert ('\x1b[' in output.getvalue()) is (not no_color)

‎tests/test_utils.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,16 @@ def test_use_highlight_manually_set(monkeypatch: pytest.MonkeyPatch):
3737

3838
@pytest.mark.skipif(sys.platform == 'win32', reason='windows os')
3939
def test_use_highlight_auto_not_win(monkeypatch: pytest.MonkeyPatch):
40-
monkeypatch.delenv('TEST_DONT_USE_HIGHLIGHT', raising=False)
40+
monkeypatch.delenv('NO_COLOR', raising=False)
41+
monkeypatch.delenv('PY_DEVTOOLS_HIGHLIGHT', raising=False)
4142
monkeypatch.setattr(devtools.utils, 'isatty', fake_isatty)
4243
assert use_highlight() is True
4344

4445

4546
@pytest.mark.skipif(sys.platform != 'win32', reason='not windows os')
4647
def test_use_highlight_auto_win(monkeypatch: pytest.MonkeyPatch):
47-
monkeypatch.delenv('TEST_DONT_USE_HIGHLIGHT', raising=False)
48+
monkeypatch.delenv('NO_COLOR', raising=False)
49+
monkeypatch.delenv('PY_DEVTOOLS_HIGHLIGHT', raising=False)
4850
monkeypatch.setattr(devtools.utils, 'isatty', fake_isatty)
4951

5052
assert use_highlight() is True

0 commit comments

Comments
 (0)