Skip to content

Commit

Permalink
Fix 'index out of range' error in decorators._parse_positionals(). (#…
Browse files Browse the repository at this point in the history
…1401)

* Fixed 'index out of range' error when passing no arguments to an argparse-based command function.

* Added Callable types for argparse-based commands which use with_unknown_args.

* Fixed broken example.
  • Loading branch information
kmvanbrunt authored Jan 9, 2025
1 parent e4ab25f commit 07ff01f
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 29 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.5.9 (TBD)
* Bug Fixes
* Fixed 'index out of range' error when passing no arguments to an argparse-based command function.

## 2.5.8 (December 17, 2024)
* Bug Fixes
* Rolled back undocumented changes to printing functions introduced in 2.5.0.
Expand Down
34 changes: 21 additions & 13 deletions cmd2/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Stateme
Cmd,
)

if (isinstance(arg, Cmd) or isinstance(arg, CommandSet)) and len(args) > pos:
if isinstance(arg, (Cmd, CommandSet)) and len(args) > pos + 1:
if isinstance(arg, CommandSet):
arg = arg._cmd
next_arg = args[pos + 1]
Expand All @@ -100,7 +100,7 @@ def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Stateme

# This shouldn't happen unless we forget to pass statement in `Cmd.onecmd` or
# somehow call the unbound class method.
raise TypeError('Expected arguments: cmd: cmd2.Cmd, statement: Union[Statement, str] Not found') # pragma: no cover
raise TypeError('Expected arguments: cmd: cmd2.Cmd, statement: Union[Statement, str] Not found')


def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> List[Any]:
Expand All @@ -118,17 +118,17 @@ def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) ->
return args_list


#: Function signature for an Command Function that accepts a pre-processed argument list from user input
#: Function signature for a command function that accepts a pre-processed argument list from user input
#: and optionally returns a boolean
ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, List[str]], Optional[bool]]
#: Function signature for an Command Function that accepts a pre-processed argument list from user input
#: Function signature for a command function that accepts a pre-processed argument list from user input
#: and returns a boolean
ArgListCommandFuncBoolReturn = Callable[[CommandParent, List[str]], bool]
#: Function signature for an Command Function that accepts a pre-processed argument list from user input
#: Function signature for a command function that accepts a pre-processed argument list from user input
#: and returns Nothing
ArgListCommandFuncNoneReturn = Callable[[CommandParent, List[str]], None]

#: Aggregate of all accepted function signatures for Command Functions that accept a pre-processed argument list
#: Aggregate of all accepted function signatures for command functions that accept a pre-processed argument list
ArgListCommandFunc = Union[
ArgListCommandFuncOptionalBoolReturn[CommandParent],
ArgListCommandFuncBoolReturn[CommandParent],
Expand Down Expand Up @@ -249,21 +249,29 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
req_args.append(action.dest)


#: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input
#: and optionally returns a boolean
#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
#: and optionally return a boolean
ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], Optional[bool]]
#: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input
#: and returns a boolean
ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace, List[str]], Optional[bool]]

#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
#: and return a boolean
ArgparseCommandFuncBoolReturn = Callable[[CommandParent, argparse.Namespace], bool]
#: Function signature for an Command Function that uses an argparse.ArgumentParser to process user input
#: and returns nothing
ArgparseCommandFuncWithUnknownArgsBoolReturn = Callable[[CommandParent, argparse.Namespace, List[str]], bool]

#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
#: and return nothing
ArgparseCommandFuncNoneReturn = Callable[[CommandParent, argparse.Namespace], None]
ArgparseCommandFuncWithUnknownArgsNoneReturn = Callable[[CommandParent, argparse.Namespace, List[str]], None]

#: Aggregate of all accepted function signatures for an argparse Command Function
#: Aggregate of all accepted function signatures for an argparse command function
ArgparseCommandFunc = Union[
ArgparseCommandFuncOptionalBoolReturn[CommandParent],
ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn[CommandParent],
ArgparseCommandFuncBoolReturn[CommandParent],
ArgparseCommandFuncWithUnknownArgsBoolReturn[CommandParent],
ArgparseCommandFuncNoneReturn[CommandParent],
ArgparseCommandFuncWithUnknownArgsNoneReturn[CommandParent],
]


Expand Down
31 changes: 15 additions & 16 deletions examples/modular_commands/commandset_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
)

from cmd2 import (
Cmd,
CommandSet,
CompletionError,
Statement,
Expand All @@ -32,15 +31,15 @@ class BasicCompletionCommandSet(CommandSet):
'/home/other user/tests.db',
]

def do_flag_based(self, cmd: Cmd, statement: Statement):
def do_flag_based(self, statement: Statement) -> None:
"""Tab completes arguments based on a preceding flag using flag_based_complete
-f, --food [completes food items]
-s, --sport [completes sports]
-p, --path [completes local file system paths]
"""
self._cmd.poutput("Args: {}".format(statement.args))

def complete_flag_based(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]:
def complete_flag_based(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""Completion function for do_flag_based"""
flag_dict = {
# Tab complete food items after -f and --food flags in command line
Expand All @@ -50,38 +49,38 @@ def complete_flag_based(self, cmd: Cmd, text: str, line: str, begidx: int, endid
'-s': self.sport_item_strs,
'--sport': self.sport_item_strs,
# Tab complete using path_complete function after -p and --path flags in command line
'-p': cmd.path_complete,
'--path': cmd.path_complete,
'-p': self._cmd.path_complete,
'--path': self._cmd.path_complete,
}

return cmd.flag_based_complete(text, line, begidx, endidx, flag_dict=flag_dict)
return self._cmd.flag_based_complete(text, line, begidx, endidx, flag_dict=flag_dict)

def do_index_based(self, cmd: Cmd, statement: Statement):
def do_index_based(self, statement: Statement) -> None:
"""Tab completes first 3 arguments using index_based_complete"""
self._cmd.poutput("Args: {}".format(statement.args))

def complete_index_based(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]:
def complete_index_based(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""Completion function for do_index_based"""
index_dict = {
1: self.food_item_strs, # Tab complete food items at index 1 in command line
2: self.sport_item_strs, # Tab complete sport items at index 2 in command line
3: cmd.path_complete, # Tab complete using path_complete function at index 3 in command line
3: self._cmd.path_complete, # Tab complete using path_complete function at index 3 in command line
}

return cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)

def do_delimiter_complete(self, cmd: Cmd, statement: Statement):
def do_delimiter_complete(self, statement: Statement) -> None:
"""Tab completes files from a list using delimiter_complete"""
self._cmd.poutput("Args: {}".format(statement.args))

def complete_delimiter_complete(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]:
return cmd.delimiter_complete(text, line, begidx, endidx, match_against=self.file_strs, delimiter='/')
def complete_delimiter_complete(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
return self._cmd.delimiter_complete(text, line, begidx, endidx, match_against=self.file_strs, delimiter='/')

def do_raise_error(self, cmd: Cmd, statement: Statement):
def do_raise_error(self, statement: Statement) -> None:
"""Demonstrates effect of raising CompletionError"""
self._cmd.poutput("Args: {}".format(statement.args))

def complete_raise_error(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]:
def complete_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""
CompletionErrors can be raised if an error occurs while tab completing.
Expand All @@ -92,5 +91,5 @@ def complete_raise_error(self, cmd: Cmd, text: str, line: str, begidx: int, endi
raise CompletionError("This is how a CompletionError behaves")

@with_category('Not Basic Completion')
def do_custom_category(self, cmd: Cmd, statement: Statement):
def do_custom_category(self, statement: Statement) -> None:
self._cmd.poutput('Demonstrates a command that bypasses the default category')
7 changes: 7 additions & 0 deletions tests/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ def test_argparse_remove_quotes(argparse_app):
assert out == ['hello there']


def test_argparse_with_no_args(argparse_app):
"""Make sure we receive TypeError when calling argparse-based function with no args"""
with pytest.raises(TypeError) as excinfo:
argparse_app.do_say()
assert 'Expected arguments' in str(excinfo.value)


def test_argparser_kwargs(argparse_app, capsys):
"""Test with_argparser wrapper passes through kwargs to command function"""
argparse_app.do_say('word', keyword_arg="foo")
Expand Down

0 comments on commit 07ff01f

Please sign in to comment.