Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,27 @@ 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. It does
not apply to individual argument ``help`` strings.

.. versionadded:: 3.15


The add_argument() method
-------------------------
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:`142390`.)

calendar
--------

Expand Down
19 changes: 18 additions & 1 deletion Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
149 changes: 149 additions & 0 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ``[cmd]...[/cmd]`` markup support in :mod:`argparse` description and epilog text to highlight command examples when color output is enabled.
Loading