-
-
Notifications
You must be signed in to change notification settings - Fork 31.8k
gh-130645: Add colour to argparse
help
#132323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
🤖 New build scheduled with the buildbot fleet by @hugovk for commit 3d174b7 🤖 Results will be shown at: https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F132323%2Fmerge If you want to schedule another build, you need to add the 🔨 test-with-buildbots label again. |
indent_increment=2, | ||
max_help_position=24, | ||
width=None, | ||
prefix_chars='-', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm assuming you just wanted to add prefix_chars
to the constructor for consistency reasons and I'm not missing something else, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do need to pass the prefix chars from the ArgumentParser
to the HelpFormatter
, so we can distinguish short options from long options.
For example, with this:
import argparse
parser = argparse.ArgumentParser(prefix_chars="-+", color=True)
parser.add_argument("+a")
parser.add_argument("-b")
parser.add_argument("++bleep")
parser.add_argument("-+bloop")
parser.add_argument("-c", "--count")
parser.add_argument("-d", "-+donut")
args = parser.parse_args()
Commenting out line 2735, we don't recognise +a
as a short option, don't recognise ++bleep
as a long option, and misrecognise -+bloop
and -+donut
as short options:

With this PR, we get:

|
||
if color and can_colorize(): | ||
self._ansi = ANSIColors() | ||
self._decolor = decolor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of having to do this, couldn't we just set color
enabled in the constructor and then also define something like:
def color_me(text, some_color):
if color:
return f"{some_color}{text}"
return text
...Then wrap all of the places we want to colorize? I think that way we could remove the ANSI color stripping and make this a bit easier to follow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One place we're using this _decolor
is in _format_usage
for calculating the offsets.
We've already processed all the bits and constructed a string, and added colour relevant for each bit. Here's what the full string looks like:

We then want to post-process it check:
- will it even fit on one line
- if not, calculate the offset for each line, based on the offset for the "usage" prefix and
prog
name:

If we had not already added colour, we would have to somehow figure out where to insert the colour, which I think would be more complicated.
But because we already have colour, we need to make sure the ANSI colour characters aren't included in the width calculations. It seems simpler to use the len()
of a colour-stripped string, than the len()
of the one with colour.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other place we#re using _decolor
is in calclating the padding of actions in _format_action
.
So given desired output like:
We need to calculate the red lines here, the gap between the action_header
(for example, x
) and the help (for example, the base
):
There are three cases:
- No help: easy, we don't need to calculate a gap. Colour doesn't matter.
- Long action name: fairly easy, action goes on one line, help on a new line. Help has no colour, we don't need to worry about colour codes when calculating the offset.
- Short action name: bit harder: action and help fit on one line.
We're using this formatter:
tup = self._current_indent, '', action_width, action_header
action_header = '%*s%-*s ' % tup
%*s
means take a min width from the first value -- self._current_indent
-- and use it with the next value -- ''
-- so just apply the indent. This is okay.
%-*s
is similar but means left-align. action_width
is the space we have to left align and action_header
is something like x
. But if we have ANSI codes, we'll count some of these in the width and the gap will be too short.
https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
So we need to calculate this without colour too. Again, some of these strings can have multiple colours applied, so it seems easier to remove the codes when calculating widths, rather than somehow needing to reconstruct the string or re-insert colour.
However, I've noticed I can make this particular change more focused, only for this third case, so I've committed that (c0a0688).
When you're done making the requested changes, leave the comment: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A comment about the approach and whether we can simplify a bit.
Many argparse CLIs I see already use keywords for most parameters, but I think we should make these new ones keyword-only, especially for new bools, as a long list of
What do you think? |
Add
color
parameter toargparse.ArgumentParser
, which by default isFalse
. When set toTrue
, the help text is in colour if allowed (for example, we're not piping,NO_COLOR=1
is not set).Example output
Using this script:
Performance
With the same script and PGO+LTO on macOS, running it once (
time ./python.exe argparse-cli.py --help
) and 100 times (time ./100.sh
):main
(3feac7a):PR and colour not enabled:
PR and colour enabled:
Something like 0.65 ms difference per run between before and after, averaging from
100.sh
.argparse
help #130645📚 Documentation preview 📚: https://cpython-previews--132323.org.readthedocs.build/