summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md39
-rw-r--r--LICENSE2
-rw-r--r--cmd2/argparse_completer.py3
-rw-r--r--cmd2/cmd2.py301
-rw-r--r--cmd2/constants.py2
-rw-r--r--cmd2/pyscript_bridge.py27
-rw-r--r--cmd2/transcript.py24
-rw-r--r--cmd2/utils.py29
-rw-r--r--docs/freefeatures.rst4
-rw-r--r--docs/hooks.rst2
-rwxr-xr-xexamples/cmd_as_argument.py3
-rwxr-xr-xexamples/decorator_example.py3
-rwxr-xr-xexamples/persistent_history.py3
-rw-r--r--examples/scripts/quit.txt1
-rwxr-xr-xexamples/tab_autocomp_dynamic.py3
-rwxr-xr-xexamples/tab_autocompletion.py3
-rw-r--r--examples/transcripts/quit.txt1
-rw-r--r--tests/conftest.py3
-rw-r--r--tests/pyscript/stop.py9
-rw-r--r--tests/test_acargparse.py3
-rw-r--r--tests/test_autocompletion.py3
-rw-r--r--tests/test_cmd2.py259
-rw-r--r--tests/test_completion.py3
-rw-r--r--tests/test_parsing.py3
-rw-r--r--tests/test_plugin.py47
-rw-r--r--tests/test_pyscript.py14
-rw-r--r--tests/test_transcript.py89
-rw-r--r--tests/test_utils.py3
-rw-r--r--tests/transcripts/from_cmdloop.txt1
29 files changed, 484 insertions, 403 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e372f438..b6786706 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,39 +9,50 @@
precedence even though it appeared later in the command.
* Fixed issue where quotes around redirection file paths were being lost in `Statement.expanded_command_line()`
* Fixed a bug in how line numbers were calculated for transcript testing
+ * Fixed issue where `_cmdloop()` suppressed exceptions by returning from within its `finally` code
+ * Fixed UnsupportedOperation on fileno error when a shell command was one of the commands run while generating
+ a transcript
* Enhancements
* Added capability to chain pipe commands and redirect their output (e.g. !ls -l | grep user | wc -l > out.txt)
* `pyscript` limits a command's stdout capture to the same period that redirection does.
- Therefore output from a command's postparsing and finalization hooks isn't saved in the StdSim object.
+ Therefore output from a command's postparsing and finalization hooks isn't saved in the StdSim object.
* `StdSim.buffer.write()` now flushes when the wrapped stream uses line buffering and the bytes being written
- contain a newline or carriage return. This helps when `pyscript` is echoing the output of a shell command
- since the output will print at the same frequency as when the command is run in a terminal.
+ contain a newline or carriage return. This helps when `pyscript` is echoing the output of a shell command
+ since the output will print at the same frequency as when the command is run in a terminal.
* **ACArgumentParser** no longer prints complete help text when a parsing error occurs since long help messages
- scroll the actual error message off the screen.
+ scroll the actual error message off the screen.
* Exceptions occurring in tab completion functions are now printed to stderr before returning control back to
readline. This makes debugging a lot easier since readline suppresses these exceptions.
* Added support for custom Namespaces in the argparse decorators. See description of `ns_provider` argument
for more information.
* Transcript testing now sets the `exit_code` returned from `cmdloop` based on Success/Failure
* 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.
-
+ 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.
+ * Added member to `CommandResult` called `stop` which is the return value of `onecmd_plus_hooks` after it runs
+ the given command line.
* Breaking changes
* Replaced `unquote_redirection_tokens()` with `unquote_specific_tokens()`. This was to support the fix
- that allows terminators in alias and macro values.
+ that allows terminators in alias and macro values.
* Changed `Statement.pipe_to` to a string instead of a list
* `preserve_quotes` is now a keyword-only argument in the argparse decorators
* Refactored so that `cmd2.Cmd.cmdloop()` returns the `exit_code` instead of a call to `sys.exit()`
- It is now application developer's responsibility to treat the return value from `cmdloop()` accordingly
+ It is now application developer's responsibility to treat the return value from `cmdloop()` accordingly
* Only valid commands are persistent in history between invocations of `cmd2` based apps. Previously
- all user input was persistent in history. If readline is installed, the history available with the up and
- down arrow keys (readline history) may not match that shown in the `history` command, because `history`
- only tracks valid input, while readline history captures all input.
+ all user input was persistent in history. If readline is installed, the history available with the up and
+ down arrow keys (readline history) may not match that shown in the `history` command, because `history`
+ only tracks valid input, while readline history captures all input.
* History is now persisted in a binary format, not plain text format. Previous history files are destroyed
- on first launch of a `cmd2` based app of version 0.9.13 or higher.
+ 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.
+ 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
+ * Removed `cmd2` member called `_should_quit` since `PyscriptBridge` now handles this logic
+ * Removed support for `cmd.cmdqueue`
+ * `allow_cli_args` is now an argument to __init__ instead of a `cmd2` class member
* **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/LICENSE b/LICENSE
index 2eb890d7..7f916117 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2008-2016 Catherine Devlin and others
+Copyright (c) 2008-2019 Catherine Devlin and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index feff4835..6b3f5298 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -55,9 +55,6 @@ How to supply completion choice lists or functions for sub-commands:
The subcommand group dictionary maps subcommand names to tuple(arg_choices, subcmd_args_lookup)
For more details of this more complex approach see tab_autocompletion.py in the examples
-
-Copyright 2018 Eric Lin <anselor@gmail.com>
-Released under MIT license, see LICENSE file
"""
import argparse
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 387fe6d9..02462d96 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -335,11 +335,12 @@ class Cmd(cmd.Cmd):
DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
DEFAULT_EDITOR = utils.find_editor()
- def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent_history_file: str = '',
- persistent_history_length: int = 1000, startup_script: Optional[str] = None, use_ipython: bool = False,
- transcript_files: Optional[List[str]] = None, allow_redirection: bool = True,
- multiline_commands: Optional[List[str]] = None, terminators: Optional[List[str]] = None,
- shortcuts: Optional[Dict[str, str]] = None) -> None:
+ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
+ persistent_history_file: str = '', persistent_history_length: int = 1000,
+ startup_script: Optional[str] = None, use_ipython: bool = False,
+ allow_cli_args: bool = True, transcript_files: Optional[List[str]] = None,
+ allow_redirection: bool = True, multiline_commands: Optional[List[str]] = None,
+ terminators: Optional[List[str]] = None, shortcuts: Optional[Dict[str, str]] = None) -> None:
"""An easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.
:param completekey: (optional) readline name of a completion key, default to Tab
@@ -349,6 +350,9 @@ class Cmd(cmd.Cmd):
:param persistent_history_length: (optional) max number of history items to write to the persistent history file
:param startup_script: (optional) file path to a a script to load and execute at startup
:param use_ipython: (optional) should the "ipy" command be included for an embedded IPython shell
+ :param allow_cli_args: (optional) if True, then cmd2 will process command line arguments as either
+ commands to be run or, if -t is specified, transcript files to run.
+ This should be set to False if your application parses its own arguments.
:param transcript_files: (optional) allows running transcript tests when allow_cli_args is False
:param allow_redirection: (optional) should output redirection and pipes be allowed
:param multiline_commands: (optional) list of commands allowed to accept multi-line input
@@ -372,7 +376,6 @@ class Cmd(cmd.Cmd):
super().__init__(completekey=completekey, stdin=stdin, stdout=stdout)
# Attributes which should NOT be dynamically settable at runtime
- self.allow_cli_args = True # Should arguments passed on the command-line be processed as commands?
self.default_to_shell = False # Attempt to run unrecognized commands as shell commands
self.quit_on_sigint = False # Quit the loop on interrupt instead of just resetting prompt
@@ -400,13 +403,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()
@@ -423,10 +426,6 @@ class Cmd(cmd.Cmd):
terminators=terminators,
multiline_commands=multiline_commands,
shortcuts=shortcuts)
- self._transcript_files = transcript_files
-
- # Used to enable the ability for a Python script to quit the application
- self._should_quit = False
# True if running inside a Python script or interactive console, False otherwise
self._in_py = False
@@ -435,9 +434,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 = []
@@ -466,11 +462,33 @@ class Cmd(cmd.Cmd):
# If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing
self.broken_pipe_warning = ''
+ # Commands that will run at the beginning of the command loop
+ self._startup_commands = []
+
# If a startup script is provided, then add it in the queue to load
if startup_script is not None:
startup_script = os.path.abspath(os.path.expanduser(startup_script))
if os.path.exists(startup_script) and os.path.getsize(startup_script) > 0:
- self.cmdqueue.append("load '{}'".format(startup_script))
+ self._startup_commands.append("load '{}'".format(startup_script))
+
+ # Transcript files to run instead of interactive command loop
+ self._transcript_files = None
+
+ # Check for command line args
+ if allow_cli_args:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-t', '--test', action="store_true",
+ help='Test against transcript(s) in FILE (wildcards OK)')
+ callopts, callargs = parser.parse_known_args()
+
+ # If transcript testing was called for, use other arguments as transcript files
+ if callopts.test:
+ self._transcript_files = callargs
+ # If commands were supplied at invocation, then add them to the command queue
+ elif callargs:
+ self._startup_commands.extend(callargs)
+ elif transcript_files:
+ self._transcript_files = transcript_files
# The default key for sorting tab completion matches. This only applies when the matches are not
# already marked as sorted by setting self.matches_sorted to True. Its default value performs a
@@ -1166,7 +1184,7 @@ class Cmd(cmd.Cmd):
# Find every executable file in the user's path that matches the pattern
for path in paths:
full_path = os.path.join(path, starts_with)
- matches = [f for f in glob.glob(full_path + '*') if os.path.isfile(f) and os.access(f, os.X_OK)]
+ matches = utils.files_from_glob_pattern(full_path + '*', access=os.X_OK)
for match in matches:
exes_set.add(os.path.basename(match))
@@ -1689,7 +1707,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
@@ -1810,47 +1828,25 @@ class Cmd(cmd.Cmd):
except Exception as ex:
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.
+ def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]]) -> bool:
+ """
+ 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.
- Example: cmd_obj.runcmds_plus_hooks(['load myscript.txt'])
+ :param cmds: commands to run
+ :return: True if running of commands should stop
+ """
+ for line in cmds:
+ if isinstance(line, HistoryItem):
+ line = line.raw
- :param cmds: command strings suitable for onecmd_plus_hooks.
- :return: True implies the entire application should exit.
+ if self.echo:
+ self.poutput('{}{}'.format(self.prompt, line))
- """
- 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))
+ if self.onecmd_plus_hooks(line):
+ return True
- 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
+ return False
def _complete_statement(self, line: str) -> Statement:
"""Keep accepting lines of input until the command is complete.
@@ -2234,14 +2230,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,25 +2265,21 @@ class Cmd(cmd.Cmd):
# Enable tab completion
readline.parse_and_bind(self.completekey + ": complete")
- stop = False
try:
- 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)
+ # Run startup commands
+ stop = self.runcmds_plus_hooks(self._startup_commands)
+ self._startup_commands.clear()
- if self.echo and line != 'eos':
- self.poutput('{}{}'.format(self.prompt, line))
- else:
- # Otherwise, read a command from stdin
- try:
- line = self.pseudo_raw_input(self.prompt)
- except KeyboardInterrupt as ex:
- if self.quit_on_sigint:
- raise ex
- else:
- self.poutput('^C')
- line = ''
+ while not stop:
+ # Get commands from user
+ try:
+ line = self.pseudo_raw_input(self.prompt)
+ except KeyboardInterrupt as ex:
+ if self.quit_on_sigint:
+ raise ex
+ else:
+ self.poutput('^C')
+ line = ''
# Run the command along with all associated pre and post hooks
stop = self.onecmd_plus_hooks(line)
@@ -2307,11 +2297,6 @@ class Cmd(cmd.Cmd):
# noinspection PyUnresolvedReferences
readline.rl.mode._display_completions = orig_pyreadline_display
- self.cmdqueue.clear()
- self._script_dir.clear()
-
- return stop
-
# ----- Alias sub-command functions -----
def alias_create(self, args: argparse.Namespace) -> None:
@@ -2863,14 +2848,14 @@ 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:
@@ -2925,10 +2910,8 @@ class Cmd(cmd.Cmd):
"""
read_only_settings = """
Commands may be terminated with: {}
- Arguments at invocation allowed: {}
Output redirection and pipes allowed: {}"""
- return read_only_settings.format(str(self.statement_parser.terminators), self.allow_cli_args,
- self.allow_redirection)
+ return read_only_settings.format(str(self.statement_parser.terminators), self.allow_redirection)
def show(self, args: argparse.Namespace, parameter: str = '') -> None:
"""Shows current settings of parameters.
@@ -3085,6 +3068,8 @@ class Cmd(cmd.Cmd):
self.perror(err, traceback_war=False)
return False
+ bridge = PyscriptBridge(self)
+
try:
self._in_py = True
@@ -3110,7 +3095,6 @@ class Cmd(cmd.Cmd):
raise EmbeddedConsoleExit
# Set up Python environment
- bridge = PyscriptBridge(self)
self.pystate[self.pyscript_name] = bridge
self.pystate['run'] = py_run
self.pystate['quit'] = py_quit
@@ -3255,7 +3239,7 @@ class Cmd(cmd.Cmd):
finally:
self._in_py = False
- return self._should_quit
+ return bridge.stop
pyscript_parser = ACArgumentParser()
setattr(pyscript_parser.add_argument('script_path', help='path to the script file'),
@@ -3313,7 +3297,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 +3335,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 +3401,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 +3413,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:
@@ -3525,8 +3509,9 @@ class Cmd(cmd.Cmd):
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."""
- import io
+ """
+ Generate a transcript file from a given history of commands
+ """
# 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))
transcript_dir = os.path.dirname(transcript_path)
@@ -3535,6 +3520,7 @@ class Cmd(cmd.Cmd):
traceback_war=False)
return
+ commands_run = 0
try:
with self.sigint_protection:
# Disable echo while we manually redirect stdout to a StringIO buffer
@@ -3566,24 +3552,31 @@ class Cmd(cmd.Cmd):
else:
command += '{}{}\n'.format(self.continuation_prompt, line)
transcript += command
- # create a new string buffer and set it to stdout to catch the output
- # of the command
- membuf = io.StringIO()
- self.stdout = membuf
+
+ # Use a StdSim object to capture output
+ self.stdout = utils.StdSim(self.stdout)
+
# then run the command and let the output go into our buffer
- self.onecmd_plus_hooks(history_item)
- # 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'\/')
+ stop = self.onecmd_plus_hooks(history_item)
+ commands_run += 1
+
+ # add the regex-escaped output to the transcript
+ transcript += self.stdout.getvalue().replace('/', r'\/')
+
+ # check if we are supposed to stop
+ if stop:
+ break
finally:
with self.sigint_protection:
# Restore altered attributes to their original state
self.echo = saved_echo
self.stdout = saved_stdout
+ # Check if all commands ran
+ if commands_run < len(history):
+ warning = "Command {} triggered a stop and ended transcript generation early".format(commands_run)
+ self.perror(warning, err_color=Fore.LIGHTYELLOW_EX, traceback_war=False)
+
# finally, we can write the transcript out to the file
try:
with open(transcript_file, 'w') as fout:
@@ -3592,12 +3585,12 @@ 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))
edit_description = ("Edit a file in a text editor\n"
"\n"
@@ -3629,22 +3622,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 +3638,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 +3666,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:
+ 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,20 +3701,23 @@ 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:
+ def run_transcript_tests(self, transcript_paths: List[str]) -> None:
"""Runs transcript tests for provided file(s).
This is called when either -t is provided on the command line or the transcript_files argument is provided
during construction of the cmd2.Cmd instance.
- :param callargs: list of transcript test file names
+ :param transcript_paths: list of transcript test file paths
"""
import unittest
from .transcript import Cmd2TestCase
@@ -3730,7 +3725,14 @@ class Cmd(cmd.Cmd):
class TestMyAppCase(Cmd2TestCase):
cmdapp = self
- self.__class__.testfiles = callargs
+ # Validate that there is at least one transcript file
+ transcripts_expanded = utils.files_from_glob_patterns(transcript_paths, access=os.R_OK)
+ if not transcripts_expanded:
+ self.perror('No test files found - nothing to test', traceback_war=False)
+ self.exit_code = -1
+ return
+
+ self.__class__.testfiles = transcripts_expanded
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
testcase = TestMyAppCase()
stream = utils.StdSim(sys.stderr)
@@ -3740,7 +3742,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 ')
@@ -4003,7 +4005,6 @@ class Cmd(cmd.Cmd):
_cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with
the following extra features provided by cmd2:
- - commands at invocation
- transcript testing
- intro banner
- exit code
@@ -4020,20 +4021,6 @@ class Cmd(cmd.Cmd):
original_sigint_handler = signal.getsignal(signal.SIGINT)
signal.signal(signal.SIGINT, self.sigint_handler)
- if self.allow_cli_args:
- parser = argparse.ArgumentParser()
- parser.add_argument('-t', '--test', action="store_true",
- help='Test against transcript(s) in FILE (wildcards OK)')
- callopts, callargs = parser.parse_known_args()
-
- # If transcript testing was called for, use other arguments as transcript files
- if callopts.test:
- self._transcript_files = callargs
-
- # If commands were supplied at invocation, then add them to the command queue
- if callargs:
- self.cmdqueue.extend(callargs)
-
# Grab terminal lock before the prompt has been drawn by readline
self.terminal_lock.acquire()
diff --git a/cmd2/constants.py b/cmd2/constants.py
index 854862c5..dede0381 100644
--- a/cmd2/constants.py
+++ b/cmd2/constants.py
@@ -20,7 +20,7 @@ ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m')
LINE_FEED = '\n'
-# values for colors setting
+# Values for colors setting
COLORS_NEVER = 'Never'
COLORS_TERMINAL = 'Terminal'
COLORS_ALWAYS = 'Always'
diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py
index 1c720cf9..e5633173 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/pyscript_bridge.py
@@ -2,9 +2,6 @@
"""
Bridges calls made inside of pyscript with the Cmd2 host app while maintaining a reasonable
degree of isolation between the two
-
-Copyright 2018 Eric Lin <anselor@gmail.com>
-Released under MIT license, see LICENSE file
"""
import sys
@@ -20,17 +17,18 @@ else:
from contextlib import redirect_stdout, redirect_stderr
-class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr', 'data'])):
- """Encapsulates the results from a command.
+class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr', 'stop', 'data'])):
+ """Encapsulates the results from a cmd2 app command
Named tuple attributes
----------------------
- stdout: str - Output captured from stdout while this command is executing
- stderr: str - Output captured from stderr while this command is executing. None if no error captured.
- data - Data returned by the command.
+ stdout: str - output captured from stdout while this command is executing
+ stderr: str - output captured from stderr while this command is executing. None if no error captured.
+ stop: bool - return value of onecmd_plus_hooks after it runs the given command line.
+ data - possible data populated by the command.
Any combination of these fields can be used when developing a scripting API for a given command.
- By default stdout and stderr will be captured for you. If there is additional command specific data,
+ By default stdout, stderr, and stop will be captured for you. If there is additional command specific data,
then write that to cmd2's _last_result member. That becomes the data member of this tuple.
In some cases, the data member may contain everything needed for a command and storing stdout
@@ -67,6 +65,9 @@ class PyscriptBridge(object):
self._cmd2_app = cmd2_app
self.cmd_echo = False
+ # Tells if any of the commands run via __call__ returned True for stop
+ self.stop = False
+
def __dir__(self):
"""Return a custom set of attribute names"""
attributes = []
@@ -95,16 +96,20 @@ class PyscriptBridge(object):
self._cmd2_app._last_result = None
+ stop = False
try:
self._cmd2_app.stdout = copy_cmd_stdout
with redirect_stdout(copy_cmd_stdout):
with redirect_stderr(copy_stderr):
- self._cmd2_app.onecmd_plus_hooks(command, pyscript_bridge_call=True)
+ stop = self._cmd2_app.onecmd_plus_hooks(command, pyscript_bridge_call=True)
finally:
- self._cmd2_app.stdout = copy_cmd_stdout.inner_stream
+ with self._cmd2_app.sigint_protection:
+ self._cmd2_app.stdout = copy_cmd_stdout.inner_stream
+ self.stop = stop or self.stop
# Save the output. If stderr is empty, set it to None.
result = CommandResult(stdout=copy_cmd_stdout.getvalue(),
stderr=copy_stderr.getvalue() if copy_stderr.getvalue() else None,
+ stop=stop,
data=self._cmd2_app._last_result)
return result
diff --git a/cmd2/transcript.py b/cmd2/transcript.py
index a635c1d3..5a115496 100644
--- a/cmd2/transcript.py
+++ b/cmd2/transcript.py
@@ -10,7 +10,6 @@ This file contains the classess necessary to make that work. These
classes are used in cmd2.py::run_transcript_tests()
"""
import re
-import glob
import unittest
from typing import Tuple
@@ -30,13 +29,10 @@ class Cmd2TestCase(unittest.TestCase):
def fetchTranscripts(self):
self.transcripts = {}
- for fileset in self.cmdapp.testfiles:
- for fname in glob.glob(fileset):
- tfile = open(fname)
- self.transcripts[fname] = iter(tfile.readlines())
- tfile.close()
- if not len(self.transcripts):
- raise Exception("No test files found - nothing to test.")
+ for fname in self.cmdapp.testfiles:
+ tfile = open(fname)
+ self.transcripts[fname] = iter(tfile.readlines())
+ tfile.close()
def setUp(self):
if self.cmdapp:
@@ -84,14 +80,16 @@ class Cmd2TestCase(unittest.TestCase):
line_num += 1
command = ''.join(command)
# Send the command into the application and capture the resulting output
- # TODO: Should we get the return value and act if stop == True?
- self.cmdapp.onecmd_plus_hooks(command)
+ stop = self.cmdapp.onecmd_plus_hooks(command)
result = self.cmdapp.stdout.read()
+ stop_msg = 'Command indicated application should quit, but more commands in transcript'
# Read the expected result from transcript
if utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt):
message = '\nFile {}, line {}\nCommand was:\n{}\nExpected: (nothing)\nGot:\n{}\n'.format(
fname, line_num, command, result)
self.assertTrue(not (result.strip()), message)
+ # If the command signaled the application to quit there should be no more commands
+ self.assertFalse(stop, stop_msg)
continue
expected = []
while not utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt):
@@ -102,9 +100,13 @@ class Cmd2TestCase(unittest.TestCase):
finished = True
break
line_num += 1
- expected = ''.join(expected)
+
+ if stop:
+ # This should only be hit if the command that set stop to True had output text
+ self.assertTrue(finished, stop_msg)
# transform the expected text into a valid regular expression
+ expected = ''.join(expected)
expected = self._transform_transcript_expected(expected)
message = '\nFile {}, line {}\nCommand was:\n{}\nExpected:\n{}\nGot:\n{}\n'.format(
fname, line_num, command, expected, result)
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 54ad763d..3500ba7a 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -2,6 +2,7 @@
"""Shared utility functions"""
import collections
+import glob
import os
import re
import subprocess
@@ -319,6 +320,34 @@ def find_editor() -> str:
return editor
+def files_from_glob_pattern(pattern: str, access=os.F_OK) -> List[str]:
+ """Return a list of file paths based on a glob pattern.
+
+ Only files are returned, not directories, and optionally only files for which the user has a specified access to.
+
+ :param pattern: file name or glob pattern
+ :param access: file access type to verify (os.* where * is F_OK, R_OK, W_OK, or X_OK)
+ :return: list of files matching the name or glob pattern
+ """
+ return [f for f in glob.glob(pattern) if os.path.isfile(f) and os.access(f, access)]
+
+
+def files_from_glob_patterns(patterns: List[str], access=os.F_OK) -> List[str]:
+ """Return a list of file paths based on a list of glob patterns.
+
+ Only files are returned, not directories, and optionally only files for which the user has a specified access to.
+
+ :param patterns: list of file names and/or glob patterns
+ :param access: file access type to verify (os.* where * is F_OK, R_OK, W_OK, or X_OK)
+ :return: list of files matching the names and/or glob patterns
+ """
+ files = []
+ for pattern in patterns:
+ matches = files_from_glob_pattern(pattern, access=access)
+ files.extend(matches)
+ return files
+
+
class StdSim(object):
"""
Class to simulate behavior of sys.stdout or sys.stderr.
diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst
index 05b5391d..12421601 100644
--- a/docs/freefeatures.rst
+++ b/docs/freefeatures.rst
@@ -81,13 +81,13 @@ quotation marks if it is more than a one-word command.
.. note::
If you wish to disable cmd2's consumption of command-line arguments, you can do so by setting the ``allow_cli_args``
- attribute of your ``cmd2.Cmd`` class instance to ``False``. This would be useful, for example, if you wish to use
+ argument of your ``cmd2.Cmd`` class instance to ``False``. This would be useful, for example, if you wish to use
something like Argparse_ to parse the overall command line arguments for your application::
from cmd2 import Cmd
class App(Cmd):
def __init__(self):
- self.allow_cli_args = False
+ super().__init__(allow_cli_args=False)
.. _Argparse: https://docs.python.org/3/library/argparse.html
diff --git a/docs/hooks.rst b/docs/hooks.rst
index 35753e59..5db97fe5 100644
--- a/docs/hooks.rst
+++ b/docs/hooks.rst
@@ -54,7 +54,7 @@ value is ignored.
Application Lifecycle Attributes
--------------------------------
-There are numerous attributes (member variables of the ``cmd2.Cmd``) which have
+There are numerous attributes of and arguments to ``cmd2.Cmd`` which have
a significant effect on the application behavior upon entering or during the
main loop. A partial list of some of the more important ones is presented here:
diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py
index 9eb0befb..538feac1 100755
--- a/examples/cmd_as_argument.py
+++ b/examples/cmd_as_argument.py
@@ -31,9 +31,8 @@ class CmdLineApp(cmd2.Cmd):
shortcuts = dict(self.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
- super().__init__(use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts)
+ super().__init__(allow_cli_args=False, use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts)
- self.allow_cli_args = False
self.maxrepeats = 3
# Make maxrepeats settable at runtime
self.settable['maxrepeats'] = 'max repetitions for speak command'
diff --git a/examples/decorator_example.py b/examples/decorator_example.py
index e268c615..bb0d58c0 100755
--- a/examples/decorator_example.py
+++ b/examples/decorator_example.py
@@ -29,9 +29,6 @@ class CmdLineApp(cmd2.Cmd):
# Make maxrepeats settable at runtime
self.settable['maxrepeats'] = 'Max number of `--repeat`s allowed'
- # Disable cmd's usage of command-line arguments as commands to be run at invocation
- # self.allow_cli_args = False
-
# Example of args set from the command-line (but they aren't being used here)
self._ip = ip_addr
self._port = port
diff --git a/examples/persistent_history.py b/examples/persistent_history.py
index 12d8b813..e88fd5d9 100755
--- a/examples/persistent_history.py
+++ b/examples/persistent_history.py
@@ -15,8 +15,7 @@ class Cmd2PersistentHistory(cmd2.Cmd):
:param hist_file: file to load readline history from at start and write it to at end
"""
- super().__init__(persistent_history_file=hist_file, persistent_history_length=500)
- self.allow_cli_args = False
+ super().__init__(persistent_history_file=hist_file, persistent_history_length=500, allow_cli_args=False)
self.prompt = 'ph> '
# ... your class code here ...
diff --git a/examples/scripts/quit.txt b/examples/scripts/quit.txt
new file mode 100644
index 00000000..ff604669
--- /dev/null
+++ b/examples/scripts/quit.txt
@@ -0,0 +1 @@
+quit
diff --git a/examples/tab_autocomp_dynamic.py b/examples/tab_autocomp_dynamic.py
index 03e46f8a..b518c013 100755
--- a/examples/tab_autocomp_dynamic.py
+++ b/examples/tab_autocomp_dynamic.py
@@ -2,9 +2,6 @@
# coding=utf-8
"""
A example usage of AutoCompleter with delayed initialization of the argparse object
-
-Copyright 2018 Eric Lin <anselor@gmail.com>
-Released under MIT license, see LICENSE file
"""
from typing import List
diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py
index 6883c423..8f27cb90 100755
--- a/examples/tab_autocompletion.py
+++ b/examples/tab_autocompletion.py
@@ -2,9 +2,6 @@
# coding=utf-8
"""
A example usage of the AutoCompleter
-
-Copyright 2018 Eric Lin <anselor@gmail.com>
-Released under MIT license, see LICENSE file
"""
import argparse
import itertools
diff --git a/examples/transcripts/quit.txt b/examples/transcripts/quit.txt
new file mode 100644
index 00000000..6dcf8c66
--- /dev/null
+++ b/examples/transcripts/quit.txt
@@ -0,0 +1 @@
+(Cmd) quit
diff --git a/tests/conftest.py b/tests/conftest.py
index dc5c1ab1..517e2865 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,9 +1,6 @@
# coding=utf-8
"""
Cmd2 unit/functional testing
-
-Copyright 2016 Federico Ceratto <federico.ceratto@gmail.com>
-Released under MIT license, see LICENSE file
"""
import sys
from typing import Optional
diff --git a/tests/pyscript/stop.py b/tests/pyscript/stop.py
new file mode 100644
index 00000000..e731218e
--- /dev/null
+++ b/tests/pyscript/stop.py
@@ -0,0 +1,9 @@
+# flake8: noqa F821
+app.cmd_echo = True
+app('help')
+
+# This will set stop to True in the PyscriptBridge
+app('quit')
+
+# Exercise py_quit() in unit test
+quit()
diff --git a/tests/test_acargparse.py b/tests/test_acargparse.py
index 64612737..436158db 100644
--- a/tests/test_acargparse.py
+++ b/tests/test_acargparse.py
@@ -1,9 +1,6 @@
# flake8: noqa E302
"""
Unit/functional testing for argparse customizations in cmd2
-
-Copyright 2018 Eric Lin <anselor@gmail.com>
-Released under MIT license, see LICENSE file
"""
import pytest
from cmd2.argparse_completer import ACArgumentParser, is_potential_flag
diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py
index 005eee81..4e1ceff0 100644
--- a/tests/test_autocompletion.py
+++ b/tests/test_autocompletion.py
@@ -2,9 +2,6 @@
# flake8: noqa E302
"""
Unit/functional testing for argparse completer in cmd2
-
-Copyright 2018 Eric Lin <anselor@gmail.com>
-Released under MIT license, see LICENSE file
"""
import pytest
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 1aafefc2..0dc8c7c2 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -2,9 +2,6 @@
# flake8: noqa E302
"""
Cmd2 unit/functional testing
-
-Copyright 2016 Federico Ceratto <federico.ceratto@gmail.com>
-Released under MIT license, see LICENSE file
"""
import argparse
import builtins
@@ -28,13 +25,15 @@ from cmd2 import clipboard, constants, utils
from .conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \
HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG
-
-@pytest.fixture
-def outsim_app():
+def CreateOutsimApp():
c = cmd2.Cmd()
c.stdout = utils.StdSim(c.stdout)
return c
+@pytest.fixture
+def outsim_app():
+ return CreateOutsimApp()
+
def test_version(base_app):
assert cmd2.__version__
@@ -112,9 +111,8 @@ def test_base_show_readonly(base_app):
out, err = run_cmd(base_app, 'set -a')
expected = normalize(SHOW_TXT + '\nRead only settings:' + """
Commands may be terminated with: {}
- Arguments at invocation allowed: {}
Output redirection and pipes allowed: {}
-""".format(base_app.statement_parser.terminators, base_app.allow_cli_args, base_app.allow_redirection))
+""".format(base_app.statement_parser.terminators, base_app.allow_redirection))
assert out == expected
@@ -297,17 +295,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._script_dir == []
+ assert base_app._current_script_dir is None
- 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
+ # 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 +324,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 +332,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 +340,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 +350,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 +361,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 +415,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 +434,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 +509,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))
@@ -717,60 +735,66 @@ def test_base_py_interactive(base_app):
m.assert_called_once()
-def test_base_cmdloop_with_queue(outsim_app):
- # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
- outsim_app.use_rawinput = True
+def test_base_cmdloop_with_startup_commands():
intro = 'Hello World, this is an intro ...'
- outsim_app.cmdqueue.append('quit\n')
# Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
- testargs = ["prog"]
+ testargs = ["prog", 'quit']
expected = intro + '\n'
+
with mock.patch.object(sys, 'argv', testargs):
- # Run the command loop with custom intro
- outsim_app.cmdloop(intro=intro)
- out = outsim_app.stdout.getvalue()
+ app = CreateOutsimApp()
+
+ app.use_rawinput = True
+
+ # Run the command loop with custom intro
+ app.cmdloop(intro=intro)
+
+ out = app.stdout.getvalue()
assert out == expected
-def test_base_cmdloop_without_queue(outsim_app):
- # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
- outsim_app.use_rawinput = True
- outsim_app.intro = 'Hello World, this is an intro ...'
+def test_base_cmdloop_without_startup_commands():
+ # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
+ testargs = ["prog"]
+ with mock.patch.object(sys, 'argv', testargs):
+ app = CreateOutsimApp()
+
+ app.use_rawinput = True
+ app.intro = 'Hello World, this is an intro ...'
# Mock out the input call so we don't actually wait for a user's response on stdin
m = mock.MagicMock(name='input', return_value='quit')
builtins.input = m
+ expected = app.intro + '\n'
+
+ # Run the command loop
+ app.cmdloop()
+ out = app.stdout.getvalue()
+ assert out == expected
+
+
+def test_cmdloop_without_rawinput():
# Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
testargs = ["prog"]
- expected = outsim_app.intro + '\n'
with mock.patch.object(sys, 'argv', testargs):
- # Run the command loop
- outsim_app.cmdloop()
- out = outsim_app.stdout.getvalue()
- assert out == expected
+ app = CreateOutsimApp()
-
-def test_cmdloop_without_rawinput(outsim_app):
- # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
- outsim_app.use_rawinput = False
- outsim_app.echo = False
- outsim_app.intro = 'Hello World, this is an intro ...'
+ app.use_rawinput = False
+ app.echo = False
+ app.intro = 'Hello World, this is an intro ...'
# Mock out the input call so we don't actually wait for a user's response on stdin
m = mock.MagicMock(name='input', return_value='quit')
builtins.input = m
- # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
- testargs = ["prog"]
- expected = outsim_app.intro + '\n'
- with mock.patch.object(sys, 'argv', testargs):
- # Run the command loop
- outsim_app.cmdloop()
- out = outsim_app.stdout.getvalue()
- assert out == expected
+ expected = app.intro + '\n'
+ with pytest.raises(OSError):
+ app.cmdloop()
+ out = app.stdout.getvalue()
+ assert out == expected
class HookFailureApp(cmd2.Cmd):
def __init__(self, *args, **kwargs):
@@ -807,8 +831,7 @@ class SayApp(cmd2.Cmd):
@pytest.fixture
def say_app():
- app = SayApp()
- app.allow_cli_args = False
+ app = SayApp(allow_cli_args=False)
app.stdout = utils.StdSim(app.stdout)
return app
@@ -820,7 +843,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 +860,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 +1387,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')
-
- assert app._current_script_dir is not None
+ commands = ['help history']
- # 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
@@ -1395,7 +1403,7 @@ def test_pseudo_raw_input_tty_rawinput_true():
with mock.patch('sys.stdin.isatty', mock.MagicMock(name='isatty', return_value=True)):
with mock.patch('builtins.input', mock.MagicMock(name='input', side_effect=['set', EOFError])) as m_input:
# run the cmdloop, which should pull input from our mocks
- app = cmd2.Cmd()
+ app = cmd2.Cmd(allow_cli_args=False)
app.use_rawinput = True
app._cmdloop()
# because we mocked the input() call, we won't get the prompt
@@ -1414,7 +1422,7 @@ def test_pseudo_raw_input_tty_rawinput_false():
fakein.readline = mreadline
# run the cmdloop, telling it where to get input from
- app = cmd2.Cmd(stdin=fakein)
+ app = cmd2.Cmd(stdin=fakein, allow_cli_args=False)
app.use_rawinput = False
app._cmdloop()
@@ -1428,7 +1436,7 @@ def test_pseudo_raw_input_tty_rawinput_false():
# the next helper function and two tests check for piped
# input when use_rawinput is True.
def piped_rawinput_true(capsys, echo, command):
- app = cmd2.Cmd()
+ app = cmd2.Cmd(allow_cli_args=False)
app.use_rawinput = True
app.echo = echo
# run the cmdloop, which should pull input from our mock
@@ -1458,8 +1466,7 @@ def test_pseudo_raw_input_piped_rawinput_true_echo_false(capsys):
# input when use_rawinput=False
def piped_rawinput_false(capsys, echo, command):
fakein = io.StringIO(u'{}'.format(command))
- # run the cmdloop, telling it where to get input from
- app = cmd2.Cmd(stdin=fakein)
+ app = cmd2.Cmd(stdin=fakein, allow_cli_args=False)
app.use_rawinput = False
app.echo = echo
app._cmdloop()
@@ -1913,7 +1920,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
@@ -1927,7 +1934,7 @@ class ReplWithExitCode(cmd2.Cmd):
""" Example cmd2 application where we can specify an exit code when existing."""
def __init__(self):
- super().__init__()
+ super().__init__(allow_cli_args=False)
@cmd2.with_argument_list
def do_exit(self, arg_list) -> bool:
@@ -1945,8 +1952,8 @@ Usage: exit [exit_code]
self.perror("{} isn't a valid integer exit code".format(arg_list[0]))
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."""
@@ -1959,7 +1966,6 @@ def exit_code_repl():
return app
def test_exit_code_default(exit_code_repl):
- # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
app = exit_code_repl
app.use_rawinput = True
@@ -1967,17 +1973,14 @@ def test_exit_code_default(exit_code_repl):
m = mock.MagicMock(name='input', return_value='exit')
builtins.input = m
- # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
- testargs = ["prog"]
expected = 'exiting with code: 0\n'
- with mock.patch.object(sys, 'argv', testargs):
- # Run the command loop
- app.cmdloop()
+
+ # Run the command loop
+ app.cmdloop()
out = app.stdout.getvalue()
assert out == expected
def test_exit_code_nonzero(exit_code_repl):
- # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
app = exit_code_repl
app.use_rawinput = True
@@ -1985,12 +1988,10 @@ def test_exit_code_nonzero(exit_code_repl):
m = mock.MagicMock(name='input', return_value='exit 23')
builtins.input = m
- # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
- testargs = ["prog"]
expected = 'exiting with code: 23\n'
- with mock.patch.object(sys, 'argv', testargs):
- # Run the command loop
- app.cmdloop()
+
+ # Run the command loop
+ app.cmdloop()
out = app.stdout.getvalue()
assert out == expected
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 6fd45ff9..0a16bc28 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -5,9 +5,6 @@ Unit/functional testing for readline tab-completion functions in the cmd2.py mod
These are primarily tests related to readline completer functions which handle tab-completion of cmd2/cmd commands,
file system paths, and shell commands.
-
-Copyright 2017 Todd Leonhardt <todd.leonhardt@gmail.com>
-Released under MIT license, see LICENSE file
"""
import argparse
import os
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index de8d67af..5ba02a95 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -2,9 +2,6 @@
# flake8: noqa E302
"""
Test the parsing logic in parsing.py
-
-Copyright 2017 Todd Leonhardt <todd.leonhardt@gmail.com>
-Released under MIT license, see LICENSE file
"""
import attr
import pytest
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 242b0d25..f7065db5 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -2,12 +2,17 @@
# flake8: noqa E302
"""
Test plugin infrastructure and hooks.
-
-Copyright 2018 Jared Crapo <jared@kotfu.net>
-Released under MIT license, see LICENSE file
"""
+import sys
+
import pytest
+# Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available
+try:
+ import mock
+except ImportError:
+ from unittest import mock
+
import cmd2
from cmd2 import plugin
@@ -265,21 +270,27 @@ def test_register_preloop_hook_with_return_annotation():
app.register_preloop_hook(app.prepost_hook_with_wrong_return_annotation)
def test_preloop_hook(capsys):
- app = PluggedApp()
+ # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
+ testargs = ["prog", "say hello", 'quit']
+
+ with mock.patch.object(sys, 'argv', testargs):
+ app = PluggedApp()
+
app.register_preloop_hook(app.prepost_hook_one)
- app.cmdqueue.append('say hello')
- app.cmdqueue.append('quit')
app.cmdloop()
out, err = capsys.readouterr()
assert out == 'one\nhello\n'
assert not err
def test_preloop_hooks(capsys):
- app = PluggedApp()
+ # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
+ testargs = ["prog", "say hello", 'quit']
+
+ with mock.patch.object(sys, 'argv', testargs):
+ app = PluggedApp()
+
app.register_preloop_hook(app.prepost_hook_one)
app.register_preloop_hook(app.prepost_hook_two)
- app.cmdqueue.append('say hello')
- app.cmdqueue.append('quit')
app.cmdloop()
out, err = capsys.readouterr()
assert out == 'one\ntwo\nhello\n'
@@ -296,21 +307,27 @@ def test_register_postloop_hook_with_wrong_return_annotation():
app.register_postloop_hook(app.prepost_hook_with_wrong_return_annotation)
def test_postloop_hook(capsys):
- app = PluggedApp()
+ # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
+ testargs = ["prog", "say hello", 'quit']
+
+ with mock.patch.object(sys, 'argv', testargs):
+ app = PluggedApp()
+
app.register_postloop_hook(app.prepost_hook_one)
- app.cmdqueue.append('say hello')
- app.cmdqueue.append('quit')
app.cmdloop()
out, err = capsys.readouterr()
assert out == 'hello\none\n'
assert not err
def test_postloop_hooks(capsys):
- app = PluggedApp()
+ # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
+ testargs = ["prog", "say hello", 'quit']
+
+ with mock.patch.object(sys, 'argv', testargs):
+ app = PluggedApp()
+
app.register_postloop_hook(app.prepost_hook_one)
app.register_postloop_hook(app.prepost_hook_two)
- app.cmdqueue.append('say hello')
- app.cmdqueue.append('quit')
app.cmdloop()
out, err = capsys.readouterr()
assert out == 'hello\none\ntwo\n'
diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py
index 4866548b..8da4b35a 100644
--- a/tests/test_pyscript.py
+++ b/tests/test_pyscript.py
@@ -40,3 +40,17 @@ def test_pyscript_stdout_capture(base_app, request):
assert out[0] == "PASSED"
assert out[1] == "PASSED"
+
+def test_pyscript_stop(base_app, request):
+ # Verify onecmd_plus_hooks() returns True if any commands in a pyscript return True for stop
+ test_dir = os.path.dirname(request.module.__file__)
+
+ # help.py doesn't run any commands that returns True for stop
+ python_script = os.path.join(test_dir, 'pyscript', 'help.py')
+ stop = base_app.onecmd_plus_hooks('pyscript {}'.format(python_script))
+ assert not stop
+
+ # stop.py runs the quit command which does return True for stop
+ python_script = os.path.join(test_dir, 'pyscript', 'stop.py')
+ stop = base_app.onecmd_plus_hooks('pyscript {}'.format(python_script))
+ assert stop
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 70c9119c..5dd39e1b 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -2,9 +2,6 @@
# flake8: noqa E302
"""
Cmd2 functional testing based on transcript
-
-Copyright 2016 Federico Ceratto <federico.ceratto@gmail.com>
-Released under MIT license, see LICENSE file
"""
import argparse
import os
@@ -90,10 +87,11 @@ def test_commands_at_invocation():
expected = "This is an intro banner ...\nhello\nGracie\n"
with mock.patch.object(sys, 'argv', testargs):
app = CmdLineApp()
- app.stdout = StdSim(app.stdout)
- app.cmdloop()
- out = app.stdout.getvalue()
- assert out == expected
+
+ app.stdout = StdSim(app.stdout)
+ app.cmdloop()
+ out = app.stdout.getvalue()
+ assert out == expected
@pytest.mark.parametrize('filename,feedback_to_output', [
('bol_eol.txt', False),
@@ -113,11 +111,6 @@ def test_commands_at_invocation():
('word_boundaries.txt', False),
])
def test_transcript(request, capsys, filename, feedback_to_output):
- # Create a cmd2.Cmd() instance and make sure basic settings are
- # like we want for test
- app = CmdLineApp()
- app.feedback_to_output = feedback_to_output
-
# Get location of the transcript
test_dir = os.path.dirname(request.module.__file__)
transcript_file = os.path.join(test_dir, 'transcripts', filename)
@@ -126,9 +119,15 @@ def test_transcript(request, capsys, filename, feedback_to_output):
# arguments equal to the py.test args
testargs = ['prog', '-t', transcript_file]
with mock.patch.object(sys, 'argv', testargs):
- # Run the command loop
- sys_exit_code = app.cmdloop()
- assert sys_exit_code == 0
+ # Create a cmd2.Cmd() instance and make sure basic settings are
+ # like we want for test
+ app = CmdLineApp()
+
+ app.feedback_to_output = feedback_to_output
+
+ # Run the command loop
+ sys_exit_code = app.cmdloop()
+ assert sys_exit_code == 0
# Check for the unittest "OK" condition for the 1 test which ran
expected_start = ".\n----------------------------------------------------------------------\nRan 1 test in"
@@ -195,7 +194,6 @@ def test_load_record_transcript(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
filename = os.path.join(test_dir, 'scripts', 'help.txt')
- assert base_app.cmdqueue == []
assert base_app._script_dir == []
assert base_app._current_script_dir is None
@@ -206,7 +204,6 @@ def test_load_record_transcript(base_app, request):
# Run the load command with the -r option to generate a transcript
run_cmd(base_app, 'load {} -t {}'.format(filename, transcript_fname))
- assert base_app.cmdqueue == []
assert base_app._script_dir == []
assert base_app._current_script_dir is None
@@ -219,6 +216,27 @@ def test_load_record_transcript(base_app, request):
assert xscript == expected
+def test_generate_transcript_stop(capsys):
+ # Verify transcript generation stops when a command returns True for stop
+ app = CmdLineApp()
+
+ # Make a tmp file to use as a transcript
+ fd, transcript_fname = tempfile.mkstemp(prefix='', suffix='.trn')
+ os.close(fd)
+
+ # This should run all commands
+ commands = ['help', 'alias']
+ app._generate_transcript(commands, transcript_fname)
+ _, err = capsys.readouterr()
+ assert err.startswith("2 commands")
+
+ # Since quit returns True for stop, only the first 2 commands will run
+ commands = ['help', 'quit', 'alias']
+ app._generate_transcript(commands, transcript_fname)
+ _, err = capsys.readouterr()
+ assert err.startswith("Command 2 triggered a stop")
+
+
@pytest.mark.parametrize('expected, transformed', [
# strings with zero or one slash or with escaped slashes means no regular
# expression present, so the result should just be what re.escape returns.
@@ -251,11 +269,6 @@ def test_parse_transcript_expected(expected, transformed):
def test_transcript_failure(request, capsys):
- # Create a cmd2.Cmd() instance and make sure basic settings are
- # like we want for test
- app = CmdLineApp()
- app.feedback_to_output = False
-
# Get location of the transcript
test_dir = os.path.dirname(request.module.__file__)
transcript_file = os.path.join(test_dir, 'transcripts', 'failure.txt')
@@ -264,13 +277,37 @@ def test_transcript_failure(request, capsys):
# arguments equal to the py.test args
testargs = ['prog', '-t', transcript_file]
with mock.patch.object(sys, 'argv', testargs):
- # Run the command loop
- sys_exit_code = app.cmdloop()
- assert sys_exit_code != 0
+ # Create a cmd2.Cmd() instance and make sure basic settings are
+ # like we want for test
+ app = CmdLineApp()
+
+ app.feedback_to_output = False
+
+ # Run the command loop
+ sys_exit_code = app.cmdloop()
+ assert sys_exit_code != 0
- # Check for the unittest "OK" condition for the 1 test which ran
expected_start = "File "
expected_end = "s\n\nFAILED (failures=1)\n\n"
_, err = capsys.readouterr()
assert err.startswith(expected_start)
assert err.endswith(expected_end)
+
+
+def test_transcript_no_file(request, capsys):
+ # Need to patch sys.argv so cmd2 doesn't think it was called with
+ # arguments equal to the py.test args
+ testargs = ['prog', '-t']
+ with mock.patch.object(sys, 'argv', testargs):
+ app = CmdLineApp()
+
+ app.feedback_to_output = False
+
+ # Run the command loop
+ sys_exit_code = app.cmdloop()
+ assert sys_exit_code != 0
+
+ # Check for the unittest "OK" condition for the 1 test which ran
+ expected = 'No test files found - nothing to test\n'
+ _, err = capsys.readouterr()
+ assert err == expected
diff --git a/tests/test_utils.py b/tests/test_utils.py
index c0b16990..b43eb10c 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -2,9 +2,6 @@
# flake8: noqa E302
"""
Unit testing for cmd2/utils.py module.
-
-Copyright 2018 Todd Leonhardt <todd.leonhardt@gmail.com>
-Released under MIT license, see LICENSE file
"""
import signal
import sys
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