Skip to content

Give NO_COLOR precedence in Ruff and ty diagnostics - #29898

Merged
AlexWaygood merged 1 commit into
mainfrom
alex/cli-color-environment
Nov 2, 2026
Merged

AlexWaygood merged 1 commit into
mainfrom
alex/cli-color-environment

Conversation

@AlexWaygood

@AlexWaygood AlexWaygood commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Summary

Today, if a CI job exports FORCE_COLOR=1, running NO_COLOR=1 ruff check . still prints colored diagnostics. The same happens with ty check. Giving a non-empty NO_COLOR priority 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 precedence

Previously, the shared resolver turned FORCE_COLOR into an override before colored could honor NO_COLOR. The resolver now handles the color environment for both tools, and colored styles diagnostics on stdout according to that decision. The first matching rule wins.

  1. Use an explicit --color=always or --color=never.
  2. A non-empty NO_COLOR disables color.
  3. A non-empty FORCE_COLOR or CLICOLOR_FORCE enables color, including in a pipe.
  4. If stdout is a pipe or file, disable color.
  5. On a terminal, enable color if CLICOLOR is non-empty or TERM is anything other than dumb (including unset).

All four color variables treat 0 as 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

  • The NO_COLOR convention 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 with FORCE_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.
  • The FORCE_COLOR convention specifies that any non-empty value enables color, even in a pipe. Its example applies FORCE_COLOR last, so the example produces colored output when both variables are set. Here we follow uv and CPython in giving NO_COLOR priority.
  • The CLICOLORS specification gives NO_COLOR priority over CLICOLOR_FORCE and makes CLICOLOR a terminal-only request. We follow those rules, add FORCE_COLOR as an alias for CLICOLOR_FORCE, and use TERM=dumb for the default the spec leaves to applications.

Comparison with uv

When --color is omitted, uv also checks non-empty NO_COLOR before either force variable. In Ruff and ty, passing --color=auto has the same effect as omitting it. In uv, passing it delegates the decision to anstream, which recognizes CLICOLOR_FORCE but ignores FORCE_COLOR.

The terminal heuristics also differ: uv delegates to anstream, which handles TERM=dumb differently depending on which platform uv is being run on, and also honours the CI variable. 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 Β· anstream detection Β· anstyle-query environment checks

Each example assumes other color settings are unset.

Case Ruff and ty with this change uv
FORCE_COLOR=1 with --color=auto, output piped Colored, as when the flag is omitted Plain
CLICOLOR=0 TERM=xterm, on a terminal Colored Plain
CLICOLOR="" TERM=dumb, on a terminal Plain Colored
CI=1 TERM=dumb, on a terminal Plain Colored
TERM unset, on a terminal Colored on all platforms Plain on non-Windows platforms, colored on Windows

An empty TERM allows color on a terminal in both implementations.

@AlexWaygood AlexWaygood added cli Related to the command-line interface of Ruff or ty ty The ty type checker labels Nov 2, 2026
@astral-automations-bot

Copy link
Copy Markdown
Contributor

Typing conformance results

No changes detected βœ…

Current numbers
The 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

@astral-automations-bot

Copy link
Copy Markdown
Contributor

Memory usage report

Memory usage unchanged βœ…


Results for baa3088 Β· Workflow run

@astral-automations-bot

Copy link
Copy Markdown
Contributor

ruff-ecosystem results

Linter (stable)

βœ… ecosystem check detected no linter changes.

Linter (preview)

βœ… ecosystem check detected no linter changes.

Formatter (stable)

βœ… ecosystem check detected no format changes.

Formatter (preview)

βœ… ecosystem check detected no format changes.

Linter (stable) Β· Linter (preview) Β· Formatter (stable) Β· Formatter (preview)


Results for baa3088 Β· Workflow run

@astral-automations-bot

Copy link
Copy Markdown
Contributor

ecosystem-analyzer results

No diagnostic changes detected βœ…

Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.

Full report with detailed diff Β· Timing results


Results for baa3088 Β· Workflow run

@AlexWaygood
AlexWaygood marked this pull request as ready for review November 2, 2026 09:54
@AlexWaygood
AlexWaygood requested review from a team as code owners November 2, 2026 09:54
@AlexWaygood AlexWaygood removed the ty The ty type checker label Nov 2, 2026
@AlexWaygood AlexWaygood changed the title [ty] Give NO_COLOR precedence in Ruff and ty diagnostics Give NO_COLOR precedence in Ruff and ty diagnostics Nov 2, 2026
@AlexWaygood
AlexWaygood added this pull request to stack #29901 November 2, 2026 10:54
@AlexWaygood
AlexWaygood merged commit 815c959 into main Nov 2, 2026
81 checks passed
@AlexWaygood
AlexWaygood deleted the alex/cli-color-environment branch November 2, 2026 14:30
AlexWaygood added a commit that referenced this pull request Nov 2, 2026
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>
@MichaReiser

Copy link
Copy Markdown
Member

CLICOLOR=0 should disable color (at least that's how it is in uv and Cargo)

@AlexWaygood

Copy link
Copy Markdown
Member Author

CLICOLOR=0 should disable color (at least that's how it is in uv and Cargo)

I wasn't sure if consistency with other tools was worth the internal inconsistency here (where providing 0 is interpreted as a "positive" value for the environment variable for all environment variables except this one). It becomes rather more awkward to explain in our docs. But I can fix it up.

This branch was previously deployed

1 inactive deployment
automations β€” baa30887 Deployed Nov 2, 2026 by AlexWaygood via security-review / security review #96521
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli Related to the command-line interface of Ruff or ty

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants