From a38087e0e779610018b75e6a57754d31ef83a659 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Sun, 7 Dec 2025 14:07:01 -0800 Subject: [PATCH 1/5] Add support for [cmd] colorization in argparse help text --- Doc/library/argparse.rst | 22 ++++++ Doc/whatsnew/3.15.rst | 4 + Lib/argparse.py | 19 ++++- Lib/test/test_argparse.py | 149 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 1 deletion(-) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 71c4f094886546..8b0b9813f6510e 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -645,6 +645,28 @@ are set. .. versionadded:: 3.14 +To highlight command examples in your description or epilog text, you can use +``[cmd]...[/cmd]`` markup:: + + >>> parser = argparse.ArgumentParser( + ... formatter_class=argparse.RawDescriptionHelpFormatter, + ... epilog='''Examples: + ... [cmd]python -m myapp --verbose[/cmd] + ... [cmd]python -m myapp --config settings.json[/cmd] + ... ''') + +When colors are enabled, the text inside ``[cmd]...[/cmd]`` tags will be +displayed in a distinct color to help examples stand out. When colors are +disabled, the tags are stripped and the content is displayed as plain text. + +.. note:: + + The ``[cmd]`` markup only applies to description and epilog text processed + by :meth:`HelpFormatter._format_text`. It does not apply to individual + argument ``help`` strings. + +.. versionadded:: 3.15 + The add_argument() method ------------------------- diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 1bd82545e588fa..ed3aa4ec767438 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -407,6 +407,10 @@ argparse default to ``True``. This enables suggestions for mistyped arguments by default. (Contributed by Jakob Schluse in :gh:`140450`.) +* Added ``[cmd]...[/cmd]`` markup support in description and epilog text to + highlight command examples when color output is enabled. + (Contributed by Savannah Ostrowski in :gh:`XXXXX`.) + calendar -------- diff --git a/Lib/argparse.py b/Lib/argparse.py index 398825508f5917..b309d2f862dfa4 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -517,7 +517,24 @@ def _format_text(self, text): text = text % dict(prog=self._prog) text_width = max(self._width - self._current_indent, 11) indent = ' ' * self._current_indent - return self._fill_text(text, text_width, indent) + '\n\n' + text = self._fill_text(text, text_width, indent) + text = self._apply_text_markup(text) + return text + '\n\n' + + def _apply_text_markup(self, text): + """Apply color markup to text. + + Supported markup: + [cmd]...[/cmd] - command/shell example (single color) + """ + t = self._theme + text = _re.sub( + r'\[cmd\](.*?)\[/cmd\]', + rf'{t.prog_extra}\1{t.reset}', + text, + flags=_re.DOTALL + ) + return text def _format_action(self, action): # determine the required width and the entry label diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 7c5eed21219de0..f10b1a40198d21 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7558,6 +7558,155 @@ def test_error_and_warning_not_colorized_when_disabled(self): self.assertNotIn('\x1b[', warn) self.assertIn('warning:', warn) + def test_cmd_markup_in_epilog(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='Example: [cmd]python -m myapp --verbose[/cmd]', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'Example: {prog_extra}python -m myapp --verbose{reset}', + help_text) + self.assertNotIn('[cmd]', help_text) + self.assertNotIn('[/cmd]', help_text) + + def test_cmd_markup_in_description(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + description='Run [cmd]python -m myapp[/cmd] to start.', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'Run {prog_extra}python -m myapp{reset} to start.', + help_text) + + def test_cmd_markup_multiline(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='Example:\n[cmd]python -m myapp \\\n --verbose[/cmd]', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}python -m myapp \\\n --verbose{reset}', + help_text) + + def test_cmd_markup_multiple_tags(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='Try [cmd]app run[/cmd] or [cmd]app test[/cmd].', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}app run{reset}', help_text) + self.assertIn(f'{prog_extra}app test{reset}', help_text) + + def test_cmd_markup_not_applied_when_color_disabled(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=False, + epilog='Example: [cmd]python -m myapp[/cmd]', + ) + + help_text = parser.format_help() + self.assertNotIn('[cmd]', help_text) + self.assertNotIn('[/cmd]', help_text) + self.assertIn('python -m myapp', help_text) + self.assertNotIn('\x1b[', help_text) + + def test_cmd_markup_unclosed_tag_unchanged(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='Example: [cmd]python -m myapp without closing tag', + ) + + help_text = parser.format_help() + self.assertIn('[cmd]', help_text) + + def test_cmd_markup_empty_tag(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='Before [cmd][/cmd] after', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'Before {prog_extra}{reset} after', help_text) + + def test_cmd_markup_with_format_string(self): + parser = argparse.ArgumentParser( + prog='myapp', + color=True, + epilog='Run [cmd]%(prog)s --help[/cmd] for more info.', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}myapp --help{reset}', help_text) + + def test_cmd_markup_case_sensitive(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='[CMD]uppercase[/CMD] vs [cmd]lowercase[/cmd]', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn('[CMD]uppercase[/CMD]', help_text) + self.assertIn(f'{prog_extra}lowercase{reset}', help_text) + + def test_cmd_markup_in_subparser(self): + parser = argparse.ArgumentParser(prog='PROG', color=True) + subparsers = parser.add_subparsers() + sub = subparsers.add_parser( + 'sub', + description='Run [cmd]PROG sub --foo[/cmd] to start.', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = sub.format_help() + self.assertIn(f'{prog_extra}PROG sub --foo{reset}', help_text) + + def test_cmd_markup_special_regex_chars(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='[cmd]grep "foo.*bar" | sort[/cmd]', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}grep "foo.*bar" | sort{reset}', help_text) + class TestModule(unittest.TestCase): def test_deprecated__version__(self): From 888294a42232fad1683c532a0c236e356a94b488 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 22:13:31 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-12-07-22-13-28.gh-issue-142389.J9v904.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-12-07-22-13-28.gh-issue-142389.J9v904.rst diff --git a/Misc/NEWS.d/next/Library/2025-12-07-22-13-28.gh-issue-142389.J9v904.rst b/Misc/NEWS.d/next/Library/2025-12-07-22-13-28.gh-issue-142389.J9v904.rst new file mode 100644 index 00000000000000..49ef9f3bbb303c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-07-22-13-28.gh-issue-142389.J9v904.rst @@ -0,0 +1 @@ +Add ``[cmd]...[/cmd]`` markup support in :mod:`argparse` description and epilog text to highlight command examples when color output is enabled. From 398a1750234dfed8c20c89f847cc255731dd06b8 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Sun, 7 Dec 2025 14:14:39 -0800 Subject: [PATCH 3/5] Update whats new PR number --- Doc/whatsnew/3.15.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index ed3aa4ec767438..65fc11017bc129 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -409,7 +409,7 @@ argparse * Added ``[cmd]...[/cmd]`` markup support in description and epilog text to highlight command examples when color output is enabled. - (Contributed by Savannah Ostrowski in :gh:`XXXXX`.) + (Contributed by Savannah Ostrowski in :gh:`142390`.) calendar -------- From 9422e8927c347335187da0931f317a065b7cbbea Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Sun, 7 Dec 2025 14:19:40 -0800 Subject: [PATCH 4/5] Appease Sphinx --- Doc/library/argparse.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 8b0b9813f6510e..b79349c0f215c3 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -661,9 +661,8 @@ disabled, the tags are stripped and the content is displayed as plain text. .. note:: - The ``[cmd]`` markup only applies to description and epilog text processed - by :meth:`HelpFormatter._format_text`. It does not apply to individual - argument ``help`` strings. + The ``[cmd]`` markup only applies to description and epilog text. It does + not apply to individual argument ``help`` strings. .. versionadded:: 3.15 From cb2163a203d52789554349bebccf34afa792a269 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Sun, 7 Dec 2025 20:09:45 -0800 Subject: [PATCH 5/5] Do not transform text if color=False --- Doc/library/argparse.rst | 2 +- Lib/argparse.py | 4 ++++ Lib/test/test_argparse.py | 5 ++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index b79349c0f215c3..72eda5b47e0c6f 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -657,7 +657,7 @@ To highlight command examples in your description or epilog text, you can use When colors are enabled, the text inside ``[cmd]...[/cmd]`` tags will be displayed in a distinct color to help examples stand out. When colors are -disabled, the tags are stripped and the content is displayed as plain text. +disabled, no transformation is applied and the tags remain as-is. .. note:: diff --git a/Lib/argparse.py b/Lib/argparse.py index b309d2f862dfa4..8d55ddc639085d 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -526,8 +526,12 @@ def _apply_text_markup(self, text): Supported markup: [cmd]...[/cmd] - command/shell example (single color) + + When colors are disabled, no transformation is applied. """ t = self._theme + if not t.reset: + return text text = _re.sub( r'\[cmd\](.*?)\[/cmd\]', rf'{t.prog_extra}\1{t.reset}', diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index f10b1a40198d21..db13c78886d721 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7618,6 +7618,7 @@ def test_cmd_markup_multiple_tags(self): self.assertIn(f'{prog_extra}app test{reset}', help_text) def test_cmd_markup_not_applied_when_color_disabled(self): + # When color is disabled, markup is not transformed (tags remain as-is) parser = argparse.ArgumentParser( prog='PROG', color=False, @@ -7625,9 +7626,7 @@ def test_cmd_markup_not_applied_when_color_disabled(self): ) help_text = parser.format_help() - self.assertNotIn('[cmd]', help_text) - self.assertNotIn('[/cmd]', help_text) - self.assertIn('python -m myapp', help_text) + self.assertIn('[cmd]python -m myapp[/cmd]', help_text) self.assertNotIn('\x1b[', help_text) def test_cmd_markup_unclosed_tag_unchanged(self):