Skip to content

Colourful pip help#13649

Merged
notatallshaw merged 2 commits intopypa:mainfrom
ichard26:rich-help
Jan 30, 2026
Merged

Colourful pip help#13649
notatallshaw merged 2 commits intopypa:mainfrom
ichard26:rich-help

Conversation

@ichard26
Copy link
Member

Supersedes PR #12143. I've copied the work done in the original PR, but simplified the code and added support for PIP_NO_COLOR and NO_COLOR since I imagine some people won't want the colours.

image

Copy link
Contributor

@hugovk hugovk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice improvement!


It could be useful to have a general PIP_COLORS rather than PIP_NO_COLOR. Compare CPython's PYTHON_COLORS and pytest's PY_COLORS.


I find the --args cyan too close to the <name> dark cyan. In fact, I didn't even spot the difference at first:

image

Maybe use something more contrasting?

A comparison with argparse help, which uses green for short options and cyan for long:

image

Similar to Typer:

image

@hugovk hugovk mentioned this pull request Nov 16, 2025
@ichard26
Copy link
Member Author

It could be useful to have a general PIP_COLORS rather than PIP_NO_COLOR. Compare CPython's PYTHON_COLORS and pytest's PY_COLORS.

I agree, but this isn't the right PR to add that.

Anyway, I implemented your suggestions, opting to reuse the colour scheme used by argparse.

image

The green short and cyan long options are still similar, but that's because I have my terminal ANSI colours set weirdly (I choose what looked pretty over contrast 🙂).

@hugovk
Copy link
Contributor

hugovk commented Nov 17, 2025

Thanks, and here it is with default colours:

image

The dark orange headings (Usage/Description/etc) are different to CPython's blue, and I think the blue is more readable on a light terminal background.

@ichard26
Copy link
Member Author

The blue blends with the option colours, but sure. If matching CPython is what we want to do, we can do that.

@ichard26
Copy link
Member Author

Is anyone else interested in reviewing this PR? If not, I'll probably merge this at a later date (once I double check behaviour on Windows). The idea was already approved in #12143 and now this PR cleans up the implementation. It is still a bit more complicated than I would've wished, but this part of the codebase is incredibly stable so it has minimal impact on maintenance.

@ichard26
Copy link
Member Author

If not, I'll probably merge this at a later date (once I double check behaviour on Windows).

Annnnnd this is indeed broken on legacy Windows console. It should be fixable by replacing the weird console capturing logic with our pre-existing pip console infrastructure though. It should be a straightforward change, but will touch many lines of this PR so I'll get around to that later.

@ichard26 ichard26 marked this pull request as draft November 28, 2025 03:03
Use Rich color markup within our help output.

Unfortunately, I had to copy the format_option() method from optparse
as it needs to be patched to ignore the Rich `[...]` markup tags while
calculating length for text wrapping (otherwise the help is wrapped
horribly).

While this patch shares very little code with Ali's original patch,
they do deserve credit for the idea and initial implementation.

Co-authored-by: Ali Hamdan <ali.hamdan.dev@gmail.com>
@ichard26
Copy link
Member Author

OK, I've pushed an update which rewrites the PR to let rich handle the console markup rendering for us. It has massively simplified the patch and fixes Windows command prompt:

image

@ichard26 ichard26 marked this pull request as ready for review December 19, 2025 16:56
Copy link
Member

@pradyunsg pradyunsg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to have tests to validate the behaviours here!

@henryiii
Copy link
Contributor

henryiii commented Jan 23, 2026

Here's my test, added to the bottom of tests/functional/test_help.py:

@pytest.mark.parametrize("envvar", ["", "NO_COLOR", "PIP_NO_COLOR"])
def test_help_command_colors(
    in_memory_pip: InMemoryPip, monkeypatch: pytest.MonkeyPatch, envvar: str
) -> None:
    """
    Test if color disables correctly.
    """
    monkeypatch.delenv("NO_COLOR", raising=False)
    monkeypatch.delenv("PIP_NO_COLOR", raising=False)
    monkeypatch.delenv("FORCE_COLOR", raising=False)

    PipConsole = pip._internal.cli.parser.PipConsole

    TestConsole = functools.partial(
        PipConsole, force_terminal=True, color_system="standard", width=80
    )
    monkeypatch.setattr(pip._internal.cli.parser, "PipConsole", TestConsole)

    if envvar:
        monkeypatch.setenv(envvar, "1")

    res = in_memory_pip.pip("help")
    text = Text.from_ansi(res.stdout)

    bold_spans = [s for s in text.spans if getattr(s.style, "bold", None)]
    bold_text = [text.plain[s.start : s.end] for s in bold_spans]

    assert bold_text == ["Usage:", "Commands:", "General Options:"]

    for span in bold_spans:
        style = span.style
        assert not isinstance(style, str)
        if envvar:
            assert style.color is None
        else:
            assert style.color is not None

You'll also need to adjust the imports:

diff --git a/tests/functional/test_help.py b/tests/functional/test_help.py
index cba036927..802e0567e 100644
--- a/tests/functional/test_help.py
+++ b/tests/functional/test_help.py
@@ -1,7 +1,11 @@
+import functools
 from unittest.mock import Mock

 import pytest

+from pip._vendor.rich.text import Text
+
+import pip._internal.cli.parser
 from pip._internal.cli.status_codes import ERROR, SUCCESS
 from pip._internal.commands import commands_dict, create_command
 from pip._internal.exceptions import CommandError
@@ -116,3 +120,41 @@ def test_help_commands_equally_functional(in_memory_pip: InMemoryPip) -> None:

I also added dev = [{ include-group = "test" }] to pyproject.toml, which makes uv run pytest tests/functional/test_help.py -k color -v work out of the box on tests that don't need the wheel download, great for testing, adding breakpoints, etc.

@henryiii
Copy link
Contributor

And unit tests in tests/unit/test_cli_colors.py (new file):

import optparse

import pip._internal.cli.parser


def test_color_formatter_option_strings() -> None:
    formatter = pip._internal.cli.parser.PrettyHelpFormatter()
    strs = formatter.format_option_strings(
        optparse.Option(
            "--test-option",
            "-t",
            help="A test option for colors",
        )
    )

    assert (
        "[optparse.shortargs]-t[/], [optparse.longargs]--test-option[/]"
        " [optparse.metavar]<test_option>[/]" == strs
    )


def test_color_formatter_usage() -> None:
    formatter = pip._internal.cli.parser.PrettyHelpFormatter()
    usage = formatter.format_usage("usage: %prog [options] arg1 arg2")

    assert (
        "\n[optparse.groups]Usage:[/]   usage: %prog \\[options] arg1 arg2\n" == usage
    )


def test_color_formatter_section_heading() -> None:
    formatter = pip._internal.cli.parser.PrettyHelpFormatter()
    assert formatter.format_heading("Options") == ""
    heading = formatter.format_heading("Other")

    assert "[optparse.groups]Other:[/]\n" == heading


def test_color_formatter_description() -> None:
    formatter = pip._internal.cli.parser.PrettyHelpFormatter()
    description = formatter.format_description("This is a test description.")

    assert (
        "[optparse.groups]Description:[/]\n  This is a test description.\n"
        == description
    )


def test_color_formatter_expand_defaults() -> None:
    parser = pip._internal.cli.parser.CustomOptionParser(
        formatter=pip._internal.cli.parser.PrettyHelpFormatter()
    )
    option = optparse.Option(
        "--example",
        help="An example option [default: %default]",
        default="default_value",
    )
    parser.add_option(option)
    expanded = parser.formatter.expand_default(option)

    assert "An example option \\[default: default_value]" == expanded

Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
@ichard26
Copy link
Member Author

Thanks for the tests @henryiii! If you're okay with it, I'd like to get this in for pip 26.0 @notatallshaw.

@ichard26 ichard26 added this to the 26.0 milestone Jan 25, 2026
Copy link
Member

@notatallshaw notatallshaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Colors are cool but I'm far from an expert and don't really want to weigh in too much. The code looks fine so I am approving this.

I will say, I did try it all of VS Code's default themes I think it does make the options in light mode terminals a tiny bit less readable on my screen, where as in dark modes the options felt easier to read.

I'm not sure if a screenshot will help, this because I think it's my monitor's sub-pixel layout, VS Code's rendering choices, and my aging eye sight that come into play here, but there's one anyway.

image

Like I say, if there is a readability difference it is tiny.

@notatallshaw
Copy link
Member

It's approved, the code looks good, we have tests, multiple people like the look of it, merging.

@notatallshaw notatallshaw merged commit aa2b770 into pypa:main Jan 30, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants