Skip to content

Commit 95be12b

Browse files
committed
Checking for Cmd2ArgumentParser instance in _build_parser().
1 parent 5ec21a5 commit 95be12b

2 files changed

Lines changed: 66 additions & 11 deletions

File tree

cmd2/cmd2.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -896,21 +896,30 @@ def _build_parser(
896896
:param parent: object which owns the command using the parser.
897897
When parser_builder is a classmethod, this function passes
898898
parent's class to it.
899-
:param parser_builder: means used to build the parser
899+
:param parser_builder: an existing Cmd2ArgumentParser instance or a factory
900+
(callable, staticmethod, or classmethod) that returns one.
900901
:param prog: prog value to set in new parser
901902
:return: new parser
902-
:raises TypeError: if parser_builder is invalid type
903+
:raises TypeError: if parser_builder is an invalid type or if the factory fails
904+
to return a Cmd2ArgumentParser
903905
"""
904-
if isinstance(parser_builder, staticmethod):
905-
parser = parser_builder.__func__()
906-
elif isinstance(parser_builder, classmethod):
907-
parser = parser_builder.__func__(parent.__class__)
908-
elif callable(parser_builder):
909-
parser = parser_builder()
910-
elif isinstance(parser_builder, Cmd2ArgumentParser):
906+
if isinstance(parser_builder, Cmd2ArgumentParser):
911907
parser = copy.deepcopy(parser_builder)
912908
else:
913-
raise TypeError(f"Invalid type for parser_builder: {type(parser_builder)}")
909+
# Try to build the parser with a factory
910+
if isinstance(parser_builder, staticmethod):
911+
parser = parser_builder.__func__()
912+
elif isinstance(parser_builder, classmethod):
913+
parser = parser_builder.__func__(parent.__class__)
914+
elif callable(parser_builder):
915+
parser = parser_builder()
916+
else:
917+
raise TypeError(f"Invalid type for parser_builder: {type(parser_builder)}")
918+
919+
# Verify the factory returned the required type
920+
if not isinstance(parser, Cmd2ArgumentParser):
921+
builder_name = getattr(parser_builder, "__name__", str(parser_builder)) # type: ignore[unreachable]
922+
raise TypeError(f"The parser returned by '{builder_name}' must be a Cmd2ArgumentParser or a subclass of it")
914923

915924
argparse_custom.set_parser_prog(parser, prog)
916925

tests/test_argparse.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,56 @@ def test_preservelist(argparse_app) -> None:
247247

248248
def test_invalid_parser_builder(argparse_app):
249249
parser_builder = None
250-
with pytest.raises(TypeError):
250+
with pytest.raises(TypeError, match="Invalid type for parser_builder"):
251251
argparse_app._build_parser(argparse_app, parser_builder, "fake_prog")
252252

253253

254+
def test_invalid_parser_return_type(argparse_app):
255+
def bad_builder():
256+
return argparse.ArgumentParser()
257+
258+
with pytest.raises(TypeError, match="must be a Cmd2ArgumentParser or a subclass of it"):
259+
argparse_app._build_parser(argparse_app, bad_builder, "fake_prog")
260+
261+
262+
def test_invalid_parser_return_type_staticmethod(argparse_app):
263+
def bad_builder():
264+
return argparse.ArgumentParser()
265+
266+
sm = staticmethod(bad_builder)
267+
268+
with pytest.raises(TypeError, match="must be a Cmd2ArgumentParser or a subclass of it"):
269+
argparse_app._build_parser(argparse_app, sm, "fake_prog")
270+
271+
272+
def test_invalid_parser_return_type_classmethod(argparse_app):
273+
def bad_builder(cls):
274+
return argparse.ArgumentParser()
275+
276+
cm = classmethod(bad_builder)
277+
278+
with pytest.raises(TypeError, match="must be a Cmd2ArgumentParser or a subclass of it"):
279+
argparse_app._build_parser(argparse_app, cm, "fake_prog")
280+
281+
282+
def test_invalid_parser_return_type_nameless_object(argparse_app):
283+
# A class that is callable but has no __name__ attribute
284+
class NamelessBuilder:
285+
def __call__(self):
286+
return argparse.ArgumentParser()
287+
288+
builder = NamelessBuilder()
289+
290+
# Verify __name__ is actually missing
291+
assert not hasattr(builder, '__name__')
292+
293+
# The error message should now contain the string representation of the object
294+
expected_msg = f"The parser returned by '{builder}' must be a Cmd2ArgumentParser"
295+
296+
with pytest.raises(TypeError, match=expected_msg):
297+
argparse_app._build_parser(argparse_app, builder, "fake_prog")
298+
299+
254300
def _build_has_subcmd_parser() -> cmd2.Cmd2ArgumentParser:
255301
has_subcmds_parser = cmd2.Cmd2ArgumentParser(description="Tests as_subcmd_to decorator")
256302
has_subcmds_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND', required=True)

0 commit comments

Comments
 (0)