diff options
-rw-r--r-- | CHANGELOG.md | 5 | ||||
-rw-r--r-- | cmd2/cmd2.py | 185 | ||||
-rw-r--r-- | tests/test_cmd2.py | 149 | ||||
-rw-r--r-- | tests/transcripts/from_cmdloop.txt | 1 |
4 files changed, 164 insertions, 176 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e372f438..cfd0c78c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,8 @@ * The history of entered commands previously was saved using the readline persistence mechanism, and only persisted if you had readline installed. Now history is persisted independent of readline; user input from previous invocations of `cmd2` based apps now shows in the `history` command. - + * Text scripts now run immediately instead of adding their commands to `cmdqueue`. This allows easy capture of + the entire script's output. * Breaking changes * Replaced `unquote_redirection_tokens()` with `unquote_specific_tokens()`. This was to support the fix that allows terminators in alias and macro values. @@ -42,6 +43,8 @@ on first launch of a `cmd2` based app of version 0.9.13 or higher. * HistoryItem class is no longer a subclass of `str`. If you are directly accessing the `.history` attribute of a `cmd2` based app, you will need to update your code to use `.history.get(1).statement.raw` instead. + * Removed internally used `eos` command that was used to keep track of when a text script's commands ended + * Removed cmd2 member called _STOP_AND_EXIT since it was just a boolean value that should always be True * **Python 3.4 EOL notice** * Python 3.4 reached its [end of life](https://www.python.org/dev/peps/pep-0429/) on March 18, 2019 * This is the last release of `cmd2` which will support Python 3.4 diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 387fe6d9..50dbcd79 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -400,13 +400,13 @@ class Cmd(cmd.Cmd): 'timing': 'Report execution times'} # Commands to exclude from the help menu and tab completion - self.hidden_commands = ['eof', 'eos', '_relative_load'] + self.hidden_commands = ['eof', '_relative_load'] # Commands to exclude from the history command # initialize history self.persistent_history_length = persistent_history_length self._initialize_history(persistent_history_file) - self.exclude_from_history = '''history edit eof eos'''.split() + self.exclude_from_history = '''history edit eof'''.split() # Command aliases and macros self.macros = dict() @@ -435,9 +435,6 @@ class Cmd(cmd.Cmd): # Built-in commands don't make use of this. It is purely there for user-defined commands and convenience. self._last_result = None - # Codes used for exit conditions - self._STOP_AND_EXIT = True # cmd convention - # Used load command to store the current script dir as a LIFO queue to support _relative_load command self._script_dir = [] @@ -1689,7 +1686,7 @@ class Cmd(cmd.Cmd): :param pyscript_bridge_call: This should only ever be set to True by PyscriptBridge to signify the beginning of an app() call in a pyscript. It is used to enable/disable the storage of the command's stdout. - :return: True if cmdloop() should exit, False otherwise + :return: True if running of commands should stop """ import datetime @@ -1811,46 +1808,21 @@ class Cmd(cmd.Cmd): self.perror(ex) def runcmds_plus_hooks(self, cmds: List[str]) -> bool: - """Convenience method to run multiple commands by onecmd_plus_hooks. - - This method adds the given cmds to the command queue and processes the - queue until completion or an error causes it to abort. Scripts that are - loaded will have their commands added to the queue. Scripts may even - load other scripts recursively. This means, however, that you should not - use this method if there is a running cmdloop or some other event-loop. - This method is only intended to be used in "one-off" scenarios. - - NOTE: You may need this method even if you only have one command. If - that command is a load, then you will need this command to fully process - all the subsequent commands that are loaded from the script file. This - is an improvement over onecmd_plus_hooks, which expects to be used - inside of a command loop which does the processing of loaded commands. - - Example: cmd_obj.runcmds_plus_hooks(['load myscript.txt']) - - :param cmds: command strings suitable for onecmd_plus_hooks. - :return: True implies the entire application should exit. + """ + Used when commands are being run in an automated fashion like text scripts or history replays. + The prompt and command line for each command will be printed if echo is True. + :param cmds: command strings suitable for onecmd_plus_hooks + :return: True if running of commands should stop """ - stop = False - self.cmdqueue = list(cmds) + self.cmdqueue - try: - while self.cmdqueue and not stop: - line = self.cmdqueue.pop(0) - if self.echo and line != 'eos': - self.poutput('{}{}'.format(self.prompt, line)) + for line in cmds: + if self.echo: + self.poutput('{}{}'.format(self.prompt, line)) - stop = self.onecmd_plus_hooks(line) - finally: - # Clear out the command queue and script directory stack, just in - # case we hit an error and they were not completed. - self.cmdqueue = [] - self._script_dir = [] - # NOTE: placing this return here inside the finally block will - # swallow exceptions. This is consistent with what is done in - # onecmd_plus_hooks and _cmdloop, although it may not be - # necessary/desired here. - return stop + if self.onecmd_plus_hooks(line): + return True + + return False def _complete_statement(self, line: str) -> Statement: """Keep accepting lines of input until the command is complete. @@ -2234,14 +2206,12 @@ class Cmd(cmd.Cmd): return line.strip() - def _cmdloop(self) -> bool: + def _cmdloop(self) -> None: """Repeatedly issue a prompt, accept input, parse an initial prefix off the received input, and dispatch to action methods, passing them the remainder of the line as argument. This serves the same role as cmd.cmdloop(). - - :return: True implies the entire application should exit. """ # An almost perfect copy from Cmd; however, the pseudo_raw_input portion # has been split out so that it can be called separately @@ -2271,15 +2241,13 @@ class Cmd(cmd.Cmd): # Enable tab completion readline.parse_and_bind(self.completekey + ": complete") - stop = False try: + stop = False while not stop: if self.cmdqueue: - # Run command out of cmdqueue if nonempty (populated by load command or commands at invocation) - line = self.cmdqueue.pop(0) - - if self.echo and line != 'eos': - self.poutput('{}{}'.format(self.prompt, line)) + # Run commands out of cmdqueue if nonempty (populated by commands at invocation) + stop = self.runcmds_plus_hooks(self.cmdqueue) + self.cmdqueue.clear() else: # Otherwise, read a command from stdin try: @@ -2291,8 +2259,8 @@ class Cmd(cmd.Cmd): self.poutput('^C') line = '' - # Run the command along with all associated pre and post hooks - stop = self.onecmd_plus_hooks(line) + # Run the command along with all associated pre and post hooks + stop = self.onecmd_plus_hooks(line) finally: if self.use_rawinput and self.completekey and rl_type != RlType.NONE: @@ -2306,11 +2274,7 @@ class Cmd(cmd.Cmd): elif rl_type == RlType.PYREADLINE: # noinspection PyUnresolvedReferences readline.rl.mode._display_completions = orig_pyreadline_display - self.cmdqueue.clear() - self._script_dir.clear() - - return stop # ----- Alias sub-command functions ----- @@ -2863,14 +2827,15 @@ class Cmd(cmd.Cmd): @with_argparser(ACArgumentParser(epilog=INTERNAL_COMMAND_EPILOG)) def do_eof(self, _: argparse.Namespace) -> bool: """Called when <Ctrl>-D is pressed""" - # End of script should not exit app, but <Ctrl>-D should. - return self._STOP_AND_EXIT + # Return True to stop the command loop + return True @with_argparser(ACArgumentParser()) def do_quit(self, _: argparse.Namespace) -> bool: """Exit this application""" self._should_quit = True - return self._STOP_AND_EXIT + # Return True to stop the command loop + return True def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> str: @@ -3313,7 +3278,9 @@ class Cmd(cmd.Cmd): embed(banner1=banner, exit_msg=exit_msg) load_ipy(bridge) - history_parser = ACArgumentParser() + history_description = "View, run, edit, save, or clear previously entered commands" + + history_parser = ACArgumentParser(description=history_description) history_action_group = history_parser.add_mutually_exclusive_group() history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items') history_action_group.add_argument('-e', '--edit', action='store_true', @@ -3349,8 +3316,11 @@ class Cmd(cmd.Cmd): history_parser.add_argument('arg', nargs='?', help=history_arg_help) @with_argparser(history_parser) - def do_history(self, args: argparse.Namespace) -> None: - """View, run, edit, save, or clear previously entered commands""" + def do_history(self, args: argparse.Namespace) -> Optional[bool]: + """ + View, run, edit, save, or clear previously entered commands + :return: True if running of commands should stop + """ # -v must be used alone with no other options if args.verbose: @@ -3412,10 +3382,7 @@ class Cmd(cmd.Cmd): self.perror("If this is what you want to do, specify '1:' as the range of history.", traceback_war=False) else: - for runme in history: - self.pfeedback(runme.raw) - if runme: - self.onecmd_plus_hooks(runme.raw) + return self.runcmds_plus_hooks(history) elif args.edit: import tempfile fd, fname = tempfile.mkstemp(suffix='.txt', text=True) @@ -3427,9 +3394,7 @@ class Cmd(cmd.Cmd): fobj.write('{}\n'.format(command.raw)) try: self.do_edit(fname) - self.do_load(fname) - except Exception: - raise + return self.do_load(fname) finally: os.remove(fname) elif args.output_file: @@ -3445,7 +3410,7 @@ class Cmd(cmd.Cmd): except Exception as e: self.perror('Saving {!r} - {}'.format(args.output_file, e), traceback_war=False) elif args.transcript: - self._generate_transcript(history, args.transcript) + return self._generate_transcript(history, args.transcript) else: # Display the history items retrieved for hi in history: @@ -3524,8 +3489,11 @@ class Cmd(cmd.Cmd): msg = "can not write persistent history file '{}': {}" self.perror(msg.format(self.persistent_history_file, ex), traceback_war=False) - def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcript_file: str) -> None: - """Generate a transcript file from a given history of commands.""" + def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcript_file: str) -> Optional[bool]: + """ + Generate a transcript file from a given history of commands + :return: True if running of commands should stop + """ import io # Validate the transcript file path to make sure directory exists and write access is available transcript_path = os.path.abspath(os.path.expanduser(transcript_file)) @@ -3535,6 +3503,8 @@ class Cmd(cmd.Cmd): traceback_war=False) return + commands_run = 0 + stop = False try: with self.sigint_protection: # Disable echo while we manually redirect stdout to a StringIO buffer @@ -3570,14 +3540,21 @@ class Cmd(cmd.Cmd): # of the command membuf = io.StringIO() self.stdout = membuf + # then run the command and let the output go into our buffer - self.onecmd_plus_hooks(history_item) + stop = self.onecmd_plus_hooks(history_item) + commands_run += 1 + # rewind the buffer to the beginning membuf.seek(0) # get the output out of the buffer output = membuf.read() # and add the regex-escaped output to the transcript transcript += output.replace('/', r'\/') + + # check if we are supposed to stop + if stop: + break finally: with self.sigint_protection: # Restore altered attributes to their original state @@ -3592,12 +3569,14 @@ class Cmd(cmd.Cmd): self.perror('Failed to save transcript: {}'.format(ex), traceback_war=False) else: # and let the user know what we did - if len(history) > 1: + if commands_run > 1: plural = 'commands and their outputs' else: plural = 'command and its output' msg = '{} {} saved to transcript file {!r}' - self.pfeedback(msg.format(len(history), plural, transcript_file)) + self.pfeedback(msg.format(commands_run, plural, transcript_file)) + + return stop edit_description = ("Edit a file in a text editor\n" "\n" @@ -3629,22 +3608,11 @@ class Cmd(cmd.Cmd): else: return None - @with_argparser(ACArgumentParser(epilog=INTERNAL_COMMAND_EPILOG)) - def do_eos(self, _: argparse.Namespace) -> None: - """Handle cleanup when a script has finished executing""" - if self._script_dir: - self._script_dir.pop() - load_description = ("Run commands in script file that is encoded as either ASCII or UTF-8 text\n" "\n" "Script should contain one command per line, just like the command would be\n" "typed in the console.\n" "\n" - "It loads commands from a script file into a queue and then the normal cmd2\n" - "REPL resumes control and executes the commands in the queue in FIFO order.\n" - "If you attempt to redirect/pipe a load command, it will capture the output\n" - "of the load command itself, not what it adds to the queue.\n" - "\n" "If the -r/--record_transcript flag is used, this command instead records\n" "the output of the script commands to a transcript for testing purposes.\n" ) @@ -3656,8 +3624,11 @@ class Cmd(cmd.Cmd): ACTION_ARG_CHOICES, ('path_complete',)) @with_argparser(load_parser) - def do_load(self, args: argparse.Namespace) -> None: - """Run commands in script file that is encoded as either ASCII or UTF-8 text""" + def do_load(self, args: argparse.Namespace) -> Optional[bool]: + """ + Run commands in script file that is encoded as either ASCII or UTF-8 text + :return: True if running of commands should stop + """ expanded_path = os.path.abspath(os.path.expanduser(args.script_path)) # Make sure the path exists and we can access it @@ -3681,21 +3652,28 @@ class Cmd(cmd.Cmd): return try: - # Read all lines of the script and insert into the head of the - # command queue. Add an "end of script (eos)" command to cleanup the - # self._script_dir list when done. + # Read all lines of the script with open(expanded_path, encoding='utf-8') as target: script_commands = target.read().splitlines() except OSError as ex: # pragma: no cover self.perror("Problem accessing script from '{}': {}".format(expanded_path, ex)) return - if args.transcript: - self._generate_transcript(script_commands, args.transcript) - return + orig_script_dir_count = len(self._script_dir) - self.cmdqueue = script_commands + ['eos'] + self.cmdqueue - self._script_dir.append(os.path.dirname(expanded_path)) + try: + self._script_dir.append(os.path.dirname(expanded_path)) + + if args.transcript: + return self._generate_transcript(script_commands, os.path.expanduser(args.transcript)) + else: + return self.runcmds_plus_hooks(script_commands) + + finally: + with self.sigint_protection: + # Check if a script dir was added before an exception occurred + if orig_script_dir_count != len(self._script_dir): + self._script_dir.pop() relative_load_description = load_description relative_load_description += ("\n\n" @@ -3709,12 +3687,15 @@ class Cmd(cmd.Cmd): relative_load_parser.add_argument('file_path', help='a file path pointing to a script') @with_argparser(relative_load_parser) - def do__relative_load(self, args: argparse.Namespace) -> None: - """Run commands in script file that is encoded as either ASCII or UTF-8 text""" + def do__relative_load(self, args: argparse.Namespace) -> Optional[bool]: + """ + Run commands in script file that is encoded as either ASCII or UTF-8 text + :return: True if running of commands should stop + """ file_path = args.file_path # NOTE: Relative path is an absolute path, it is just relative to the current script directory relative_path = os.path.join(self._current_script_dir or '', file_path) - self.do_load(relative_path) + return self.do_load(relative_path) def run_transcript_tests(self, callargs: List[str]) -> None: """Runs transcript tests for provided file(s). @@ -3740,7 +3721,7 @@ class Cmd(cmd.Cmd): self.decolorized_write(sys.stderr, stream.read()) self.poutput('Tests passed', color=Fore.LIGHTGREEN_EX) else: - # Strip off the initial trackeback which isn't particularly useful for end users + # Strip off the initial traceback which isn't particularly useful for end users error_str = stream.read() end_of_trace = error_str.find('AssertionError:') file_offset = error_str[end_of_trace:].find('File ') diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 1aafefc2..7ea4f227 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -297,17 +297,28 @@ def test_base_load(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') - assert base_app.cmdqueue == [] assert base_app._script_dir == [] assert base_app._current_script_dir is None - # Run the load command, which populates the command queue and sets the script directory - run_cmd(base_app, 'load {}'.format(filename)) + # Get output out the script + script_out, script_err = run_cmd(base_app, 'load {}'.format(filename)) - assert base_app.cmdqueue == ['help history', 'eos'] - sdir = os.path.dirname(filename) - assert base_app._script_dir == [sdir] - assert base_app._current_script_dir == sdir + assert base_app._script_dir == [] + assert base_app._current_script_dir is None + + # Now run the commands manually and compare their output to script's + with open(filename, encoding='utf-8') as file: + script_commands = file.read().splitlines() + + manual_out = [] + manual_err = [] + for cmdline in script_commands: + out, err = run_cmd(base_app, cmdline) + manual_out.extend(out) + manual_err.extend(err) + + assert script_out == manual_out + assert script_err == manual_err def test_load_with_empty_args(base_app): # The way the load command works, we can't directly capture its stdout or stderr @@ -315,7 +326,6 @@ def test_load_with_empty_args(base_app): # The load command requires a file path argument, so we should get an error message assert "the following arguments are required" in err[1] - assert base_app.cmdqueue == [] def test_load_with_nonexistent_file(base_app, capsys): @@ -324,7 +334,6 @@ def test_load_with_nonexistent_file(base_app, capsys): # The load command requires a path to an existing file assert "does not exist" in err[0] - assert base_app.cmdqueue == [] def test_load_with_directory(base_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -333,7 +342,6 @@ def test_load_with_directory(base_app, request): out, err = run_cmd(base_app, 'load {}'.format(test_dir)) assert "is not a file" in err[0] - assert base_app.cmdqueue == [] def test_load_with_empty_file(base_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -344,7 +352,6 @@ def test_load_with_empty_file(base_app, request): # The load command requires non-empty script files assert "is empty" in err[0] - assert base_app.cmdqueue == [] def test_load_with_binary_file(base_app, request): @@ -356,42 +363,45 @@ def test_load_with_binary_file(base_app, request): # The load command requires non-empty scripts files assert "is not an ASCII or UTF-8 encoded text file" in err[0] - assert base_app.cmdqueue == [] def test_load_with_utf8_file(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'utf8.txt') - assert base_app.cmdqueue == [] assert base_app._script_dir == [] assert base_app._current_script_dir is None - # Run the load command, which populates the command queue and sets the script directory - run_cmd(base_app, 'load {}'.format(filename)) + # Get output out the script + script_out, script_err = run_cmd(base_app, 'load {}'.format(filename)) + + assert base_app._script_dir == [] + assert base_app._current_script_dir is None - assert base_app.cmdqueue == ['!echo γνωρίζω', 'eos'] - sdir = os.path.dirname(filename) - assert base_app._script_dir == [sdir] - assert base_app._current_script_dir == sdir + # Now run the commands manually and compare their output to script's + with open(filename, encoding='utf-8') as file: + script_commands = file.read().splitlines() + + manual_out = [] + manual_err = [] + for cmdline in script_commands: + out, err = run_cmd(base_app, cmdline) + manual_out.extend(out) + manual_err.extend(err) + + assert script_out == manual_out + assert script_err == manual_err def test_load_nested_loads(base_app, request): # Verify that loading a script with nested load commands works correctly, - # and loads the nested script commands in the correct order. The recursive - # loads don't happen all at once, but as the commands are interpreted. So, - # we will need to drain the cmdqueue and inspect the stdout to see if all - # steps were executed in the expected order. + # and loads the nested script commands in the correct order. test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'nested.txt') - assert base_app.cmdqueue == [] - # Load the top level script and then run the command queue until all - # commands have been exhausted. + # Load the top level script initial_load = 'load ' + filename run_cmd(base_app, initial_load) - while base_app.cmdqueue: - base_app.onecmd_plus_hooks(base_app.cmdqueue.pop(0)) # Check that the right commands were executed. expected = """ @@ -407,12 +417,9 @@ set colors Never""" % initial_load def test_base_runcmds_plus_hooks(base_app, request): - # Make sure that runcmds_plus_hooks works as intended. I.E. to run multiple - # commands and process any commands added, by them, to the command queue. test_dir = os.path.dirname(request.module.__file__) prefilepath = os.path.join(test_dir, 'scripts', 'precmds.txt') postfilepath = os.path.join(test_dir, 'scripts', 'postcmds.txt') - assert base_app.cmdqueue == [] base_app.runcmds_plus_hooks(['load ' + prefilepath, 'help', @@ -429,27 +436,36 @@ set colors Never""" % (prefilepath, postfilepath) out, err = run_cmd(base_app, 'history -s') assert out == normalize(expected) - def test_base_relative_load(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') - assert base_app.cmdqueue == [] assert base_app._script_dir == [] assert base_app._current_script_dir is None - # Run the load command, which populates the command queue and sets the script directory - run_cmd(base_app, '_relative_load {}'.format(filename)) + # Get output out the script + script_out, script_err = run_cmd(base_app, 'load {}'.format(filename)) + + assert base_app._script_dir == [] + assert base_app._current_script_dir is None + + # Now run the commands manually and compare their output to script's + with open(filename, encoding='utf-8') as file: + script_commands = file.read().splitlines() - assert base_app.cmdqueue == ['help history', 'eos'] - sdir = os.path.dirname(filename) - assert base_app._script_dir == [sdir] - assert base_app._current_script_dir == sdir + manual_out = [] + manual_err = [] + for cmdline in script_commands: + out, err = run_cmd(base_app, cmdline) + manual_out.extend(out) + manual_err.extend(err) + + assert script_out == manual_out + assert script_err == manual_err def test_relative_load_requires_an_argument(base_app): out, err = run_cmd(base_app, '_relative_load') assert 'Error: the following arguments' in err[1] - assert base_app.cmdqueue == [] def test_output_redirection(base_app): @@ -495,7 +511,11 @@ def test_output_redirection_to_nonexistent_directory(base_app): assert content == expected def test_output_redirection_to_too_long_filename(base_app): - filename = '~/sdkfhksdjfhkjdshfkjsdhfkjsdhfkjdshfkjdshfkjshdfkhdsfkjhewfuihewiufhweiufhiweufhiuewhiuewhfiuwehfiuewhfiuewhfiuewhfiuewhiuewhfiuewhfiuewfhiuwehewiufhewiuhfiweuhfiuwehfiuewfhiuwehiuewfhiuewhiewuhfiuewhfiuwefhewiuhewiufhewiufhewiufhewiufhewiufhewiufhewiufhewiuhewiufhewiufhewiuheiufhiuewheiwufhewiufheiufheiufhieuwhfewiuhfeiufhiuewfhiuewheiwuhfiuewhfiuewhfeiuwfhewiufhiuewhiuewhfeiuwhfiuwehfuiwehfiuehiuewhfieuwfhieufhiuewhfeiuwfhiuefhueiwhfw' + filename = '~/sdkfhksdjfhkjdshfkjsdhfkjsdhfkjdshfkjdshfkjshdfkhdsfkjhewfuihewiufhweiufhiweufhiuewhiuewhfiuwehfia' \ + 'ewhfiuewhfiuewhfiuewhiuewhfiuewhfiuewfhiuwehewiufhewiuhfiweuhfiuwehfiuewfhiuwehiuewfhiuewhiewuhfiueh' \ + 'fiuwefhewiuhewiufhewiufhewiufhewiufhewiufhewiufhewiufhewiuhewiufhewiufhewiuheiufhiuewheiwufhewiufheu' \ + 'fheiufhieuwhfewiuhfeiufhiuewfhiuewheiwuhfiuewhfiuewhfeiuwfhewiufhiuewhiuewhfeiuwhfiuwehfuiwehfiuehie' \ + 'whfieuwfhieufhiuewhfeiuwfhiuefhueiwhfw' # Verify that writing to a file in a non-existent directory doesn't work run_cmd(base_app, 'help > {}'.format(filename)) @@ -766,12 +786,11 @@ def test_cmdloop_without_rawinput(outsim_app): testargs = ["prog"] expected = outsim_app.intro + '\n' with mock.patch.object(sys, 'argv', testargs): - # Run the command loop - outsim_app.cmdloop() + with pytest.raises(OSError): + outsim_app.cmdloop() out = outsim_app.stdout.getvalue() assert out == expected - class HookFailureApp(cmd2.Cmd): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -820,7 +839,10 @@ def test_interrupt_quit(say_app): m.side_effect = ['say hello', KeyboardInterrupt(), 'say goodbye', 'eof'] builtins.input = m - say_app.cmdloop() + try: + say_app.cmdloop() + except KeyboardInterrupt: + pass # And verify the expected output to stdout out = say_app.stdout.getvalue() @@ -834,7 +856,10 @@ def test_interrupt_noquit(say_app): m.side_effect = ['say hello', KeyboardInterrupt(), 'say goodbye', 'eof'] builtins.input = m - say_app.cmdloop() + try: + say_app.cmdloop() + except KeyboardInterrupt: + pass # And verify the expected output to stdout out = say_app.stdout.getvalue() @@ -1358,36 +1383,15 @@ def test_eof(base_app): # Only thing to verify is that it returns True assert base_app.do_eof('') -def test_eos(base_app): - sdir = 'dummy_dir' - base_app._script_dir.append(sdir) - assert len(base_app._script_dir) == 1 - - # Assert that it does NOT return true - assert not base_app.do_eos('') - - # And make sure it reduced the length of the script dir list - assert len(base_app._script_dir) == 0 - def test_echo(capsys): app = cmd2.Cmd() - # Turn echo on and pre-stage some commands in the queue, simulating like we are in the middle of a script app.echo = True - command = 'help history' - app.cmdqueue = [command, 'quit', 'eos'] - app._script_dir.append('some_dir') + commands = ['help history'] - assert app._current_script_dir is not None - - # Run the inner _cmdloop - app._cmdloop() + app.runcmds_plus_hooks(commands) out, err = capsys.readouterr() - - # Check the output - assert app.cmdqueue == [] - assert app._current_script_dir is None - assert out.startswith('{}{}\n'.format(app.prompt, command) + HELP_HISTORY.split()[0]) + assert out.startswith('{}{}\n'.format(app.prompt, commands[0]) + HELP_HISTORY.split()[0]) def test_pseudo_raw_input_tty_rawinput_true(): # use context managers so original functions get put back when we are done @@ -1913,7 +1917,7 @@ def test_onecmd_raw_str_quit(outsim_app): def test_get_all_commands(base_app): # Verify that the base app has the expected commands commands = base_app.get_all_commands() - expected_commands = ['_relative_load', 'alias', 'edit', 'eof', 'eos', 'help', 'history', 'load', 'macro', + expected_commands = ['_relative_load', 'alias', 'edit', 'eof', 'help', 'history', 'load', 'macro', 'py', 'pyscript', 'quit', 'set', 'shell', 'shortcuts'] assert commands == expected_commands @@ -1946,7 +1950,8 @@ Usage: exit [exit_code] self.exit_code = -1 self._should_quit = True - return self._STOP_AND_EXIT + # Return True to stop the command loop + return True def postloop(self) -> None: """Hook method executed once when the cmdloop() method is about to return.""" diff --git a/tests/transcripts/from_cmdloop.txt b/tests/transcripts/from_cmdloop.txt index 871b71f1..84d7f8fc 100644 --- a/tests/transcripts/from_cmdloop.txt +++ b/tests/transcripts/from_cmdloop.txt @@ -42,7 +42,6 @@ OODNIGHT, GRACIEGAY 5 set maxrepeats 5 6 say -ps --repeat=5 goodnight, Gracie (Cmd) history -r 4 -say -ps --repeat=5 goodnight, Gracie OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY |