Skip to content

Commit 230d658

Browse files
hugovkambv
authored andcommitted
Add theming to argparse
1 parent 965c7cf commit 230d658

File tree

2 files changed

+52
-36
lines changed

2 files changed

+52
-36
lines changed

Lib/_colorize.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,22 @@ def __iter__(self) -> Iterator[str]:
135135
return iter(self.__dataclass_fields__)
136136

137137

138+
@dataclass(frozen=True)
139+
class Argparse(ThemeSection):
140+
usage: str = ANSIColors.BOLD_BLUE
141+
prog: str = ANSIColors.BOLD_MAGENTA
142+
prog_extra: str = ANSIColors.MAGENTA
143+
heading: str = ANSIColors.BOLD_BLUE
144+
summary_long_option: str = ANSIColors.CYAN
145+
summary_short_option: str = ANSIColors.GREEN
146+
summary_label: str = ANSIColors.YELLOW
147+
long_option: str = ANSIColors.BOLD_CYAN
148+
short_option: str = ANSIColors.BOLD_GREEN
149+
label: str = ANSIColors.BOLD_YELLOW
150+
action: str = ANSIColors.BOLD_GREEN
151+
reset: str = ANSIColors.RESET
152+
153+
138154
@dataclass(frozen=True)
139155
class REPL(ThemeSection):
140156
prompt: str = ANSIColors.BOLD_MAGENTA
@@ -177,13 +193,15 @@ class Theme:
177193
When adding a new one, remember to also modify `copy_with` and `no_colors`
178194
below.
179195
"""
196+
argparse: Argparse = field(default_factory=Argparse)
180197
repl: REPL = field(default_factory=REPL)
181198
traceback: Traceback = field(default_factory=Traceback)
182199
unittest: Unittest = field(default_factory=Unittest)
183200

184201
def copy_with(
185202
self,
186203
*,
204+
argparse: Argparse | None = None,
187205
repl: REPL | None = None,
188206
traceback: Traceback | None = None,
189207
unittest: Unittest | None = None,
@@ -194,6 +212,7 @@ def copy_with(
194212
could lead to invalid terminal states.
195213
"""
196214
return type(self)(
215+
argparse=argparse or self.argparse,
197216
repl=repl or self.repl,
198217
traceback=traceback or self.traceback,
199218
unittest=unittest or self.unittest,
@@ -207,6 +226,7 @@ def no_colors(self) -> Self:
207226
and possible, and empty strings otherwise.
208227
"""
209228
return type(self)(
229+
argparse=self.argparse.no_colors(),
210230
repl=self.repl.no_colors(),
211231
traceback=self.traceback.no_colors(),
212232
unittest=self.unittest.no_colors(),

Lib/argparse.py

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,13 @@ def __init__(
176176
width = shutil.get_terminal_size().columns
177177
width -= 2
178178

179-
from _colorize import ANSIColors, NoColors, can_colorize, decolor
179+
from _colorize import can_colorize, decolor, get_theme
180180

181181
if color and can_colorize():
182-
self._ansi = ANSIColors()
182+
self._theme = get_theme(force_color=True).argparse
183183
self._decolor = decolor
184184
else:
185-
self._ansi = NoColors
185+
self._theme = get_theme(force_no_color=True).argparse
186186
self._decolor = lambda text: text
187187

188188
self._prefix_chars = prefix_chars
@@ -237,14 +237,12 @@ def format_help(self):
237237

238238
# add the heading if the section was non-empty
239239
if self.heading is not SUPPRESS and self.heading is not None:
240-
bold_blue = self.formatter._ansi.BOLD_BLUE
241-
reset = self.formatter._ansi.RESET
242-
243240
current_indent = self.formatter._current_indent
244241
heading_text = _('%(heading)s:') % dict(heading=self.heading)
242+
t = self.formatter._theme
245243
heading = (
246244
f'{" " * current_indent}'
247-
f'{bold_blue}{heading_text}{reset}\n'
245+
f'{t.heading}{heading_text}{t.reset}\n'
248246
)
249247
else:
250248
heading = ''
@@ -314,26 +312,23 @@ def _join_parts(self, part_strings):
314312
if part and part is not SUPPRESS])
315313

316314
def _format_usage(self, usage, actions, groups, prefix):
317-
bold_blue = self._ansi.BOLD_BLUE
318-
bold_magenta = self._ansi.BOLD_MAGENTA
319-
magenta = self._ansi.MAGENTA
320-
reset = self._ansi.RESET
315+
t = self._theme
321316

322317
if prefix is None:
323318
prefix = _('usage: ')
324319

325320
# if usage is specified, use that
326321
if usage is not None:
327322
usage = (
328-
magenta
323+
t.prog_extra
329324
+ usage
330-
% {"prog": f"{bold_magenta}{self._prog}{reset}{magenta}"}
331-
+ reset
325+
% {"prog": f"{t.prog}{self._prog}{t.reset}{t.prog_extra}"}
326+
+ t.reset
332327
)
333328

334329
# if no optionals or positionals are available, usage is just prog
335330
elif usage is None and not actions:
336-
usage = f"{bold_magenta}{self._prog}{reset}"
331+
usage = f"{t.prog}{self._prog}{t.reset}"
337332

338333
# if optionals and positionals are available, calculate usage
339334
elif usage is None:
@@ -411,10 +406,10 @@ def get_lines(parts, indent, prefix=None):
411406
usage = '\n'.join(lines)
412407

413408
usage = usage.removeprefix(prog)
414-
usage = f"{bold_magenta}{prog}{reset}{usage}"
409+
usage = f"{t.prog}{prog}{t.reset}{usage}"
415410

416411
# prefix with 'usage:'
417-
return f'{bold_blue}{prefix}{reset}{usage}\n\n'
412+
return f'{t.usage}{prefix}{t.reset}{usage}\n\n'
418413

419414
def _format_actions_usage(self, actions, groups):
420415
return ' '.join(self._get_actions_usage_parts(actions, groups))
@@ -452,10 +447,7 @@ def _get_actions_usage_parts(self, actions, groups):
452447

453448
# collect all actions format strings
454449
parts = []
455-
cyan = self._ansi.CYAN
456-
green = self._ansi.GREEN
457-
yellow = self._ansi.YELLOW
458-
reset = self._ansi.RESET
450+
t = self._theme
459451
for action in actions:
460452

461453
# suppressed arguments are marked with None
@@ -465,7 +457,11 @@ def _get_actions_usage_parts(self, actions, groups):
465457
# produce all arg strings
466458
elif not action.option_strings:
467459
default = self._get_default_metavar_for_positional(action)
468-
part = green + self._format_args(action, default) + reset
460+
part = (
461+
t.summary_short_option
462+
+ self._format_args(action, default)
463+
+ t.reset
464+
)
469465

470466
# if it's in a group, strip the outer []
471467
if action in group_actions:
@@ -481,20 +477,23 @@ def _get_actions_usage_parts(self, actions, groups):
481477
if action.nargs == 0:
482478
part = action.format_usage()
483479
if self._is_long_option(part):
484-
part = f"{cyan}{part}{reset}"
480+
part = f"{t.summary_long_option}{part}{t.reset}"
485481
elif self._is_short_option(part):
486-
part = f"{green}{part}{reset}"
482+
part = f"{t.summary_short_option}{part}{t.reset}"
487483

488484
# if the Optional takes a value, format is:
489485
# -s ARGS or --long ARGS
490486
else:
491487
default = self._get_default_metavar_for_optional(action)
492488
args_string = self._format_args(action, default)
493489
if self._is_long_option(option_string):
494-
option_string = f"{cyan}{option_string}"
490+
option_color = t.summary_long_option
495491
elif self._is_short_option(option_string):
496-
option_string = f"{green}{option_string}"
497-
part = f"{option_string} {yellow}{args_string}{reset}"
492+
option_color = t.summary_short_option
493+
part = (
494+
f"{option_color}{option_string} "
495+
f"{t.summary_label}{args_string}{t.reset}"
496+
)
498497

499498
# make it look optional if it's not required or in a group
500499
if not action.required and action not in group_actions:
@@ -590,17 +589,14 @@ def _format_action(self, action):
590589
return self._join_parts(parts)
591590

592591
def _format_action_invocation(self, action):
593-
bold_green = self._ansi.BOLD_GREEN
594-
bold_cyan = self._ansi.BOLD_CYAN
595-
bold_yellow = self._ansi.BOLD_YELLOW
596-
reset = self._ansi.RESET
592+
t = self._theme
597593

598594
if not action.option_strings:
599595
default = self._get_default_metavar_for_positional(action)
600596
return (
601-
bold_green
597+
t.action
602598
+ ' '.join(self._metavar_formatter(action, default)(1))
603-
+ reset
599+
+ t.reset
604600
)
605601

606602
else:
@@ -609,9 +605,9 @@ def color_option_strings(strings):
609605
parts = []
610606
for s in strings:
611607
if self._is_long_option(s):
612-
parts.append(f"{bold_cyan}{s}{reset}")
608+
parts.append(f"{t.long_option}{s}{t.reset}")
613609
elif self._is_short_option(s):
614-
parts.append(f"{bold_green}{s}{reset}")
610+
parts.append(f"{t.short_option}{s}{t.reset}")
615611
else:
616612
parts.append(s)
617613
return parts
@@ -628,7 +624,7 @@ def color_option_strings(strings):
628624
default = self._get_default_metavar_for_optional(action)
629625
option_strings = color_option_strings(action.option_strings)
630626
args_string = (
631-
f"{bold_yellow}{self._format_args(action, default)}{reset}"
627+
f"{t.label}{self._format_args(action, default)}{t.reset}"
632628
)
633629
return ', '.join(option_strings) + ' ' + args_string
634630

0 commit comments

Comments
 (0)