Skip to content

Commit 156d158

Browse files
committed
Allow attached parser to be a subclass of _SubparsersAction's parser class.
1 parent 42371e6 commit 156d158

3 files changed

Lines changed: 22 additions & 20 deletions

File tree

cmd2/argparse_custom.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -896,9 +896,9 @@ def attach_subcommand(
896896
:param subcommand: name of the new subcommand
897897
:param subcommand_parser: the parser to attach
898898
:param add_parser_kwargs: additional arguments for the subparser registration (e.g. help, aliases)
899-
:raises TypeError: if the subcommand parser is not an instance of 'Cmd2ArgumentParser'
900-
(or one of its subclasses), or if its type does not match the 'parser_class'
901-
configured for the target subcommand group.
899+
:raises TypeError: if subcommand_parser is not an instance of the following or their subclasses:
900+
1. Cmd2ArgumentParser
901+
2. The parser_class configured for the target subcommand group
902902
:raises ValueError: if the command path is invalid or doesn't support subcommands
903903
"""
904904
if not isinstance(subcommand_parser, Cmd2ArgumentParser):
@@ -910,12 +910,15 @@ def attach_subcommand(
910910
target_parser = self._find_parser(subcommand_path)
911911
subparsers_action = target_parser._get_subparsers_action()
912912

913-
# Mirror argparse's add_parser() behavior by requiring an exact type match with _parser_class
914-
if type(subcommand_parser) is not subparsers_action._parser_class:
913+
# Verify the parser is compatible with the 'parser_class' configured for this
914+
# subcommand group. We use isinstance() here to allow for subclasses, providing
915+
# more flexibility than the standard add_parser() factory approach which enforces
916+
# a specific class.
917+
if not isinstance(subcommand_parser, subparsers_action._parser_class):
915918
raise TypeError(
916-
f"The attached parser must be of type '{subparsers_action._parser_class.__name__}' "
917-
f"to match the 'parser_class' configured for this subparsers action. "
918-
f"Received '{type(subcommand_parser).__name__}'."
919+
f"The attached parser must be an instance of '{subparsers_action._parser_class.__name__}' "
920+
f"(or a subclass) to match the 'parser_class' configured for this subcommand group. "
921+
f"Received: '{type(subcommand_parser).__name__}'."
919922
)
920923

921924
# Use add_parser to register the subcommand name and any aliases

cmd2/cmd2.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,9 +1189,9 @@ def attach_subcommand(
11891189
:param subcommand: name of the new subcommand
11901190
:param subcommand_parser: the parser to attach
11911191
:param add_parser_kwargs: additional arguments for the subparser registration (e.g. help, aliases)
1192-
:raises TypeError: if the subcommand parser is not an instance of 'Cmd2ArgumentParser'
1193-
(or one of its subclasses), or if its type does not match the 'parser_class'
1194-
configured for the target subcommand group.
1192+
:raises TypeError: if subcommand_parser is not an instance of the following or their subclasses:
1193+
1. Cmd2ArgumentParser
1194+
2. The parser_class configured for the target subcommand group
11951195
:raises ValueError: if the command path is invalid or doesn't support subcommands
11961196
"""
11971197
root_parser, subcommand_path = self._get_root_parser_and_subcmd_path(command)

tests/test_argparse_custom.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -430,19 +430,14 @@ def test_subcommand_attachment_errors() -> None:
430430
with pytest.raises(TypeError, match=r"must be an instance of 'Cmd2ArgumentParser' \(or a subclass\)"):
431431
root_parser.attach_subcommand([], "sub", ap_parser) # type: ignore[arg-type]
432432

433-
# Verify TypeError when attaching a parser of a different type
434-
class SubParser(Cmd2ArgumentParser):
435-
pass
436-
437-
subclass_parser = SubParser(prog="subclass")
438-
with pytest.raises(TypeError, match="to match the 'parser_class' configured for this subparsers action"):
439-
root_parser.attach_subcommand([], "sub", subclass_parser)
440-
441433

442434
def test_subcommand_attachment_parser_class_override() -> None:
443435
class MyParser(Cmd2ArgumentParser):
444436
pass
445437

438+
class MySubParser(MyParser):
439+
pass
440+
446441
root_parser = Cmd2ArgumentParser(prog="root")
447442

448443
# Explicitly override parser_class for this subparsers action
@@ -452,9 +447,13 @@ class MyParser(Cmd2ArgumentParser):
452447
my_parser = MyParser(prog="sub")
453448
root_parser.attach_subcommand([], "sub", my_parser)
454449

450+
# Attaching a MySubParser instance should also succeed (isinstance check)
451+
my_sub_parser = MySubParser(prog="sub2")
452+
root_parser.attach_subcommand([], "sub2", my_sub_parser)
453+
455454
# Attaching a standard Cmd2ArgumentParser instance should fail
456455
standard_parser = Cmd2ArgumentParser(prog="standard")
457-
with pytest.raises(TypeError, match="The attached parser must be of type 'MyParser'"):
456+
with pytest.raises(TypeError, match=r"must be an instance of 'MyParser' \(or a subclass\)"):
458457
root_parser.attach_subcommand([], "fail", standard_parser)
459458

460459

0 commit comments

Comments
 (0)