Skip to content

Commit 475b28c

Browse files
committed
Fixed issue where argument parsers for overridden commands were not being created.
1 parent 91897f4 commit 475b28c

File tree

8 files changed

+253
-92
lines changed

8 files changed

+253
-92
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.5.7 (TBD)
2+
* Bug Fixes
3+
* Fixed issue where argument parsers for overridden commands were not being created.
4+
15
## 2.5.6 (November 14, 2024)
26
* Bug Fixes
37
* Fixed type hint for `with_default_category` decorator which caused type checkers to mistype

Diff for: cmd2/cmd2.py

+153-84
Large diffs are not rendered by default.

Diff for: cmd2/decorators.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,9 @@ def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]:
346346
statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(
347347
command_name, statement_arg, preserve_quotes
348348
)
349-
arg_parser = cmd2_app._command_parsers.get(command_name, None)
349+
350+
# Pass cmd_wrapper instead of func, since it contains the parser info.
351+
arg_parser = cmd2_app._command_parsers.get(cmd_wrapper)
350352
if arg_parser is None:
351353
# This shouldn't be possible to reach
352354
raise ValueError(f'No argument parser found for {command_name}') # pragma: no cover

Diff for: pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ dev-dependencies = [
328328
"pytest",
329329
"pytest-cov",
330330
"pytest-mock",
331+
"ruff",
331332
"sphinx",
332333
"sphinx-autobuild",
333334
"sphinx-rtd-theme",

Diff for: tests/test_argparse.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import cmd2
1515

1616
from .conftest import (
17-
find_subcommand,
1817
run_cmd,
1918
)
2019

@@ -402,8 +401,7 @@ def test_add_another_subcommand(subcommand_app):
402401
This tests makes sure _set_parser_prog() sets _prog_prefix on every _SubParsersAction so that all future calls
403402
to add_parser() write the correct prog value to the parser being added.
404403
"""
405-
base_parser = subcommand_app._command_parsers.get('base')
406-
find_subcommand(subcommand_app._command_parsers.get('base'), [])
404+
base_parser = subcommand_app._command_parsers.get(subcommand_app.do_base)
407405
for sub_action in base_parser._actions:
408406
if isinstance(sub_action, argparse._SubParsersAction):
409407
new_parser = sub_action.add_parser('new_sub', help='stuff')

Diff for: tests/test_cmd2.py

+35
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,13 @@ def do_multiline_docstr(self, arg):
12101210
"""
12111211
pass
12121212

1213+
parser_cmd_parser = cmd2.Cmd2ArgumentParser(description="This is the description.")
1214+
1215+
@cmd2.with_argparser(parser_cmd_parser)
1216+
def do_parser_cmd(self, args):
1217+
"""This is the docstring."""
1218+
pass
1219+
12131220

12141221
@pytest.fixture
12151222
def help_app():
@@ -1249,6 +1256,11 @@ def test_help_multiline_docstring(help_app):
12491256
assert help_app.last_result is True
12501257

12511258

1259+
def test_help_verbose_uses_parser_description(help_app: HelpApp):
1260+
out, err = run_cmd(help_app, 'help --verbose')
1261+
verify_help_text(help_app, out, verbose_strings=[help_app.parser_cmd_parser.description])
1262+
1263+
12521264
class HelpCategoriesApp(cmd2.Cmd):
12531265
"""Class for testing custom help_* methods which override docstring help."""
12541266

@@ -3016,3 +3028,26 @@ def test_columnize_too_wide(outsim_app):
30163028

30173029
expected = "\n".join(str_list) + "\n"
30183030
assert outsim_app.stdout.getvalue() == expected
3031+
3032+
3033+
def test_command_parser_retrieval(outsim_app: cmd2.Cmd):
3034+
# Pass something that isn't a method
3035+
not_a_method = "just a string"
3036+
assert outsim_app._command_parsers.get(not_a_method) is None
3037+
3038+
# Pass a non-command method
3039+
assert outsim_app._command_parsers.get(outsim_app.__init__) is None
3040+
3041+
3042+
def test_command_synonym_parser():
3043+
# Make sure a command synonym returns the same parser as what it aliases
3044+
class SynonymApp(cmd2.cmd2.Cmd):
3045+
do_synonym = cmd2.cmd2.Cmd.do_help
3046+
3047+
app = SynonymApp()
3048+
3049+
synonym_parser = app._command_parsers.get(app.do_synonym)
3050+
help_parser = app._command_parsers.get(app.do_help)
3051+
3052+
assert synonym_parser is not None
3053+
assert synonym_parser is help_parser

Diff for: tests/transcripts/from_cmdloop.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# so you can see where they are.
33

44
(Cmd) help say
5-
Usage: say [-h] [-p] [-s] [-r REPEAT]/ */
5+
Usage: speak [-h] [-p] [-s] [-r REPEAT]/ */
66

77
Repeats what you tell me to./ */
88

Diff for: tests_isolated/test_commandset/test_commandset.py

+55-3
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,50 @@ def test_autoload_commands(command_sets_app):
149149
assert 'Command Set B' not in cmds_cats
150150

151151

152+
def test_command_synonyms():
153+
"""Test the use of command synonyms in CommandSets"""
154+
155+
class SynonymCommandSet(cmd2.CommandSet):
156+
def __init__(self, arg1):
157+
super().__init__()
158+
self._arg1 = arg1
159+
160+
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser(description="Native Command"))
161+
def do_builtin(self, _):
162+
pass
163+
164+
# Create a synonym to a command inside of this CommandSet
165+
do_builtin_synonym = do_builtin
166+
167+
# Create a synonym to a command outside of this CommandSet
168+
do_help_synonym = cmd2.Cmd.do_help
169+
170+
cs = SynonymCommandSet("foo")
171+
app = WithCommandSets(command_sets=[cs])
172+
173+
# Make sure the synonyms have the same parser as what they alias
174+
builtin_parser = app._command_parsers.get(app.do_builtin)
175+
builtin_synonym_parser = app._command_parsers.get(app.do_builtin_synonym)
176+
assert builtin_parser is not None
177+
assert builtin_parser is builtin_synonym_parser
178+
179+
help_parser = app._command_parsers.get(cmd2.Cmd.do_help)
180+
help_synonym_parser = app._command_parsers.get(app.do_help_synonym)
181+
assert help_parser is not None
182+
assert help_parser is help_synonym_parser
183+
184+
# Unregister the CommandSet and make sure built-in command and synonyms are gone
185+
app.unregister_command_set(cs)
186+
assert not hasattr(app, "do_builtin")
187+
assert not hasattr(app, "do_builtin_synonym")
188+
assert not hasattr(app, "do_help_synonym")
189+
190+
# Make sure the help command still exists, has the same parser, and works.
191+
assert help_parser is app._command_parsers.get(cmd2.Cmd.do_help)
192+
out, err = run_cmd(app, 'help')
193+
assert app.doc_header in out
194+
195+
152196
def test_custom_construct_commandsets():
153197
command_set_b = CommandSetB('foo')
154198

@@ -291,7 +335,7 @@ def test_load_commandset_errors(command_sets_manual, capsys):
291335
cmd_set = CommandSetA()
292336

293337
# create a conflicting command before installing CommandSet to verify rollback behavior
294-
command_sets_manual._install_command_function('durian', cmd_set.do_durian)
338+
command_sets_manual._install_command_function('do_durian', cmd_set.do_durian)
295339
with pytest.raises(CommandSetRegistrationError):
296340
command_sets_manual.register_command_set(cmd_set)
297341

@@ -316,13 +360,21 @@ def test_load_commandset_errors(command_sets_manual, capsys):
316360
assert "Deleting alias 'banana'" in err
317361
assert "Deleting macro 'apple'" in err
318362

363+
# verify command functions which don't start with "do_" raise an exception
364+
with pytest.raises(CommandSetRegistrationError):
365+
command_sets_manual._install_command_function('new_cmd', cmd_set.do_banana)
366+
367+
# verify methods which don't start with "do_" raise an exception
368+
with pytest.raises(CommandSetRegistrationError):
369+
command_sets_manual._install_command_function('do_new_cmd', cmd_set.on_register)
370+
319371
# verify duplicate commands are detected
320372
with pytest.raises(CommandSetRegistrationError):
321-
command_sets_manual._install_command_function('banana', cmd_set.do_banana)
373+
command_sets_manual._install_command_function('do_banana', cmd_set.do_banana)
322374

323375
# verify bad command names are detected
324376
with pytest.raises(CommandSetRegistrationError):
325-
command_sets_manual._install_command_function('bad command', cmd_set.do_banana)
377+
command_sets_manual._install_command_function('do_bad command', cmd_set.do_banana)
326378

327379
# verify error conflict with existing completer function
328380
with pytest.raises(CommandSetRegistrationError):

0 commit comments

Comments
 (0)