Skip to content

Commit

Permalink
Merge pull request #220 from python-cmd2/piped_input_improvements
Browse files Browse the repository at this point in the history
Piped input now properly honors the echo setting
  • Loading branch information
tleonhardt authored Aug 24, 2017
2 parents ba1319f + 9ab2614 commit 93a7eb7
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 14 deletions.
44 changes: 33 additions & 11 deletions cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,8 +717,9 @@ def postparsing_postcmd(self, stop):
"""
if not sys.platform.startswith('win'):
# Fix those annoying problems that occur with terminal programs like "less" when you pipe to them
proc = subprocess.Popen(shlex.split('stty sane'))
proc.communicate()
if self.stdin.isatty():
proc = subprocess.Popen(shlex.split('stty sane'))
proc.communicate()
return stop

def parseline(self, line):
Expand Down Expand Up @@ -969,25 +970,46 @@ def _surround_ansi_escapes(prompt, start="\x01", end="\x02"):
return result

def pseudo_raw_input(self, prompt):
"""copied from cmd's cmdloop; like raw_input, but accounts for changed stdin, stdout"""
"""
began life as a copy of cmd's cmdloop; like raw_input but
- accounts for changed stdin, stdout
- if input is a pipe (instead of a tty), look at self.echo
to decide whether to print the prompt and the input
"""

# Deal with the vagaries of readline and ANSI escape codes
safe_prompt = self._surround_ansi_escapes(prompt)

if self.use_rawinput:
try:
line = sm.input(safe_prompt)
if sys.stdin.isatty():
line = sm.input(safe_prompt)
else:
line = sm.input()
if self.echo:
sys.stdout.write('{}{}\n'.format(safe_prompt,line))
except EOFError:
line = 'eof'
else:
self.poutput(safe_prompt, end='')
self.stdout.flush()
line = self.stdin.readline()
if not len(line):
line = 'eof'
if self.stdin.isatty():
# on a tty, print the prompt first, then read the line
self.poutput(safe_prompt, end='')
self.stdout.flush()
line = self.stdin.readline()
if len(line) == 0:
line = 'eof'
else:
line = line.rstrip('\r\n')

# we are reading from a pipe, read the line to see if there is
# anything there, if so, then decide whether to print the
# prompt or not
line = self.stdin.readline()
if len(line):
# we read something, output the prompt and the something
if self.echo:
self.poutput('{}{}'.format(safe_prompt, line))
else:
line = 'eof'
return line.strip()

def _cmdloop(self):
Expand Down
106 changes: 103 additions & 3 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""
import os
import sys
import io
import tempfile

import mock
Expand Down Expand Up @@ -849,6 +850,7 @@ def test_cmdloop_without_rawinput():
# Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
app = cmd2.Cmd()
app.use_rawinput = False
app.echo = False
app.intro = 'Hello World, this is an intro ...'
app.stdout = StdOut()

Expand All @@ -858,7 +860,7 @@ 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 = app.intro + '\n{}'.format(app.prompt)
expected = app.intro + '\n'
with mock.patch.object(sys, 'argv', testargs):
# Run the command loop
app.cmdloop()
Expand Down Expand Up @@ -1367,7 +1369,6 @@ def test_eos(base_app):
# 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
Expand All @@ -1388,7 +1389,106 @@ def test_echo(capsys):
assert app._current_script_dir is None
assert out.startswith('{}{}\n'.format(app.prompt, command) + 'history [arg]: lists past commands issued')


def test_pseudo_raw_input_tty_rawinput_true():
# use context managers so original functions get put back when we are done
# we dont use decorators because we need m_input for the assertion
with mock.patch('sys.stdin.isatty',
mock.MagicMock(name='isatty', return_value=True)):
with mock.patch('six.moves.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.use_rawinput = True
app._cmdloop()
# because we mocked the input() call, we won't get the prompt
# or the name of the command in the output, so we can't check
# if its there. We assume that if input got called twice, once
# for the 'set' command, and once for the 'quit' command,
# that the rest of it worked
assert m_input.call_count == 2

def test_pseudo_raw_input_tty_rawinput_false():
# gin up some input like it's coming from a tty
fakein = io.StringIO(u'{}'.format('set\n'))
mtty = mock.MagicMock(name='isatty', return_value=True)
fakein.isatty = mtty
mreadline = mock.MagicMock(name='readline', wraps=fakein.readline)
fakein.readline = mreadline

# run the cmdloop, telling it where to get input from
app = cmd2.Cmd(stdin=fakein)
app.use_rawinput = False
app._cmdloop()

# because we mocked the readline() call, we won't get the prompt
# or the name of the command in the output, so we can't check
# if its there. We assume that if readline() got called twice, once
# for the 'set' command, and once for the 'quit' command,
# that the rest of it worked
assert mreadline.call_count == 2

# 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.use_rawinput = True
app.echo = echo
# run the cmdloop, which should pull input from our mock
app._cmdloop()
out, err = capsys.readouterr()
return (app, out)

# using the decorator puts the original function at six.moves.input
# back when this method returns
@mock.patch('six.moves.input',
mock.MagicMock(name='input', side_effect=['set', EOFError]))
def test_pseudo_raw_input_piped_rawinput_true_echo_true(capsys):
command = 'set'
app, out = piped_rawinput_true(capsys, True, command)
out = out.splitlines()
assert out[0] == '{}{}'.format(app.prompt, command)
assert out[1] == 'abbrev: False'

# using the decorator puts the original function at six.moves.input
# back when this method returns
@mock.patch('six.moves.input',
mock.MagicMock(name='input', side_effect=['set', EOFError]))
def test_pseudo_raw_input_piped_rawinput_true_echo_false(capsys):
command = 'set'
app, out = piped_rawinput_true(capsys, False, command)
firstline = out.splitlines()[0]
assert firstline == 'abbrev: False'
assert not '{}{}'.format(app.prompt, command) in out

# the next helper function and two tests check for piped
# 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.use_rawinput = False
app.echo = echo
app.abbrev = False
app._cmdloop()
out, err = capsys.readouterr()
return (app, out)

def test_pseudo_raw_input_piped_rawinput_false_echo_true(capsys):
command = 'set'
app, out = piped_rawinput_false(capsys, True, command)
out = out.splitlines()
assert out[0] == '{}{}'.format(app.prompt, command)
assert out[1] == 'abbrev: False'

def test_pseudo_raw_input_piped_rawinput_false_echo_false(capsys):
command = 'set'
app, out = piped_rawinput_false(capsys, False, command)
firstline = out.splitlines()[0]
assert firstline == 'abbrev: False'
assert not '{}{}'.format(app.prompt, command) in out

#
# other input tests
def test_raw_input(base_app):
base_app.use_raw_input = True
fake_input = 'quit'
Expand Down

0 comments on commit 93a7eb7

Please sign in to comment.