Skip to content

Commit 07ff01f

Browse files
authored
Fix 'index out of range' error in decorators._parse_positionals(). (#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.
1 parent e4ab25f commit 07ff01f

File tree

4 files changed

+47
-29
lines changed

4 files changed

+47
-29
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.5.9 (TBD)
2+
* Bug Fixes
3+
* Fixed 'index out of range' error when passing no arguments to an argparse-based command function.
4+
15
## 2.5.8 (December 17, 2024)
26
* Bug Fixes
37
* Rolled back undocumented changes to printing functions introduced in 2.5.0.

cmd2/decorators.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Stateme
9191
Cmd,
9292
)
9393

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

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

105105

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

120120

121-
#: Function signature for an Command Function that accepts a pre-processed argument list from user input
121+
#: Function signature for a command function that accepts a pre-processed argument list from user input
122122
#: and optionally returns a boolean
123123
ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, List[str]], Optional[bool]]
124-
#: Function signature for an Command Function that accepts a pre-processed argument list from user input
124+
#: Function signature for a command function that accepts a pre-processed argument list from user input
125125
#: and returns a boolean
126126
ArgListCommandFuncBoolReturn = Callable[[CommandParent, List[str]], bool]
127-
#: Function signature for an Command Function that accepts a pre-processed argument list from user input
127+
#: Function signature for a command function that accepts a pre-processed argument list from user input
128128
#: and returns Nothing
129129
ArgListCommandFuncNoneReturn = Callable[[CommandParent, List[str]], None]
130130

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

251251

252-
#: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input
253-
#: and optionally returns a boolean
252+
#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
253+
#: and optionally return a boolean
254254
ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], Optional[bool]]
255-
#: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input
256-
#: and returns a boolean
255+
ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace, List[str]], Optional[bool]]
256+
257+
#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
258+
#: and return a boolean
257259
ArgparseCommandFuncBoolReturn = Callable[[CommandParent, argparse.Namespace], bool]
258-
#: Function signature for an Command Function that uses an argparse.ArgumentParser to process user input
259-
#: and returns nothing
260+
ArgparseCommandFuncWithUnknownArgsBoolReturn = Callable[[CommandParent, argparse.Namespace, List[str]], bool]
261+
262+
#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
263+
#: and return nothing
260264
ArgparseCommandFuncNoneReturn = Callable[[CommandParent, argparse.Namespace], None]
265+
ArgparseCommandFuncWithUnknownArgsNoneReturn = Callable[[CommandParent, argparse.Namespace, List[str]], None]
261266

262-
#: Aggregate of all accepted function signatures for an argparse Command Function
267+
#: Aggregate of all accepted function signatures for an argparse command function
263268
ArgparseCommandFunc = Union[
264269
ArgparseCommandFuncOptionalBoolReturn[CommandParent],
270+
ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn[CommandParent],
265271
ArgparseCommandFuncBoolReturn[CommandParent],
272+
ArgparseCommandFuncWithUnknownArgsBoolReturn[CommandParent],
266273
ArgparseCommandFuncNoneReturn[CommandParent],
274+
ArgparseCommandFuncWithUnknownArgsNoneReturn[CommandParent],
267275
]
268276

269277

examples/modular_commands/commandset_basic.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
)
99

1010
from cmd2 import (
11-
Cmd,
1211
CommandSet,
1312
CompletionError,
1413
Statement,
@@ -32,15 +31,15 @@ class BasicCompletionCommandSet(CommandSet):
3231
'/home/other user/tests.db',
3332
]
3433

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

43-
def complete_flag_based(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]:
42+
def complete_flag_based(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
4443
"""Completion function for do_flag_based"""
4544
flag_dict = {
4645
# Tab complete food items after -f and --food flags in command line
@@ -50,38 +49,38 @@ def complete_flag_based(self, cmd: Cmd, text: str, line: str, begidx: int, endid
5049
'-s': self.sport_item_strs,
5150
'--sport': self.sport_item_strs,
5251
# Tab complete using path_complete function after -p and --path flags in command line
53-
'-p': cmd.path_complete,
54-
'--path': cmd.path_complete,
52+
'-p': self._cmd.path_complete,
53+
'--path': self._cmd.path_complete,
5554
}
5655

57-
return cmd.flag_based_complete(text, line, begidx, endidx, flag_dict=flag_dict)
56+
return self._cmd.flag_based_complete(text, line, begidx, endidx, flag_dict=flag_dict)
5857

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

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

71-
return cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
70+
return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
7271

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

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

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

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

9493
@with_category('Not Basic Completion')
95-
def do_custom_category(self, cmd: Cmd, statement: Statement):
94+
def do_custom_category(self, statement: Statement) -> None:
9695
self._cmd.poutput('Demonstrates a command that bypasses the default category')

tests/test_argparse.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ def test_argparse_remove_quotes(argparse_app):
148148
assert out == ['hello there']
149149

150150

151+
def test_argparse_with_no_args(argparse_app):
152+
"""Make sure we receive TypeError when calling argparse-based function with no args"""
153+
with pytest.raises(TypeError) as excinfo:
154+
argparse_app.do_say()
155+
assert 'Expected arguments' in str(excinfo.value)
156+
157+
151158
def test_argparser_kwargs(argparse_app, capsys):
152159
"""Test with_argparser wrapper passes through kwargs to command function"""
153160
argparse_app.do_say('word', keyword_arg="foo")

0 commit comments

Comments
 (0)