Skip to content

Commit 9f5906a

Browse files
authored
Merge pull request #652 from python-cmd2/load_generate_transcript
Added load -t flag for recording a transcript based on a script file
2 parents e89bd4f + 74d2d60 commit 9f5906a

File tree

6 files changed

+88
-18
lines changed

6 files changed

+88
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* The `with_argparser` decorators now add the Statement object created when parsing the command line to the
1212
`argparse.Namespace` object they pass to the `do_*` methods. It is stored in an attribute called `__statement__`.
1313
This can be useful if a command function needs to know the command line for things like logging.
14+
* Added a `-t` option to the `load` command for automatically generating a transcript based on a script file
1415
* Potentially breaking changes
1516
* The following commands now write to stderr instead of stdout when printing an error. This will make catching
1617
errors easier in pyscript.

cmd2/cmd2.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3350,18 +3350,22 @@ def do_history(self, args: argparse.Namespace) -> None:
33503350
for hi in history:
33513351
self.poutput(hi.pr(script=args.script, expanded=args.expanded, verbose=args.verbose))
33523352

3353-
def _generate_transcript(self, history: List[HistoryItem], transcript_file: str) -> None:
3353+
def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcript_file: str) -> None:
33543354
"""Generate a transcript file from a given history of commands."""
3355-
# Save the current echo state, and turn it off. We inject commands into the
3356-
# output using a different mechanism
33573355
import io
3356+
# Validate the transcript file path to make sure directory exists and write access is available
3357+
transcript_path = os.path.abspath(os.path.expanduser(transcript_file))
3358+
transcript_dir = os.path.dirname(transcript_path)
3359+
if not os.path.isdir(transcript_dir) or not os.access(transcript_dir, os.W_OK):
3360+
self.perror("{!r} is not a directory or you don't have write access".format(transcript_dir),
3361+
traceback_war=False)
3362+
return
33583363

3364+
# Disable echo while we manually redirect stdout to a StringIO buffer
33593365
saved_echo = self.echo
3366+
saved_stdout = self.stdout
33603367
self.echo = False
33613368

3362-
# Redirect stdout to the transcript file
3363-
saved_self_stdout = self.stdout
3364-
33653369
# The problem with supporting regular expressions in transcripts
33663370
# is that they shouldn't be processed in the command, just the output.
33673371
# In addition, when we generate a transcript, any slashes in the output
@@ -3397,10 +3401,9 @@ def _generate_transcript(self, history: List[HistoryItem], transcript_file: str)
33973401
# and add the regex-escaped output to the transcript
33983402
transcript += output.replace('/', r'\/')
33993403

3400-
# Restore stdout to its original state
3401-
self.stdout = saved_self_stdout
3402-
# Set echo back to its original state
3404+
# Restore altered attributes to their original state
34033405
self.echo = saved_echo
3406+
self.stdout = saved_stdout
34043407

34053408
# finally, we can write the transcript out to the file
34063409
try:
@@ -3456,9 +3459,20 @@ def do_eos(self, _: argparse.Namespace) -> None:
34563459
load_description = ("Run commands in script file that is encoded as either ASCII or UTF-8 text\n"
34573460
"\n"
34583461
"Script should contain one command per line, just like the command would be\n"
3459-
"typed in the console.")
3462+
"typed in the console.\n"
3463+
"\n"
3464+
"It loads commands from a script file into a queue and then the normal cmd2\n"
3465+
"REPL resumes control and executes the commands in the queue in FIFO order.\n"
3466+
"If you attempt to redirect/pipe a load command, it will capture the output\n"
3467+
"of the load command itself, not what it adds to the queue.\n"
3468+
"\n"
3469+
"If the -r/--record_transcript flag is used, this command instead records\n"
3470+
"the output of the script commands to a transcript for testing purposes.\n"
3471+
)
34603472

34613473
load_parser = ACArgumentParser(description=load_description)
3474+
setattr(load_parser.add_argument('-t', '--transcript', help='record the output of the script as a transcript file'),
3475+
ACTION_ARG_CHOICES, ('path_complete',))
34623476
setattr(load_parser.add_argument('script_path', help="path to the script file"),
34633477
ACTION_ARG_CHOICES, ('path_complete',))
34643478

@@ -3492,11 +3506,16 @@ def do_load(self, args: argparse.Namespace) -> None:
34923506
# command queue. Add an "end of script (eos)" command to cleanup the
34933507
# self._script_dir list when done.
34943508
with open(expanded_path, encoding='utf-8') as target:
3495-
self.cmdqueue = target.read().splitlines() + ['eos'] + self.cmdqueue
3509+
script_commands = target.read().splitlines()
34963510
except OSError as ex: # pragma: no cover
34973511
self.perror("Problem accessing script from '{}': {}".format(expanded_path, ex))
34983512
return
34993513

3514+
if args.transcript:
3515+
self._generate_transcript(script_commands, os.path.expanduser(args.transcript))
3516+
return
3517+
3518+
self.cmdqueue = script_commands + ['eos'] + self.cmdqueue
35003519
self._script_dir.append(os.path.dirname(expanded_path))
35013520

35023521
relative_load_description = load_description
@@ -3812,7 +3831,7 @@ def cmdloop(self, intro: Optional[str] = None) -> None:
38123831

38133832
# If transcript-based regression testing was requested, then do that instead of the main loop
38143833
if self._transcript_files is not None:
3815-
self.run_transcript_tests(self._transcript_files)
3834+
self.run_transcript_tests([os.path.expanduser(tf) for tf in self._transcript_files])
38163835
else:
38173836
# If an intro was supplied in the method call, allow it to override the default
38183837
if intro is not None:

docs/freefeatures.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ Both ASCII and UTF-8 encoded unicode text files are supported.
1919

2020
Simply include one command per line, typed exactly as you would inside a ``cmd2`` application.
2121

22+
The ``load`` command loads commands from a script file into a queue and then the normal cmd2 REPL
23+
resumes control and executes the commands in the queue in FIFO order. A side effect of this
24+
is that if you redirect/pipe the output of a load command, it will redirect the output of the ``load``
25+
command itself, but will NOT redirect the output of the command loaded from the script file. Of course,
26+
you can add redirection to the commands being run in the script file, e.g.::
27+
28+
# This is your script file
29+
command arg1 arg2 > file.txt
30+
2231
.. automethod:: cmd2.cmd2.Cmd.do_load
2332

2433
.. automethod:: cmd2.cmd2.Cmd.do__relative_load

docs/transcript.rst

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ from commands that produce dynamic or variable output.
1313
Creating a transcript
1414
=====================
1515

16-
Automatically
17-
-------------
18-
A transcript can automatically generated based upon commands previously executed in the *history*::
16+
Automatically from history
17+
--------------------------
18+
A transcript can automatically generated based upon commands previously executed in the *history* using ``history -t``::
1919

2020
(Cmd) help
2121
...
@@ -32,6 +32,17 @@ This is by far the easiest way to generate a transcript.
3232
of the ``cmd2.Cmd`` class ensure that output is properly redirected when redirecting to a file, piping to a shell
3333
command, and when generating a transcript.
3434

35+
Automatically from a script file
36+
--------------------------------
37+
A transcript can also be automatically generated from a script file using ``load -t``::
38+
39+
(Cmd) load scripts/script.txt -t transcript.txt
40+
2 commands and their outputs saved to transcript file 'transcript.txt'
41+
(Cmd)
42+
43+
This is a particularly attractive option for automatically regenerating transcripts for regression testing as your ``cmd2``
44+
application changes.
45+
3546
Manually
3647
--------
3748
Here's a transcript created from ``python examples/example.py``::

tests/scripts/help.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
help -v

tests/test_transcript.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import pytest
1818

1919
import cmd2
20-
from .conftest import run_cmd
20+
from .conftest import run_cmd, BASE_HELP_VERBOSE
2121
from cmd2 import transcript
2222
from cmd2.utils import StdSim
2323

@@ -136,7 +136,7 @@ def test_transcript(request, capsys, filename, feedback_to_output):
136136
assert err.startswith(expected_start)
137137
assert err.endswith(expected_end)
138138

139-
def test_history_transcript(request, capsys):
139+
def test_history_transcript():
140140
app = CmdLineApp()
141141
app.stdout = StdSim(app.stdout)
142142
run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
@@ -163,7 +163,7 @@ def test_history_transcript(request, capsys):
163163

164164
assert xscript == expected
165165

166-
def test_history_transcript_bad_filename(request, capsys):
166+
def test_history_transcript_bad_filename():
167167
app = CmdLineApp()
168168
app.stdout = StdSim(app.stdout)
169169
run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
@@ -189,6 +189,35 @@ def test_history_transcript_bad_filename(request, capsys):
189189
transcript = f.read()
190190
assert transcript == expected
191191

192+
193+
def test_load_record_transcript(base_app, request):
194+
test_dir = os.path.dirname(request.module.__file__)
195+
filename = os.path.join(test_dir, 'scripts', 'help.txt')
196+
197+
assert base_app.cmdqueue == []
198+
assert base_app._script_dir == []
199+
assert base_app._current_script_dir is None
200+
201+
# make a tmp file to use as a transcript
202+
fd, transcript_fname = tempfile.mkstemp(prefix='', suffix='.trn')
203+
os.close(fd)
204+
205+
# Run the load command with the -r option to generate a transcript
206+
run_cmd(base_app, 'load {} -t {}'.format(filename, transcript_fname))
207+
208+
assert base_app.cmdqueue == []
209+
assert base_app._script_dir == []
210+
assert base_app._current_script_dir is None
211+
212+
# read in the transcript created by the history command
213+
with open(transcript_fname) as f:
214+
xscript = f.read()
215+
216+
expected = '(Cmd) help -v\n' + BASE_HELP_VERBOSE + '\n'
217+
218+
assert xscript == expected
219+
220+
192221
@pytest.mark.parametrize('expected, transformed', [
193222
# strings with zero or one slash or with escaped slashes means no regular
194223
# expression present, so the result should just be what re.escape returns.

0 commit comments

Comments
 (0)