diff --git a/CHANGELOG.md b/CHANGELOG.md index 91eae494..6a6edb9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 098ea71c..cb254952 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -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] @@ -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]: @@ -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], @@ -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], ] diff --git a/examples/modular_commands/commandset_basic.py b/examples/modular_commands/commandset_basic.py index a4b7582f..8587b98d 100644 --- a/examples/modular_commands/commandset_basic.py +++ b/examples/modular_commands/commandset_basic.py @@ -8,7 +8,6 @@ ) from cmd2 import ( - Cmd, CommandSet, CompletionError, Statement, @@ -32,7 +31,7 @@ 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] @@ -40,7 +39,7 @@ def do_flag_based(self, cmd: Cmd, statement: Statement): """ 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 @@ -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. @@ -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') diff --git a/tests/test_argparse.py b/tests/test_argparse.py index c2731d37..f800c84a 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -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")