Skip to content

Commit 1dc7042

Browse files
committed
Fixed inconsistent behavior between Python versions by always checking for duplicate subcommands.
1 parent fd655f4 commit 1dc7042

4 files changed

Lines changed: 22 additions & 20 deletions

File tree

cmd2/argparse_utils.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ def _SubParsersAction_remove_parser( # noqa: N802
516516
:raises ValueError: if the subcommand doesn't exist
517517
"""
518518
if name not in self._name_parser_map:
519-
raise ValueError(f"Subcommand '{name}' not found")
519+
raise ValueError(f"Subcommand '{name}' does not exist")
520520

521521
subparser = self._name_parser_map[name]
522522

@@ -684,12 +684,12 @@ def update_prog(self, prog: str) -> None:
684684
# add_parser() will have the correct prog value.
685685
subparsers_action._prog_prefix = self._build_subparsers_prog_prefix(positionals)
686686

687-
# subparsers_action.choices includes aliases. Since primary names are inserted first,
688-
# we skip already updated parsers to ensure primary names are used in 'prog'.
687+
# subparsers_action._name_parser_map includes aliases. Since primary names are inserted
688+
# first, we skip already updated parsers to ensure primary names are used in 'prog'.
689689
updated_parsers: set[Cmd2ArgumentParser] = set()
690690

691691
# Set the prog value for each subcommand's parser
692-
for subcmd_name, subcmd_parser in subparsers_action.choices.items():
692+
for subcmd_name, subcmd_parser in subparsers_action._name_parser_map.items():
693693
if subcmd_parser in updated_parsers:
694694
continue
695695

@@ -707,9 +707,9 @@ def find_parser(self, subcommand_path: Iterable[str]) -> "Cmd2ArgumentParser":
707707
parser = self
708708
for name in subcommand_path:
709709
subparsers_action = parser.get_subparsers_action()
710-
if name not in subparsers_action.choices:
711-
raise ValueError(f"Subcommand '{name}' not found in '{parser.prog}'")
712-
parser = subparsers_action.choices[name]
710+
if name not in subparsers_action._name_parser_map:
711+
raise ValueError(f"Subcommand '{name}' does not exist for '{parser.prog}'")
712+
parser = subparsers_action._name_parser_map[name]
713713
return parser
714714

715715
def attach_subcommand(
@@ -752,12 +752,14 @@ def attach_subcommand(
752752
f"Received: '{type(subcommand_parser).__name__}'."
753753
)
754754

755+
# Do not overwrite existing subcommands or aliases
756+
all_names = (subcommand, *add_parser_kwargs.get("aliases", ()))
757+
for name in all_names:
758+
if name in subparsers_action._name_parser_map:
759+
raise ValueError(f"Subcommand '{name}' already exists for '{target_parser.prog}'")
760+
755761
# Use add_parser to register the subcommand name and any aliases
756-
try:
757-
placeholder_parser = subparsers_action.add_parser(subcommand, **add_parser_kwargs)
758-
except ArgumentError as ex:
759-
# The subcommand already exists
760-
raise ValueError(str(ex)) from None
762+
placeholder_parser = subparsers_action.add_parser(subcommand, **add_parser_kwargs)
761763

762764
# To ensure accurate usage strings, recursively update 'prog' values
763765
# within the injected parser to match its new location in the command hierarchy.
@@ -788,7 +790,7 @@ def detach_subcommand(self, subcommand_path: Iterable[str], subcommand: str) ->
788790
subparsers_action.remove_parser(subcommand), # type: ignore[attr-defined]
789791
)
790792
except ValueError:
791-
raise ValueError(f"Subcommand '{subcommand}' not found in '{target_parser.prog}'") from None
793+
raise ValueError(f"Subcommand '{subcommand}' does not exist for '{target_parser.prog}'") from None
792794

793795
def error(self, message: str) -> NoReturn:
794796
"""Override that applies custom formatting to the error message."""

cmd2/cmd2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1174,7 +1174,7 @@ def get_root_parser_and_subcmd_path(self, command: str) -> tuple[Cmd2ArgumentPar
11741174
# Search for the base command function and verify it has an argparser defined
11751175
command_func = self.get_command_func(root_command)
11761176
if command_func is None:
1177-
raise ValueError(f"Root command '{root_command}' not found")
1177+
raise ValueError(f"Root command '{root_command}' does not exist")
11781178

11791179
root_parser = self.command_parsers.get(command_func)
11801180
if root_parser is None:

tests/test_argparse_utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -453,13 +453,13 @@ def test_subcommand_attachment_errors() -> None:
453453
root_parser.add_subparsers()
454454

455455
# Verify ValueError when path is invalid (find_parser() fails)
456-
with pytest.raises(ValueError, match="Subcommand 'nonexistent' not found"):
456+
with pytest.raises(ValueError, match="Subcommand 'nonexistent' does not exist for 'root'"):
457457
root_parser.attach_subcommand(["nonexistent"], "anything", child_parser)
458-
with pytest.raises(ValueError, match="Subcommand 'nonexistent' not found"):
458+
with pytest.raises(ValueError, match="Subcommand 'nonexistent' does not exist for 'root'"):
459459
root_parser.detach_subcommand(["nonexistent"], "anything")
460460

461461
# Verify ValueError when path is valid but subcommand name is wrong
462-
with pytest.raises(ValueError, match="Subcommand 'fake' not found in 'root'"):
462+
with pytest.raises(ValueError, match="Subcommand 'fake' does not exist for 'root'"):
463463
root_parser.detach_subcommand([], "fake")
464464

465465
# Verify TypeError when attaching a non-Cmd2ArgumentParser type
@@ -470,7 +470,7 @@ def test_subcommand_attachment_errors() -> None:
470470
# Verify ValueError when subcommand name already exists
471471
sub_parser = Cmd2ArgumentParser(prog="sub")
472472
root_parser.attach_subcommand([], "sub", sub_parser)
473-
with pytest.raises(ValueError, match="conflicting subparser: sub"):
473+
with pytest.raises(ValueError, match="Subcommand 'sub' already exists for 'root'"):
474474
root_parser.attach_subcommand([], "sub", sub_parser)
475475

476476

tests/test_cmd2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4613,7 +4613,7 @@ def do_no_argparse(self, _statement: cmd2.Statement) -> None:
46134613
app.attach_subcommand("", "sub", cmd2.Cmd2ArgumentParser())
46144614

46154615
# Test non-existent command
4616-
with pytest.raises(ValueError, match="Root command 'fake' not found"):
4616+
with pytest.raises(ValueError, match="Root command 'fake' does not exist"):
46174617
app.attach_subcommand("fake", "sub", cmd2.Cmd2ArgumentParser())
46184618

46194619
# Test command that doesn't use argparse
@@ -4622,5 +4622,5 @@ def do_no_argparse(self, _statement: cmd2.Statement) -> None:
46224622

46234623
# Test duplicate subcommand
46244624
app.attach_subcommand("test", "sub", cmd2.Cmd2ArgumentParser())
4625-
with pytest.raises(ValueError, match="conflicting subparser: sub"):
4625+
with pytest.raises(ValueError, match="Subcommand 'sub' already exists for 'test'"):
46264626
app.attach_subcommand("test", "sub", cmd2.Cmd2ArgumentParser())

0 commit comments

Comments
 (0)