Repository navigation
Give NO_COLOR precedence in Ruff and ty diagnostics - #29898
Conversation
Typing conformance resultsNo changes detected βCurrent numbersThe percentage of diagnostics emitted that were expected errors held steady at 98.60%. The percentage of expected errors that received a diagnostic held steady at 98.86%. The number of fully passing files held steady at 137/146. Results for baa3088 Β· Workflow run |
Memory usage reportMemory usage unchanged β Results for baa3088 Β· Workflow run |
|
|
NO_COLOR precedence in Ruff and ty diagnosticsNO_COLOR precedence in Ruff and ty diagnostics
On `main`, `ruff check --color=always example.py >report.txt` saves colored diagnostics, but replacing `>report.txt` with `--output-file report.txt` strips the color. This PR makes both ways of saving the report honor `--color=always`. Automatic color also goes wrong with `ruff check --fix -`. Ruff writes the fixed source to stdout and diagnostics to stderr, yet it checks whether stdout is a terminal to decide whether to color the diagnostics. Redirecting only stderr can put ANSI escapes in the saved report, while redirecting only stdout can leave the diagnostics plain on the terminal. Ruff now applies the color policy from #29898 to the destination that receives the diagnostics. When the flags and environment leave color up to terminal detection, Ruff checks that destination. With `--diff -`, for example, a patch printed to the terminal on stdout can keep its color while the fix summary redirected from stderr stays plain. ## Test plan This video compares the base of this PR (#29898 at `baa3088701`) with the fix (`779a511358`). Each command uses the same one-line input, `print(unknown)`. https://github.com/user-attachments/assets/d55b0fb3-60aa-4ac8-806c-f51c394890f5 <details> <summary>Manual split-stream reproduction and results</summary> Run these commands from a terminal in a scratch directory, first with `ruff` from the base commit (`baa3088701`) on `PATH`, then with the build from this PR. The `tr` pipelines count escape bytes in the redirected files. ```sh unset NO_COLOR FORCE_COLOR CLICOLOR CLICOLOR_FORCE export TERM=xterm-256color FORCE_HYPERLINK=0 check_stdin() { ruff check --isolated --no-cache --color=auto --output-format=concise "$@" - } printf 'print(unknown)\n' | check_stdin --fix --select F821 2>err LC_ALL=C tr -cd '\033' <err | wc -c printf 'print(unknown)\n' | check_stdin --fix --select F821 >out cat out ``` On the base commit, `err` contained 11 escape bytes. With this change it contains zero. Reversing the redirection (`>out`) changed the F821 diagnostic on the terminal from plain to colored. On both revisions, `out` contained the unchanged source, `print(unknown)`. With `--diff`, stdout carries the patch and stderr carries the fix summary. ```sh printf 'import os\nprint(unknown)\n' | check_stdin --diff --config 'show-fixes = true' 2>summary LC_ALL=C tr -cd '\033' <summary | wc -c printf 'import os\nprint(unknown)\n' | check_stdin --diff --config 'show-fixes = true' >patch LC_ALL=C tr -cd '\033' <patch | wc -c ``` The removed import was red on the terminal on both revisions, while the escape count in `summary` dropped from 10 to zero. With `>patch`, the patch contained zero escapes on both revisions, and the fix summary on the terminal changed from plain to colored. </details>
|
|
I wasn't sure if consistency with other tools was worth the internal inconsistency here (where providing |
Summary
Today, if a CI job exports
FORCE_COLOR=1, runningNO_COLOR=1 ruff check .still prints colored diagnostics. The same happens withty check. Giving a non-emptyNO_COLORpriority lets users request plain output without having to unset a variable inherited from their environment. This matches how uv and the Python interpreter resolve that conflict. Users who want color for one command can still pass--color=always. uvβs resolver Β· Pythonβs color precedencePreviously, the shared resolver turned
FORCE_COLORinto an override beforecoloredcould honorNO_COLOR. The resolver now handles the color environment for both tools, andcoloredstyles diagnostics on stdout according to that decision. The first matching rule wins.--color=alwaysor--color=never.NO_COLORdisables color.FORCE_COLORorCLICOLOR_FORCEenables color, including in a pipe.CLICOLORis non-empty orTERMis anything other thandumb(including unset).All four color variables treat
0as non-empty and an empty string as unset. In particular,FORCE_COLOR=""previously disabled color. It now lets the remaining settings and terminal detection decide.Comparison with the upstream conventions
NO_COLORconvention specifies that a non-empty value disables color by default and that command-line options can override it. It gives no rule for a conflict withFORCE_COLOR. Its FAQ also limits the convention to color, while Ruff and ty suppress other diagnostic styling, such as bold text, when color is disabled.FORCE_COLORconvention specifies that any non-empty value enables color, even in a pipe. Its example appliesFORCE_COLORlast, so the example produces colored output when both variables are set. Here we follow uv and CPython in givingNO_COLORpriority.NO_COLORpriority overCLICOLOR_FORCEand makesCLICOLORa terminal-only request. We follow those rules, addFORCE_COLORas an alias forCLICOLOR_FORCE, and useTERM=dumbfor the default the spec leaves to applications.Comparison with uv
When
--coloris omitted, uv also checks non-emptyNO_COLORbefore either force variable. In Ruff and ty, passing--color=autohas the same effect as omitting it. In uv, passing it delegates the decision toanstream, which recognizesCLICOLOR_FORCEbut ignoresFORCE_COLOR.The terminal heuristics also differ: uv delegates to anstream, which handles
TERM=dumbdifferently depending on which platform uv is being run on, and also honours theCIvariable. I'd prefer not to have platform-dependent behaviour or to have behaviour automatically be different in CI; that feels too confusing/magical to me. uvβs resolver Β·anstreamdetection Β·anstyle-queryenvironment checksEach example assumes other color settings are unset.
FORCE_COLOR=1with--color=auto, output pipedCLICOLOR=0 TERM=xterm, on a terminalCLICOLOR="" TERM=dumb, on a terminalCI=1 TERM=dumb, on a terminalTERMunset, on a terminalAn empty
TERMallows color on a terminal in both implementations.