Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
showed no record of the run_script command in history.
* Made it easier for developers to override `edit` command by having `do_history` no longer call `do_edit`. This
also removes the need to exclude `edit` command from history list.
* It is no longer necessary to set the `prog` attribute of an argparser with subcommands. cmd2 now automatically
sets the prog value of it and all its subparsers so that all usage statements contain the top level command name
and not sys.argv[0].

## 0.9.19 (October 14, 2019)
* Bug Fixes
Expand Down
66 changes: 44 additions & 22 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,38 @@ def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
return arg_decorator


def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
# noinspection PyProtectedMember
def set_parser_prog(parser: argparse.ArgumentParser, prog: str):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this utility function be more appropriate in argparse_custom.py or here?

Actually, it begs the question would all of the decorators and associated utility functions belong better in a separate file?

"""
Recursively set prog attribute of a parser and all of its subparsers so that the root command
is a command name and not sys.argv[0].
:param parser: the parser being edited
:param prog: value for the current parsers prog attribute
"""
# Set the prog value for this parser
parser.prog = prog

# Set the prog value for the parser's subcommands
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):

# Set the prog value for each subcommand
for sub_cmd, sub_cmd_parser in action.choices.items():
sub_cmd_prog = parser.prog + ' ' + sub_cmd
set_parser_prog(sub_cmd_parser, sub_cmd_prog)

# We can break since argparse only allows 1 group of subcommands per level
break


def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *,
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
preserve_quotes: bool = False) -> \
Callable[[argparse.Namespace, List], Optional[bool]]:
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given
instance of argparse.ArgumentParser, but also returning unknown args as a list.

:param argparser: unique instance of ArgumentParser
:param parser: unique instance of ArgumentParser
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
state data that affects parsing.
Expand All @@ -209,27 +233,26 @@ def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
namespace = ns_provider(cmd2_app)

try:
args, unknown = argparser.parse_known_args(parsed_arglist, namespace)
args, unknown = parser.parse_known_args(parsed_arglist, namespace)
except SystemExit:
return
else:
setattr(args, '__statement__', statement)
return func(cmd2_app, args, unknown)

# argparser defaults the program name to sys.argv[0]
# we want it to be the name of our command
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
argparser.prog = command_name
set_parser_prog(parser, command_name)

# If the description has not been set, then use the method docstring if one exists
if argparser.description is None and func.__doc__:
argparser.description = func.__doc__
if parser.description is None and func.__doc__:
parser.description = func.__doc__

# Set the command's help text as argparser.description (which can be None)
cmd_wrapper.__doc__ = argparser.description
cmd_wrapper.__doc__ = parser.description

# Set some custom attributes for this command
setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, argparser)
setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, parser)
setattr(cmd_wrapper, CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)

return cmd_wrapper
Expand All @@ -238,13 +261,13 @@ def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
return arg_decorator


def with_argparser(argparser: argparse.ArgumentParser, *,
def with_argparser(parser: argparse.ArgumentParser, *,
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
preserve_quotes: bool = False) -> Callable[[argparse.Namespace], Optional[bool]]:
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
with the given instance of argparse.ArgumentParser.

:param argparser: unique instance of ArgumentParser
:param parser: unique instance of ArgumentParser
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
state data that affects parsing.
Expand All @@ -268,27 +291,26 @@ def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
namespace = ns_provider(cmd2_app)

try:
args = argparser.parse_args(parsed_arglist, namespace)
args = parser.parse_args(parsed_arglist, namespace)
except SystemExit:
return
else:
setattr(args, '__statement__', statement)
return func(cmd2_app, args)

# argparser defaults the program name to sys.argv[0]
# we want it to be the name of our command
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
argparser.prog = command_name
set_parser_prog(parser, command_name)

# If the description has not been set, then use the method docstring if one exists
if argparser.description is None and func.__doc__:
argparser.description = func.__doc__
if parser.description is None and func.__doc__:
parser.description = func.__doc__

# Set the command's help text as argparser.description (which can be None)
cmd_wrapper.__doc__ = argparser.description
cmd_wrapper.__doc__ = parser.description

# Set some custom attributes for this command
setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, argparser)
setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, parser)
setattr(cmd_wrapper, CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)

return cmd_wrapper
Expand Down Expand Up @@ -2396,7 +2418,7 @@ def _alias_list(self, args: argparse.Namespace) -> None:
"An alias is a command that enables replacement of a word by another string.")
alias_epilog = ("See also:\n"
" macro")
alias_parser = Cmd2ArgumentParser(description=alias_description, epilog=alias_epilog, prog='alias')
alias_parser = Cmd2ArgumentParser(description=alias_description, epilog=alias_epilog)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very nice little improvement that provides a better user experience. Thanks for the PR!


# Add subcommands to alias
alias_subparsers = alias_parser.add_subparsers(dest='subcommand')
Expand Down Expand Up @@ -2573,7 +2595,7 @@ def _macro_list(self, args: argparse.Namespace) -> None:
"A macro is similar to an alias, but it can contain argument placeholders.")
macro_epilog = ("See also:\n"
" alias")
macro_parser = Cmd2ArgumentParser(description=macro_description, epilog=macro_epilog, prog='macro')
macro_parser = Cmd2ArgumentParser(description=macro_description, epilog=macro_epilog)

# Add subcommands to macro
macro_subparsers = macro_parser.add_subparsers(dest='subcommand')
Expand Down
4 changes: 2 additions & 2 deletions examples/subcommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']

# create the top-level parser for the base command
base_parser = argparse.ArgumentParser(prog='base')
base_parser = argparse.ArgumentParser()
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')

# create the parser for the "foo" subcommand
Expand All @@ -38,7 +38,7 @@

# create the top-level parser for the alternate command
# The alternate command doesn't provide its own help flag
base2_parser = argparse.ArgumentParser(prog='alternate', add_help=False)
base2_parser = argparse.ArgumentParser(add_help=False)
base2_subparsers = base2_parser.add_subparsers(title='subcommands', help='subcommand help')

# create the parser for the "foo" subcommand
Expand Down
2 changes: 1 addition & 1 deletion examples/tab_autocompletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def _do_vid_shows(self, args) -> None:
'\n '.join(ep_list)))
print()

video_parser = Cmd2ArgumentParser(prog='media')
video_parser = Cmd2ArgumentParser()

video_types_subparsers = video_parser.add_subparsers(title='Media Types', dest='type')

Expand Down
2 changes: 1 addition & 1 deletion tests/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def base_bar(self, args):
self.poutput('((%s))' % args.z)

# create the top-level parser for the base command
base_parser = argparse.ArgumentParser(prog='base')
base_parser = argparse.ArgumentParser()
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')

# create the parser for the "foo" subcommand
Expand Down
2 changes: 1 addition & 1 deletion tests/test_argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def __init__(self, *args, **kwargs):
# Begin code related to help and command name completion
############################################################################################################
# Top level parser for music command
music_parser = Cmd2ArgumentParser(description='Manage music', prog='music')
music_parser = Cmd2ArgumentParser(description='Manage music')

# Add subcommands to music
music_subparsers = music_parser.add_subparsers()
Expand Down
32 changes: 16 additions & 16 deletions tests/test_argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def fake_func():
({'completer_function': fake_func, 'completer_method': fake_func}, False),
])
def test_apcustom_choices_callable_count(kwargs, is_valid):
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
try:
parser.add_argument('name', **kwargs)
assert is_valid
Expand All @@ -66,7 +66,7 @@ def test_apcustom_choices_callable_count(kwargs, is_valid):
])
def test_apcustom_no_choices_callables_alongside_choices(kwargs):
with pytest.raises(TypeError) as excinfo:
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
parser.add_argument('name', choices=['my', 'choices', 'list'], **kwargs)
assert 'None of the following parameters can be used alongside a choices parameter' in str(excinfo.value)

Expand All @@ -79,7 +79,7 @@ def test_apcustom_no_choices_callables_alongside_choices(kwargs):
])
def test_apcustom_no_choices_callables_when_nargs_is_0(kwargs):
with pytest.raises(TypeError) as excinfo:
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
parser.add_argument('name', action='store_true', **kwargs)
assert 'None of the following parameters can be used on an action that takes no arguments' in str(excinfo.value)

Expand Down Expand Up @@ -126,40 +126,40 @@ def test_apcustom_nargs_range_validation(cust_app):
])
def test_apcustom_narg_invalid_tuples(nargs_tuple):
with pytest.raises(ValueError) as excinfo:
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
parser.add_argument('invalid_tuple', nargs=nargs_tuple)
assert 'Ranged values for nargs must be a tuple of 1 or 2 integers' in str(excinfo.value)


def test_apcustom_narg_tuple_order():
with pytest.raises(ValueError) as excinfo:
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
parser.add_argument('invalid_tuple', nargs=(2, 1))
assert 'Invalid nargs range. The first value must be less than the second' in str(excinfo.value)


def test_apcustom_narg_tuple_negative():
with pytest.raises(ValueError) as excinfo:
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
parser.add_argument('invalid_tuple', nargs=(-1, 1))
assert 'Negative numbers are invalid for nargs range' in str(excinfo.value)


# noinspection PyUnresolvedReferences
def test_apcustom_narg_tuple_zero_base():
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(0,))
assert arg.nargs == argparse.ZERO_OR_MORE
assert arg.nargs_range is None
assert "[arg [...]]" in parser.format_help()

parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(0, 1))
assert arg.nargs == argparse.OPTIONAL
assert arg.nargs_range is None
assert "[arg]" in parser.format_help()

parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(0, 3))
assert arg.nargs == argparse.ZERO_OR_MORE
assert arg.nargs_range == (0, 3)
Expand All @@ -168,13 +168,13 @@ def test_apcustom_narg_tuple_zero_base():

# noinspection PyUnresolvedReferences
def test_apcustom_narg_tuple_one_base():
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(1,))
assert arg.nargs == argparse.ONE_OR_MORE
assert arg.nargs_range is None
assert "arg [...]" in parser.format_help()

parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(1, 5))
assert arg.nargs == argparse.ONE_OR_MORE
assert arg.nargs_range == (1, 5)
Expand All @@ -185,13 +185,13 @@ def test_apcustom_narg_tuple_one_base():
def test_apcustom_narg_tuple_other_ranges():

# Test range with no upper bound on max
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(2,))
assert arg.nargs == argparse.ONE_OR_MORE
assert arg.nargs_range == (2, INFINITY)

# Test finite range
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(2, 5))
assert arg.nargs == argparse.ONE_OR_MORE
assert arg.nargs_range == (2, 5)
Expand All @@ -202,13 +202,13 @@ def test_apcustom_print_message(capsys):
test_message = 'The test message'

# Specify the file
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
parser._print_message(test_message, file=sys.stdout)
out, err = capsys.readouterr()
assert test_message in out

# Make sure file defaults to sys.stderr
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
parser._print_message(test_message)
out, err = capsys.readouterr()
assert test_message in err
Expand Down Expand Up @@ -239,6 +239,6 @@ def test_generate_range_error():

def test_apcustom_required_options():
# Make sure a 'required arguments' section shows when a flag is marked required
parser = Cmd2ArgumentParser(prog='test')
parser = Cmd2ArgumentParser()
parser.add_argument('--required_flag', required=True)
assert 'required arguments' in parser.format_help()
2 changes: 1 addition & 1 deletion tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,7 @@ def base_sport(self, args):
self.poutput('Sport is {}'.format(args.sport))

# create the top-level parser for the base command
base_parser = argparse.ArgumentParser(prog='base')
base_parser = argparse.ArgumentParser()
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')

# create the parser for the "foo" subcommand
Expand Down