Skip to content

Commit b22c0bd

Browse files
authored
Merge pull request #666 from python-cmd2/stdsim_fixes
Stdsim fixes
2 parents 7a20c2e + 14b5033 commit b22c0bd

File tree

3 files changed

+52
-21
lines changed

3 files changed

+52
-21
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
* Enhancements
33
* `pyscript` limits a command's stdout capture to the same period that redirection does.
44
Therefore output from a command's postparsing and finalization hooks isn't saved in the StdSim object.
5+
* `StdSim.buffer.write()` now flushes when the wrapped stream uses line buffering and the bytes being written
6+
contain a newline or carriage return. This helps when `pyscript` is echoing the output of a shell command
7+
since the output will print at the same frequency as when the command is run in a terminal.
58

69
## 0.9.12 (April 22, 2019)
710
* Bug Fixes

cmd2/utils.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import sys
99
import threading
1010
import unicodedata
11-
from typing import Any, BinaryIO, Iterable, List, Optional, TextIO, Union
11+
from typing import Any, Iterable, List, Optional, TextIO, Union
1212

1313
from wcwidth import wcswidth
1414

@@ -297,7 +297,7 @@ def __init__(self, inner_stream, echo: bool = False,
297297
encoding: str = 'utf-8', errors: str = 'replace') -> None:
298298
"""
299299
Initializer
300-
:param inner_stream: the emulated stream
300+
:param inner_stream: the wrapped stream. Should be a TextIO or StdSim instance.
301301
:param echo: if True, then all input will be echoed to inner_stream
302302
:param encoding: codec for encoding/decoding strings (defaults to utf-8)
303303
:param errors: how to handle encoding/decoding errors (defaults to replace)
@@ -350,6 +350,17 @@ def isatty(self) -> bool:
350350
else:
351351
return False
352352

353+
@property
354+
def line_buffering(self) -> bool:
355+
"""
356+
Handle when the inner stream doesn't have a line_buffering attribute which is the case
357+
when running unit tests because pytest sets stdout to a pytest EncodedFile object.
358+
"""
359+
try:
360+
return self.inner_stream.line_buffering
361+
except AttributeError:
362+
return False
363+
353364
def __getattr__(self, item: str):
354365
if item in self.__dict__:
355366
return self.__dict__[item]
@@ -361,6 +372,9 @@ class ByteBuf(object):
361372
"""
362373
Used by StdSim to write binary data and stores the actual bytes written
363374
"""
375+
# Used to know when to flush the StdSim
376+
NEWLINES = [b'\n', b'\r']
377+
364378
def __init__(self, std_sim_instance: StdSim) -> None:
365379
self.byte_buf = bytearray()
366380
self.std_sim_instance = std_sim_instance
@@ -374,14 +388,22 @@ def write(self, b: bytes) -> None:
374388
if self.std_sim_instance.echo:
375389
self.std_sim_instance.inner_stream.buffer.write(b)
376390

391+
# Since StdSim wraps TextIO streams, we will flush the stream if line buffering is on
392+
# and the bytes being written contain a new line character. This is helpful when StdSim
393+
# is being used to capture output of a shell command because it causes the output to print
394+
# to the screen more often than if we waited for the stream to flush its buffer.
395+
if self.std_sim_instance.line_buffering:
396+
if any(newline in b for newline in ByteBuf.NEWLINES):
397+
self.std_sim_instance.flush()
398+
377399

378400
class ProcReader(object):
379401
"""
380402
Used to captured stdout and stderr from a Popen process if any of those were set to subprocess.PIPE.
381403
If neither are pipes, then the process will run normally and no output will be captured.
382404
"""
383-
def __init__(self, proc: subprocess.Popen, stdout: Union[StdSim, BinaryIO, TextIO],
384-
stderr: Union[StdSim, BinaryIO, TextIO]) -> None:
405+
def __init__(self, proc: subprocess.Popen, stdout: Union[StdSim, TextIO],
406+
stderr: Union[StdSim, TextIO]) -> None:
385407
"""
386408
ProcReader initializer
387409
:param proc: the Popen process being read from
@@ -457,17 +479,14 @@ def _reader_thread_func(self, read_stdout: bool) -> None:
457479
self._write_bytes(write_stream, available)
458480

459481
@staticmethod
460-
def _write_bytes(stream: Union[StdSim, BinaryIO, TextIO], to_write: bytes) -> None:
482+
def _write_bytes(stream: Union[StdSim, TextIO], to_write: bytes) -> None:
461483
"""
462484
Write bytes to a stream
463485
:param stream: the stream being written to
464486
:param to_write: the bytes being written
465487
"""
466488
try:
467-
if hasattr(stream, 'buffer'):
468-
stream.buffer.write(to_write)
469-
else:
470-
stream.write(to_write)
489+
stream.buffer.write(to_write)
471490
except BrokenPipeError:
472491
# This occurs if output is being piped to a process that closed
473492
pass
@@ -500,7 +519,7 @@ def __exit__(self, *args) -> None:
500519
class RedirectionSavedState(object):
501520
"""Created by each command to store information about their redirection."""
502521

503-
def __init__(self, self_stdout: Union[StdSim, BinaryIO, TextIO], sys_stdout: Union[StdSim, BinaryIO, TextIO],
522+
def __init__(self, self_stdout: Union[StdSim, TextIO], sys_stdout: Union[StdSim, TextIO],
504523
pipe_proc_reader: Optional[ProcReader]) -> None:
505524
# Used to restore values after the command ends
506525
self.saved_self_stdout = self_stdout

tests/test_utils.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,23 +126,12 @@ def stdout_sim():
126126
stdsim = cu.StdSim(sys.stdout, echo=True)
127127
return stdsim
128128

129-
@pytest.fixture
130-
def stringio_sim():
131-
import io
132-
stdsim = cu.StdSim(io.StringIO(), echo=True)
133-
return stdsim
134-
135129

136130
def test_stdsim_write_str(stdout_sim):
137131
my_str = 'Hello World'
138132
stdout_sim.write(my_str)
139133
assert stdout_sim.getvalue() == my_str
140134

141-
def test_stdsim_write_str_inner_no_buffer(stringio_sim):
142-
my_str = 'Hello World'
143-
stringio_sim.write(my_str)
144-
assert stringio_sim.getvalue() == my_str
145-
146135
def test_stdsim_write_bytes(stdout_sim):
147136
b_str = b'Hello World'
148137
with pytest.raises(TypeError):
@@ -218,6 +207,26 @@ def test_stdsim_pause_storage(stdout_sim):
218207
stdout_sim.buffer.write(b_str)
219208
assert stdout_sim.getbytes() == b''
220209

210+
def test_stdsim_line_buffering(base_app):
211+
# This exercises the case of writing binary data that contains new lines/carriage returns to a StdSim
212+
# when line buffering is on. The output should immediately be flushed to the underlying stream.
213+
import os
214+
import tempfile
215+
file = tempfile.NamedTemporaryFile(mode='wt')
216+
file.line_buffering = True
217+
218+
stdsim = cu.StdSim(file, echo=True)
219+
saved_size = os.path.getsize(file.name)
220+
221+
bytes_to_write = b'hello\n'
222+
stdsim.buffer.write(bytes_to_write)
223+
assert os.path.getsize(file.name) == saved_size + len(bytes_to_write)
224+
saved_size = os.path.getsize(file.name)
225+
226+
bytes_to_write = b'hello\r'
227+
stdsim.buffer.write(bytes_to_write)
228+
assert os.path.getsize(file.name) == saved_size + len(bytes_to_write)
229+
221230

222231
@pytest.fixture
223232
def pr_none():

0 commit comments

Comments
 (0)