Skip to content

Commit 5ce8407

Browse files
authored
Integrated rich-argparse with cmd2. (#1356)
1 parent 722dc9f commit 5ce8407

File tree

14 files changed

+266
-155
lines changed

14 files changed

+266
-155
lines changed

.github/workflows/mypy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ jobs:
1919
# Only a single commit is fetched by default, for the ref/SHA that triggered the workflow.
2020
# Set fetch-depth: 0 to fetch all history for all branches and tags.
2121
fetch-depth: 0 # Needed for setuptools_scm to work correctly
22-
- run: pip install -U --user pip mypy
22+
- run: pip install -U --user pip mypy rich rich-argparse
2323
- run: mypy .

Pipfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ verify_ssl = true
55

66
[packages]
77
pyperclip = "*"
8+
rich = "*"
9+
rich-argparse = "*"
810
setuptools = "*"
911
wcwidth = "*"
1012

cmd2/argparse_custom.py

Lines changed: 117 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
parser that inherits from it. This will give a consistent look-and-feel between
88
the help/error output of built-in cmd2 commands and the app-specific commands.
99
If you wish to override the parser used by cmd2's built-in commands, see
10-
override_parser.py example.
10+
custom_parser.py example.
1111
1212
Since the new capabilities are added by patching at the argparse API level,
1313
they are available whether or not Cmd2ArgumentParser is used. However, the help
@@ -265,6 +265,18 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
265265
runtime_checkable,
266266
)
267267

268+
from rich.console import (
269+
Group,
270+
RenderableType,
271+
)
272+
from rich_argparse import (
273+
ArgumentDefaultsRichHelpFormatter,
274+
MetavarTypeRichHelpFormatter,
275+
RawDescriptionRichHelpFormatter,
276+
RawTextRichHelpFormatter,
277+
RichHelpFormatter,
278+
)
279+
268280
from . import (
269281
ansi,
270282
constants,
@@ -1042,9 +1054,14 @@ def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str)
10421054
############################################################################################################
10431055

10441056

1045-
class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
1057+
class Cmd2HelpFormatter(RichHelpFormatter):
10461058
"""Custom help formatter to configure ordering of help text"""
10471059

1060+
# Render markup in usage, help, description, and epilog text.
1061+
RichHelpFormatter.usage_markup = True
1062+
RichHelpFormatter.help_markup = True
1063+
RichHelpFormatter.text_markup = True
1064+
10481065
def _format_usage(
10491066
self,
10501067
usage: Optional[str],
@@ -1249,17 +1266,95 @@ def _format_args(self, action: argparse.Action, default_metavar: Union[str, Tupl
12491266
return super()._format_args(action, default_metavar) # type: ignore[arg-type]
12501267

12511268

1269+
class RawDescriptionCmd2HelpFormatter(
1270+
RawDescriptionRichHelpFormatter,
1271+
Cmd2HelpFormatter,
1272+
):
1273+
"""Cmd2 help message formatter which retains any formatting in descriptions and epilogs."""
1274+
1275+
1276+
class RawTextCmd2HelpFormatter(
1277+
RawTextRichHelpFormatter,
1278+
Cmd2HelpFormatter,
1279+
):
1280+
"""Cmd2 help message formatter which retains formatting of all help text."""
1281+
1282+
1283+
class ArgumentDefaultsCmd2HelpFormatter(
1284+
ArgumentDefaultsRichHelpFormatter,
1285+
Cmd2HelpFormatter,
1286+
):
1287+
"""Cmd2 help message formatter which adds default values to argument help."""
1288+
1289+
1290+
class MetavarTypeCmd2HelpFormatter(
1291+
MetavarTypeRichHelpFormatter,
1292+
Cmd2HelpFormatter,
1293+
):
1294+
"""
1295+
Cmd2 help message formatter which uses the argument 'type' as the default
1296+
metavar value (instead of the argument 'dest').
1297+
"""
1298+
1299+
1300+
class TextGroup:
1301+
"""
1302+
A block of text which is formatted like an argparse argument group, including a title.
1303+
1304+
Title:
1305+
Here is the first row of text.
1306+
Here is yet another row of text.
1307+
"""
1308+
1309+
def __init__(
1310+
self,
1311+
title: str,
1312+
text: RenderableType,
1313+
formatter_creator: Callable[[], Cmd2HelpFormatter],
1314+
) -> None:
1315+
"""
1316+
:param title: the group's title
1317+
:param text: the group's text (string or object that may be rendered by Rich)
1318+
:param formatter_creator: callable which returns a Cmd2HelpFormatter instance
1319+
"""
1320+
self.title = title
1321+
self.text = text
1322+
self.formatter_creator = formatter_creator
1323+
1324+
def __rich__(self) -> Group:
1325+
"""Custom rendering logic."""
1326+
import rich
1327+
1328+
formatter = self.formatter_creator()
1329+
1330+
styled_title = rich.text.Text(
1331+
type(formatter).group_name_formatter(f"{self.title}:"),
1332+
style=formatter.styles["argparse.groups"],
1333+
)
1334+
1335+
# Left pad the text like an argparse argument group does
1336+
left_padding = formatter._indent_increment
1337+
1338+
text_table = rich.table.Table(
1339+
box=None,
1340+
show_header=False,
1341+
padding=(0, 0, 0, left_padding),
1342+
)
1343+
text_table.add_row(self.text)
1344+
return Group(styled_title, text_table)
1345+
1346+
12521347
class Cmd2ArgumentParser(argparse.ArgumentParser):
12531348
"""Custom ArgumentParser class that improves error and help output"""
12541349

12551350
def __init__(
12561351
self,
12571352
prog: Optional[str] = None,
12581353
usage: Optional[str] = None,
1259-
description: Optional[str] = None,
1260-
epilog: Optional[str] = None,
1354+
description: Optional[RenderableType] = None,
1355+
epilog: Optional[RenderableType] = None,
12611356
parents: Sequence[argparse.ArgumentParser] = (),
1262-
formatter_class: Type[argparse.HelpFormatter] = Cmd2HelpFormatter,
1357+
formatter_class: Type[Cmd2HelpFormatter] = Cmd2HelpFormatter,
12631358
prefix_chars: str = '-',
12641359
fromfile_prefix_chars: Optional[str] = None,
12651360
argument_default: Optional[str] = None,
@@ -1279,10 +1374,10 @@ def __init__(
12791374
super(Cmd2ArgumentParser, self).__init__(
12801375
prog=prog,
12811376
usage=usage,
1282-
description=description,
1283-
epilog=epilog,
1377+
description=description, # type: ignore[arg-type]
1378+
epilog=epilog, # type: ignore[arg-type]
12841379
parents=parents if parents else [],
1285-
formatter_class=formatter_class, # type: ignore[arg-type]
1380+
formatter_class=formatter_class,
12861381
prefix_chars=prefix_chars,
12871382
fromfile_prefix_chars=fromfile_prefix_chars,
12881383
argument_default=argument_default,
@@ -1291,6 +1386,10 @@ def __init__(
12911386
allow_abbrev=allow_abbrev,
12921387
)
12931388

1389+
# Recast to assist type checkers since in a Cmd2HelpFormatter, these can be Rich renderables.
1390+
self.description: Optional[RenderableType] = self.description # type: ignore[assignment]
1391+
self.epilog: Optional[RenderableType] = self.epilog # type: ignore[assignment]
1392+
12941393
self.set_ap_completer_type(ap_completer_type) # type: ignore[attr-defined]
12951394

12961395
def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type: ignore
@@ -1321,6 +1420,10 @@ def error(self, message: str) -> NoReturn:
13211420
formatted_message = ansi.style_error(formatted_message)
13221421
self.exit(2, f'{formatted_message}\n\n')
13231422

1423+
def _get_formatter(self) -> Cmd2HelpFormatter:
1424+
"""Copy of _get_formatter() with a different return type to assist type checkers."""
1425+
return cast(Cmd2HelpFormatter, super()._get_formatter())
1426+
13241427
def format_help(self) -> str:
13251428
"""Copy of format_help() from argparse.ArgumentParser with tweaks to separately display required parameters"""
13261429
formatter = self._get_formatter()
@@ -1329,7 +1432,7 @@ def format_help(self) -> str:
13291432
formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups) # type: ignore[arg-type]
13301433

13311434
# description
1332-
formatter.add_text(self.description)
1435+
formatter.add_text(self.description) # type: ignore[arg-type]
13331436

13341437
# Begin cmd2 customization (separate required and optional arguments)
13351438

@@ -1370,7 +1473,7 @@ def format_help(self) -> str:
13701473
# End cmd2 customization
13711474

13721475
# epilog
1373-
formatter.add_text(self.epilog)
1476+
formatter.add_text(self.epilog) # type: ignore[arg-type]
13741477

13751478
# determine help from format above
13761479
return formatter.format_help() + '\n'
@@ -1382,6 +1485,10 @@ def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None:
13821485
file = sys.stderr
13831486
ansi.style_aware_write(file, message)
13841487

1488+
def create_text_group(self, title: str, text: RenderableType) -> TextGroup:
1489+
"""Create a TextGroup using this parser's formatter creator."""
1490+
return TextGroup(title, text, self._get_formatter)
1491+
13851492

13861493
class Cmd2AttributeWrapper:
13871494
"""

0 commit comments

Comments
 (0)