diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | cmd2/cmd2.py | 43 | ||||
-rw-r--r-- | docs/freefeatures.rst | 9 | ||||
-rw-r--r-- | docs/transcript.rst | 17 | ||||
-rw-r--r-- | tests/scripts/help.txt | 1 | ||||
-rw-r--r-- | tests/test_transcript.py | 35 |
6 files changed, 88 insertions, 18 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c35a182b..da3d0751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * The `with_argparser` decorators now add the Statement object created when parsing the command line to the `argparse.Namespace` object they pass to the `do_*` methods. It is stored in an attribute called `__statement__`. This can be useful if a command function needs to know the command line for things like logging. + * Added a `-t` option to the `load` command for automatically generating a transcript based on a script file * Potentially breaking changes * The following commands now write to stderr instead of stdout when printing an error. This will make catching errors easier in pyscript. diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 33cfe3fc..0aaeb4c3 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3350,18 +3350,22 @@ class Cmd(cmd.Cmd): for hi in history: self.poutput(hi.pr(script=args.script, expanded=args.expanded, verbose=args.verbose)) - def _generate_transcript(self, history: List[HistoryItem], transcript_file: str) -> None: + def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcript_file: str) -> None: """Generate a transcript file from a given history of commands.""" - # Save the current echo state, and turn it off. We inject commands into the - # output using a different mechanism 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)) + transcript_dir = os.path.dirname(transcript_path) + if not os.path.isdir(transcript_dir) or not os.access(transcript_dir, os.W_OK): + self.perror("{!r} is not a directory or you don't have write access".format(transcript_dir), + traceback_war=False) + return + # Disable echo while we manually redirect stdout to a StringIO buffer saved_echo = self.echo + saved_stdout = self.stdout self.echo = False - # Redirect stdout to the transcript file - saved_self_stdout = self.stdout - # The problem with supporting regular expressions in transcripts # is that they shouldn't be processed in the command, just the output. # In addition, when we generate a transcript, any slashes in the output @@ -3397,10 +3401,9 @@ class Cmd(cmd.Cmd): # and add the regex-escaped output to the transcript transcript += output.replace('/', r'\/') - # Restore stdout to its original state - self.stdout = saved_self_stdout - # Set echo back to its original state + # Restore altered attributes to their original state self.echo = saved_echo + self.stdout = saved_stdout # finally, we can write the transcript out to the file try: @@ -3456,9 +3459,20 @@ class Cmd(cmd.Cmd): 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.") + "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" + ) load_parser = ACArgumentParser(description=load_description) + setattr(load_parser.add_argument('-t', '--transcript', help='record the output of the script as a transcript file'), + ACTION_ARG_CHOICES, ('path_complete',)) setattr(load_parser.add_argument('script_path', help="path to the script file"), ACTION_ARG_CHOICES, ('path_complete',)) @@ -3492,11 +3506,16 @@ class Cmd(cmd.Cmd): # command queue. Add an "end of script (eos)" command to cleanup the # self._script_dir list when done. with open(expanded_path, encoding='utf-8') as target: - self.cmdqueue = target.read().splitlines() + ['eos'] + self.cmdqueue + 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, os.path.expanduser(args.transcript)) + return + + self.cmdqueue = script_commands + ['eos'] + self.cmdqueue self._script_dir.append(os.path.dirname(expanded_path)) relative_load_description = load_description @@ -3812,7 +3831,7 @@ class Cmd(cmd.Cmd): # If transcript-based regression testing was requested, then do that instead of the main loop if self._transcript_files is not None: - self.run_transcript_tests(self._transcript_files) + self.run_transcript_tests([os.path.expanduser(tf) for tf in self._transcript_files]) else: # If an intro was supplied in the method call, allow it to override the default if intro is not None: diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst index 001a7599..11b5de68 100644 --- a/docs/freefeatures.rst +++ b/docs/freefeatures.rst @@ -19,6 +19,15 @@ Both ASCII and UTF-8 encoded unicode text files are supported. Simply include one command per line, typed exactly as you would inside a ``cmd2`` application. +The ``load`` command loads commands from a script file into a queue and then the normal cmd2 REPL +resumes control and executes the commands in the queue in FIFO order. A side effect of this +is that if you redirect/pipe the output of a load command, it will redirect the output of the ``load`` +command itself, but will NOT redirect the output of the command loaded from the script file. Of course, +you can add redirection to the commands being run in the script file, e.g.:: + + # This is your script file + command arg1 arg2 > file.txt + .. automethod:: cmd2.cmd2.Cmd.do_load .. automethod:: cmd2.cmd2.Cmd.do__relative_load diff --git a/docs/transcript.rst b/docs/transcript.rst index 36b35fcf..c7c31fd6 100644 --- a/docs/transcript.rst +++ b/docs/transcript.rst @@ -13,9 +13,9 @@ from commands that produce dynamic or variable output. Creating a transcript ===================== -Automatically -------------- -A transcript can automatically generated based upon commands previously executed in the *history*:: +Automatically from history +-------------------------- +A transcript can automatically generated based upon commands previously executed in the *history* using ``history -t``:: (Cmd) help ... @@ -32,6 +32,17 @@ This is by far the easiest way to generate a transcript. of the ``cmd2.Cmd`` class ensure that output is properly redirected when redirecting to a file, piping to a shell command, and when generating a transcript. +Automatically from a script file +-------------------------------- +A transcript can also be automatically generated from a script file using ``load -t``:: + + (Cmd) load scripts/script.txt -t transcript.txt + 2 commands and their outputs saved to transcript file 'transcript.txt' + (Cmd) + +This is a particularly attractive option for automatically regenerating transcripts for regression testing as your ``cmd2`` +application changes. + Manually -------- Here's a transcript created from ``python examples/example.py``:: diff --git a/tests/scripts/help.txt b/tests/scripts/help.txt new file mode 100644 index 00000000..71d23d97 --- /dev/null +++ b/tests/scripts/help.txt @@ -0,0 +1 @@ +help -v diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 4a03ebe0..acdbe703 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -17,7 +17,7 @@ from unittest import mock import pytest import cmd2 -from .conftest import run_cmd +from .conftest import run_cmd, BASE_HELP_VERBOSE from cmd2 import transcript from cmd2.utils import StdSim @@ -136,7 +136,7 @@ def test_transcript(request, capsys, filename, feedback_to_output): assert err.startswith(expected_start) assert err.endswith(expected_end) -def test_history_transcript(request, capsys): +def test_history_transcript(): app = CmdLineApp() app.stdout = StdSim(app.stdout) run_cmd(app, 'orate this is\na /multiline/\ncommand;\n') @@ -163,7 +163,7 @@ this is a \/multiline\/ command assert xscript == expected -def test_history_transcript_bad_filename(request, capsys): +def test_history_transcript_bad_filename(): app = CmdLineApp() app.stdout = StdSim(app.stdout) run_cmd(app, 'orate this is\na /multiline/\ncommand;\n') @@ -189,6 +189,35 @@ this is a \/multiline\/ command transcript = f.read() assert transcript == expected + +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 + + # make a tmp file to use as a transcript + fd, transcript_fname = tempfile.mkstemp(prefix='', suffix='.trn') + os.close(fd) + + # 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 + + # read in the transcript created by the history command + with open(transcript_fname) as f: + xscript = f.read() + + expected = '(Cmd) help -v\n' + BASE_HELP_VERBOSE + '\n' + + assert xscript == expected + + @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. |