Skip to content

Commit c50a092

Browse files
committed
Removed custom formatting for argparse.ZERO_OR_MORE and argparse.ONE_OR_MORE.
Added rich-argparse support for coloring cmd2's custom nargs formatting.
1 parent 970d802 commit c50a092

File tree

2 files changed

+105
-31
lines changed

2 files changed

+105
-31
lines changed

cmd2/argparse_custom.py

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -258,14 +258,11 @@ def get_items(self) -> list[CompletionItems]:
258258
import argparse
259259
import re
260260
import sys
261-
from argparse import (
262-
ONE_OR_MORE,
263-
ZERO_OR_MORE,
264-
ArgumentError,
265-
)
261+
from argparse import ArgumentError
266262
from collections.abc import (
267263
Callable,
268264
Iterable,
265+
Iterator,
269266
Sequence,
270267
)
271268
from gettext import gettext
@@ -1296,29 +1293,68 @@ def format_tuple(tuple_size: int) -> tuple[str, ...]:
12961293

12971294
return format_tuple
12981295

1296+
def _build_nargs_range_str(self, nargs_range: tuple[int, int | float]) -> str:
1297+
"""Generate nargs range string for help text."""
1298+
if nargs_range[1] == constants.INFINITY:
1299+
# {min+}
1300+
range_str = f"{{{nargs_range[0]}+}}"
1301+
else:
1302+
# {min..max}
1303+
range_str = f"{{{nargs_range[0]}..{nargs_range[1]}}}"
1304+
1305+
return range_str
1306+
12991307
def _format_args(self, action: argparse.Action, default_metavar: str) -> str:
1300-
"""Handle ranged nargs and make other output less verbose."""
1308+
"""Override to handle cmd2's custom nargs formatting.
1309+
1310+
All formats in this function need to be handled by _rich_metavar_parts().
1311+
"""
13011312
metavar = self._determine_metavar(action, default_metavar)
13021313
metavar_formatter = self._metavar_formatter(action, default_metavar)
13031314

13041315
# Handle nargs specified as a range
13051316
nargs_range = action.get_nargs_range() # type: ignore[attr-defined]
13061317
if nargs_range is not None:
1307-
range_str = f'{nargs_range[0]}+' if nargs_range[1] == constants.INFINITY else f'{nargs_range[0]}..{nargs_range[1]}'
1308-
1309-
return '{}{{{}}}'.format('%s' % metavar_formatter(1), range_str) # noqa: UP031
1310-
1311-
# Make this output less verbose. Do not customize the output when metavar is a
1312-
# tuple of strings. Allow argparse's formatter to handle that instead.
1313-
if isinstance(metavar, str):
1314-
if action.nargs == ZERO_OR_MORE:
1315-
return '[%s [...]]' % metavar_formatter(1) # noqa: UP031
1316-
if action.nargs == ONE_OR_MORE:
1317-
return '%s [...]' % metavar_formatter(1) # noqa: UP031
1318-
if isinstance(action.nargs, int) and action.nargs > 1:
1319-
return '{}{{{}}}'.format('%s' % metavar_formatter(1), action.nargs) # noqa: UP031
1318+
arg_str = '%s' % metavar_formatter(1) # noqa: UP031
1319+
range_str = self._build_nargs_range_str(nargs_range)
1320+
return f"{arg_str}{range_str}"
1321+
1322+
# When nargs is just a number, argparse repeats the arg in the help text.
1323+
# For instance, when nargs=5 the help text looks like: 'command arg arg arg arg arg'.
1324+
# To make this less verbose, format it like: 'command arg{5}'.
1325+
# Do not customize the output when metavar is a tuple of strings. Allow argparse's
1326+
# formatter to handle that instead.
1327+
if isinstance(metavar, str) and isinstance(action.nargs, int) and action.nargs > 1:
1328+
arg_str = '%s' % metavar_formatter(1) # noqa: UP031
1329+
return f"{arg_str}{{{action.nargs}}}"
1330+
1331+
# Fallback to parent for all other cases
1332+
return super()._format_args(action, default_metavar)
1333+
1334+
def _rich_metavar_parts(
1335+
self,
1336+
action: argparse.Action,
1337+
default_metavar: str,
1338+
) -> Iterator[tuple[str, bool]]:
1339+
"""Override to handle all cmd2-specific formatting in _format_args()."""
1340+
metavar = self._determine_metavar(action, default_metavar)
1341+
metavar_formatter = self._metavar_formatter(action, default_metavar)
13201342

1321-
return super()._format_args(action, default_metavar) # type: ignore[arg-type]
1343+
# Handle nargs specified as a range
1344+
nargs_range = action.get_nargs_range() # type: ignore[attr-defined]
1345+
if nargs_range is not None:
1346+
yield "%s" % metavar_formatter(1), True # noqa: UP031
1347+
yield self._build_nargs_range_str(nargs_range), False
1348+
return
1349+
1350+
# Handle specific integer nargs (e.g., nargs=5 -> arg{5})
1351+
if isinstance(metavar, str) and isinstance(action.nargs, int) and action.nargs > 1:
1352+
yield "%s" % metavar_formatter(1), True # noqa: UP031
1353+
yield f"{{{action.nargs}}}", False
1354+
return
1355+
1356+
# Fallback to parent for all other cases
1357+
yield from super()._rich_metavar_parts(action, default_metavar)
13221358

13231359

13241360
class RawDescriptionCmd2HelpFormatter(

tests/test_argparse_custom.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,9 @@
99
Cmd2ArgumentParser,
1010
constants,
1111
)
12-
from cmd2.argparse_custom import (
13-
generate_range_error,
14-
)
12+
from cmd2.argparse_custom import generate_range_error
1513

16-
from .conftest import (
17-
run_cmd,
18-
)
14+
from .conftest import run_cmd
1915

2016

2117
class ApCustomTestApp(cmd2.Cmd):
@@ -29,8 +25,6 @@ def __init__(self, *args, **kwargs) -> None:
2925
range_parser.add_argument('--arg1', nargs=2)
3026
range_parser.add_argument('--arg2', nargs=(3,))
3127
range_parser.add_argument('--arg3', nargs=(2, 3))
32-
range_parser.add_argument('--arg4', nargs=argparse.ZERO_OR_MORE)
33-
range_parser.add_argument('--arg5', nargs=argparse.ONE_OR_MORE)
3428

3529
@cmd2.with_argparser(range_parser)
3630
def do_range(self, _) -> None:
@@ -89,7 +83,7 @@ def test_apcustom_usage() -> None:
8983
def test_apcustom_nargs_help_format(cust_app) -> None:
9084
out, _err = run_cmd(cust_app, 'help range')
9185
assert 'Usage: range [-h] [--arg0 ARG0] [--arg1 ARG1{2}] [--arg2 ARG2{3+}]' in out[0]
92-
assert ' [--arg3 ARG3{2..3}] [--arg4 [ARG4 [...]]] [--arg5 ARG5 [...]]' in out[1]
86+
assert ' [--arg3 ARG3{2..3}]' in out[1]
9387

9488

9589
def test_apcustom_nargs_range_validation(cust_app) -> None:
@@ -114,6 +108,50 @@ def test_apcustom_nargs_range_validation(cust_app) -> None:
114108
assert not err
115109

116110

111+
@pytest.mark.parametrize(
112+
('nargs', 'expected_parts'),
113+
[
114+
# arg{2}
115+
(
116+
2,
117+
[("arg", True), ("{2}", False)],
118+
),
119+
# arg{2+}
120+
(
121+
(2,),
122+
[("arg", True), ("{2+}", False)],
123+
),
124+
# arg{0..5}
125+
(
126+
(0, 5),
127+
[("arg", True), ("{0..5}", False)],
128+
),
129+
],
130+
)
131+
def test_rich_metavar_parts(
132+
nargs: int | tuple[int, int | float],
133+
expected_parts: list[tuple[str, bool]],
134+
) -> None:
135+
"""
136+
Test cmd2's override of _rich_metavar_parts which handles custom nargs formats.
137+
138+
:param nargs: the arguments nargs value
139+
:param expected_parts: list to compare to _rich_metavar_parts's return value
140+
141+
Each element in this list is a 2-item tuple.
142+
item 1: one part of the argument string outputted by _format_args
143+
item 2: boolean stating whether rich-argparse should color this part
144+
"""
145+
parser = Cmd2ArgumentParser()
146+
help_formatter = parser._get_formatter()
147+
148+
action = parser.add_argument("arg", nargs=nargs) # type: ignore[arg-type]
149+
default_metavar = help_formatter._get_default_metavar_for_positional(action)
150+
151+
parts = help_formatter._rich_metavar_parts(action, default_metavar)
152+
assert list(parts) == expected_parts
153+
154+
117155
@pytest.mark.parametrize(
118156
'nargs_tuple',
119157
[
@@ -149,7 +187,7 @@ def test_apcustom_narg_tuple_zero_base() -> None:
149187
arg = parser.add_argument('arg', nargs=(0,))
150188
assert arg.nargs == argparse.ZERO_OR_MORE
151189
assert arg.nargs_range is None
152-
assert "[arg [...]]" in parser.format_help()
190+
assert "[arg ...]" in parser.format_help()
153191

154192
parser = Cmd2ArgumentParser()
155193
arg = parser.add_argument('arg', nargs=(0, 1))
@@ -169,7 +207,7 @@ def test_apcustom_narg_tuple_one_base() -> None:
169207
arg = parser.add_argument('arg', nargs=(1,))
170208
assert arg.nargs == argparse.ONE_OR_MORE
171209
assert arg.nargs_range is None
172-
assert "arg [...]" in parser.format_help()
210+
assert "arg [arg ...]" in parser.format_help()
173211

174212
parser = Cmd2ArgumentParser()
175213
arg = parser.add_argument('arg', nargs=(1, 5))

0 commit comments

Comments
 (0)