Skip to content

Commit 7a20c2e

Browse files
authored
Merge pull request #663 from python-cmd2/pyscript_capture
Pyscript now saves command output during the same period that redirection does
2 parents 63343ad + b4dd789 commit 7a20c2e

File tree

7 files changed

+70
-12
lines changed

7 files changed

+70
-12
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.9.13 (TBD, 2019)
2+
* Enhancements
3+
* `pyscript` limits a command's stdout capture to the same period that redirection does.
4+
Therefore output from a command's postparsing and finalization hooks isn't saved in the StdSim object.
5+
16
## 0.9.12 (April 22, 2019)
27
* Bug Fixes
38
* Fixed a bug in how redirection and piping worked inside ``py`` or ``pyscript`` commands

cmd2/cmd2.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1675,10 +1675,13 @@ def parseline(self, line: str) -> Tuple[str, str, str]:
16751675
statement = self.statement_parser.parse_command_only(line)
16761676
return statement.command, statement.args, statement.command_and_args
16771677

1678-
def onecmd_plus_hooks(self, line: str) -> bool:
1678+
def onecmd_plus_hooks(self, line: str, pyscript_bridge_call: bool = False) -> bool:
16791679
"""Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
16801680
16811681
:param line: line of text read from input
1682+
:param pyscript_bridge_call: This should only ever be set to True by PyscriptBridge to signify the beginning
1683+
of an app() call in a pyscript. It is used to enable/disable the storage of the
1684+
command's stdout.
16821685
:return: True if cmdloop() should exit, False otherwise
16831686
"""
16841687
import datetime
@@ -1718,6 +1721,10 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17181721
try:
17191722
# Get sigint protection while we set up redirection
17201723
with self.sigint_protection:
1724+
if pyscript_bridge_call:
1725+
# Start saving command's stdout at this point
1726+
self.stdout.pause_storage = False
1727+
17211728
redir_error, saved_state = self._redirect_output(statement)
17221729
self.cur_pipe_proc_reader = saved_state.pipe_proc_reader
17231730

@@ -1763,6 +1770,10 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17631770
if not already_redirecting:
17641771
self.redirecting = False
17651772

1773+
if pyscript_bridge_call:
1774+
# Stop saving command's stdout before command finalization hooks run
1775+
self.stdout.pause_storage = True
1776+
17661777
except EmptyStatement:
17671778
# don't do anything, but do allow command finalization hooks to run
17681779
pass
@@ -3022,11 +3033,10 @@ def _reset_py_display() -> None:
30223033
@with_argparser(py_parser, preserve_quotes=True)
30233034
def do_py(self, args: argparse.Namespace) -> bool:
30243035
"""Invoke Python command or shell"""
3025-
from .pyscript_bridge import PyscriptBridge, CommandResult
3036+
from .pyscript_bridge import PyscriptBridge
30263037
if self._in_py:
30273038
err = "Recursively entering interactive Python consoles is not allowed."
30283039
self.perror(err, traceback_war=False)
3029-
self._last_result = CommandResult('', err)
30303040
return False
30313041

30323042
try:

cmd2/pyscript_bridge.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,9 @@ def __bool__(self) -> bool:
6262

6363

6464
class PyscriptBridge(object):
65-
"""Preserves the legacy 'cmd' interface for pyscript while also providing a new python API wrapper for
66-
application commands."""
65+
"""Provides a Python API wrapper for application commands."""
6766
def __init__(self, cmd2_app):
6867
self._cmd2_app = cmd2_app
69-
self._last_result = None
7068
self.cmd_echo = False
7169

7270
def __dir__(self):
@@ -89,6 +87,9 @@ def __call__(self, command: str, echo: Optional[bool] = None) -> CommandResult:
8987
# This will be used to capture _cmd2_app.stdout and sys.stdout
9088
copy_cmd_stdout = StdSim(self._cmd2_app.stdout, echo)
9189

90+
# Pause the storing of stdout until onecmd_plus_hooks enables it
91+
copy_cmd_stdout.pause_storage = True
92+
9293
# This will be used to capture sys.stderr
9394
copy_stderr = StdSim(sys.stderr, echo)
9495

@@ -98,7 +99,7 @@ def __call__(self, command: str, echo: Optional[bool] = None) -> CommandResult:
9899
self._cmd2_app.stdout = copy_cmd_stdout
99100
with redirect_stdout(copy_cmd_stdout):
100101
with redirect_stderr(copy_stderr):
101-
self._cmd2_app.onecmd_plus_hooks(command)
102+
self._cmd2_app.onecmd_plus_hooks(command, pyscript_bridge_call=True)
102103
finally:
103104
self._cmd2_app.stdout = copy_cmd_stdout.inner_stream
104105

tests/pyscript/stdout_capture.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# flake8: noqa F821
2+
# This script demonstrates when output of a command finalization hook is captured by a pyscript app() call
3+
import sys
4+
5+
# The unit test framework passes in the string being printed by the command finalization hook
6+
hook_output = sys.argv[1]
7+
8+
# Run a help command which results in 1 call to onecmd_plus_hooks
9+
res = app('help')
10+
11+
# hook_output will not be captured because there are no nested calls to onecmd_plus_hooks
12+
if hook_output not in res.stdout:
13+
print("PASSED")
14+
else:
15+
print("FAILED")
16+
17+
# Run the last command in the history
18+
res = app('history -r -1')
19+
20+
# All output of the history command will be captured. This includes all output of the commands
21+
# started in do_history() using onecmd_plus_hooks(), including any output in those commands' hooks.
22+
# Therefore we expect the hook_output to show up this time.
23+
if hook_output in res.stdout:
24+
print("PASSED")
25+
else:
26+
print("FAILED")

tests/test_cmd2.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,8 @@ def test_recursive_pyscript_not_allowed(base_app, request):
268268
python_script = os.path.join(test_dir, 'scripts', 'recursive.py')
269269
expected = 'Recursively entering interactive Python consoles is not allowed.'
270270

271-
run_cmd(base_app, "pyscript {}".format(python_script))
272-
err = base_app._last_result.stderr
273-
assert err == expected
271+
out, err = run_cmd(base_app, "pyscript {}".format(python_script))
272+
assert err[0] == expected
274273

275274
def test_pyscript_with_nonexist_file(base_app):
276275
python_script = 'does_not_exist.py'

tests/test_plugin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,13 +205,13 @@ def cmdfinalization_hook(self, data: plugin.CommandFinalizationData) -> plugin.C
205205
return data
206206

207207
def cmdfinalization_hook_stop(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData:
208-
"""A postparsing hook which requests application exit"""
208+
"""A command finalization hook which requests application exit"""
209209
self.called_cmdfinalization += 1
210210
data.stop = True
211211
return data
212212

213213
def cmdfinalization_hook_exception(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData:
214-
"""A postparsing hook which raises an exception"""
214+
"""A command finalization hook which raises an exception"""
215215
self.called_cmdfinalization += 1
216216
raise ValueError
217217

tests/test_pyscript.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@
44
Unit/functional testing for pytest in cmd2
55
"""
66
import os
7+
from cmd2 import plugin
78

89
from .conftest import run_cmd
910

11+
HOOK_OUTPUT = "TEST_OUTPUT"
12+
13+
def cmdfinalization_hook(data: plugin.CommandFinalizationData) -> plugin.CommandFinalizationData:
14+
"""A cmdfinalization_hook hook which requests application exit"""
15+
print(HOOK_OUTPUT)
16+
return data
1017

1118
def test_pyscript_help(base_app, request):
1219
test_dir = os.path.dirname(request.module.__file__)
@@ -23,3 +30,13 @@ def test_pyscript_dir(base_app, request):
2330
out, err = run_cmd(base_app, 'pyscript {}'.format(python_script))
2431
assert out
2532
assert out[0] == "['cmd_echo']"
33+
34+
35+
def test_pyscript_stdout_capture(base_app, request):
36+
base_app.register_cmdfinalization_hook(cmdfinalization_hook)
37+
test_dir = os.path.dirname(request.module.__file__)
38+
python_script = os.path.join(test_dir, 'pyscript', 'stdout_capture.py')
39+
out, err = run_cmd(base_app, 'pyscript {} {}'.format(python_script, HOOK_OUTPUT))
40+
41+
assert out[0] == "PASSED"
42+
assert out[1] == "PASSED"

0 commit comments

Comments
 (0)