Skip to content

Commit b918b22

Browse files
committed
Merge branch 'master' into alert_printer
2 parents dc4b313 + e5e8a79 commit b918b22

File tree

12 files changed

+225
-180
lines changed

12 files changed

+225
-180
lines changed

cmd2/pyscript_bridge.py

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
import sys
1313
from typing import List, Callable
1414

15+
from .argparse_completer import _RangeAction
16+
from .utils import namedtuple_with_defaults, StdSim
17+
1518
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
1619
if sys.version_info < (3, 5):
1720
from contextlib2 import redirect_stdout, redirect_stderr
1821
else:
1922
from contextlib import redirect_stdout, redirect_stderr
2023

21-
from .argparse_completer import _RangeAction
22-
from .utils import namedtuple_with_defaults
23-
2424

2525
class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr', 'data'])):
2626
"""Encapsulates the results from a command.
@@ -38,37 +38,12 @@ def __bool__(self):
3838
return not self.stderr and self.data is not None
3939

4040

41-
class CopyStream(object):
42-
"""Copies all data written to a stream"""
43-
def __init__(self, inner_stream, echo: bool = False) -> None:
44-
self.buffer = ''
45-
self.inner_stream = inner_stream
46-
self.echo = echo
47-
48-
def write(self, s):
49-
self.buffer += s
50-
if self.echo:
51-
self.inner_stream.write(s)
52-
53-
def read(self):
54-
raise NotImplementedError
55-
56-
def clear(self):
57-
self.buffer = ''
58-
59-
def __getattr__(self, item: str):
60-
if item in self.__dict__:
61-
return self.__dict__[item]
62-
else:
63-
return getattr(self.inner_stream, item)
64-
65-
6641
def _exec_cmd(cmd2_app, func: Callable, echo: bool):
6742
"""Helper to encapsulate executing a command and capturing the results"""
68-
copy_stdout = CopyStream(sys.stdout, echo)
69-
copy_stderr = CopyStream(sys.stderr, echo)
43+
copy_stdout = StdSim(sys.stdout, echo)
44+
copy_stderr = StdSim(sys.stderr, echo)
7045

71-
copy_cmd_stdout = CopyStream(cmd2_app.stdout, echo)
46+
copy_cmd_stdout = StdSim(cmd2_app.stdout, echo)
7247

7348
cmd2_app._last_result = None
7449

@@ -81,9 +56,9 @@ def _exec_cmd(cmd2_app, func: Callable, echo: bool):
8156
cmd2_app.stdout = copy_cmd_stdout.inner_stream
8257

8358
# if stderr is empty, set it to None
84-
stderr = copy_stderr.buffer if copy_stderr.buffer else None
59+
stderr = copy_stderr.getvalue() if copy_stderr.getvalue() else None
8560

86-
outbuf = copy_cmd_stdout.buffer if copy_cmd_stdout.buffer else copy_stdout.buffer
61+
outbuf = copy_cmd_stdout.getvalue() if copy_cmd_stdout.getvalue() else copy_stdout.getvalue()
8762
result = CommandResult(stdout=outbuf, stderr=stderr, data=cmd2_app._last_result)
8863
return result
8964

cmd2/transcript.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def setUp(self):
4444

4545
# Trap stdout
4646
self._orig_stdout = self.cmdapp.stdout
47-
self.cmdapp.stdout = OutputTrap()
47+
self.cmdapp.stdout = utils.StdSim(self.cmdapp.stdout)
4848

4949
def runTest(self): # was testall
5050
if self.cmdapp:
@@ -203,24 +203,3 @@ def tearDown(self):
203203
if self.cmdapp:
204204
# Restore stdout
205205
self.cmdapp.stdout = self._orig_stdout
206-
207-
class OutputTrap(object):
208-
"""Instantiate an OutputTrap to divert/capture ALL stdout output.
209-
For use in transcript testing.
210-
"""
211-
212-
def __init__(self):
213-
self.contents = ''
214-
215-
def write(self, txt: str):
216-
"""Add text to the internal contents."""
217-
self.contents += txt
218-
219-
def read(self) -> str:
220-
"""Read from the internal contents and then clear them out.
221-
222-
:return: str - text from the internal contents
223-
"""
224-
result = self.contents
225-
self.contents = ''
226-
return result

cmd2/utils.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,61 @@ def natural_sort(list_to_sort: List[str]) -> List[str]:
246246
:return: the list sorted naturally
247247
"""
248248
return sorted(list_to_sort, key=natural_keys)
249+
250+
251+
class StdSim(object):
252+
"""Class to simulate behavior of sys.stdout or sys.stderr.
253+
254+
Stores contents in internal buffer and optionally echos to the inner stream it is simulating.
255+
"""
256+
class ByteBuf(object):
257+
"""Inner class which stores an actual bytes buffer and does the actual output if echo is enabled."""
258+
def __init__(self, inner_stream, echo: bool = False) -> None:
259+
self.byte_buf = b''
260+
self.inner_stream = inner_stream
261+
self.echo = echo
262+
263+
def write(self, b: bytes) -> None:
264+
"""Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
265+
if not isinstance(b, bytes):
266+
raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
267+
self.byte_buf += b
268+
if self.echo:
269+
self.inner_stream.buffer.write(b)
270+
271+
def __init__(self, inner_stream, echo: bool = False) -> None:
272+
self.buffer = self.ByteBuf(inner_stream, echo)
273+
self.inner_stream = inner_stream
274+
275+
def write(self, s: str) -> None:
276+
"""Add str to internal bytes buffer and if echo is True, echo contents to inner stream."""
277+
if not isinstance(s, str):
278+
raise TypeError('write() argument must be str, not {}'.format(type(s)))
279+
b = s.encode()
280+
self.buffer.write(b)
281+
282+
def getvalue(self) -> str:
283+
"""Get the internal contents as a str.
284+
285+
:return string from the internal contents
286+
"""
287+
return self.buffer.byte_buf.decode()
288+
289+
def read(self) -> str:
290+
"""Read from the internal contents as a str and then clear them out.
291+
292+
:return: string from the internal contents
293+
"""
294+
result = self.getvalue()
295+
self.clear()
296+
return result
297+
298+
def clear(self) -> None:
299+
"""Clear the internal contents."""
300+
self.buffer.byte_buf = b''
301+
302+
def __getattr__(self, item: str):
303+
if item in self.__dict__:
304+
return self.__dict__[item]
305+
else:
306+
return getattr(self.inner_stream, item)

tests/conftest.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pytest import fixture
1313

1414
import cmd2
15+
from cmd2.utils import StdSim
1516

1617
# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
1718
try:
@@ -115,21 +116,6 @@
115116
""".format(color_str)
116117

117118

118-
class StdOut(object):
119-
""" Toy class for replacing self.stdout in cmd2.Cmd instances for unit testing. """
120-
def __init__(self):
121-
self.buffer = ''
122-
123-
def write(self, s):
124-
self.buffer += s
125-
126-
def read(self):
127-
raise NotImplementedError
128-
129-
def clear(self):
130-
self.buffer = ''
131-
132-
133119
def normalize(block):
134120
""" Normalize a block of text to perform comparison.
135121
@@ -142,18 +128,18 @@ def normalize(block):
142128

143129

144130
def run_cmd(app, cmd):
145-
""" Clear StdOut buffer, run the command, extract the buffer contents, """
131+
""" Clear StdSim buffer, run the command, extract the buffer contents, """
146132
app.stdout.clear()
147133
app.onecmd_plus_hooks(cmd)
148-
out = app.stdout.buffer
134+
out = app.stdout.getvalue()
149135
app.stdout.clear()
150136
return normalize(out)
151137

152138

153139
@fixture
154140
def base_app():
155141
c = cmd2.Cmd()
156-
c.stdout = StdOut()
142+
c.stdout = StdSim(c.stdout)
157143
return c
158144

159145

tests/test_argparse.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import pytest
77

88
import cmd2
9-
from unittest import mock
9+
from cmd2.utils import StdSim
1010

11-
from .conftest import run_cmd, StdOut
11+
from .conftest import run_cmd
1212

1313
# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
1414
try:
@@ -115,7 +115,7 @@ def do_talk(self, args, extra):
115115
@pytest.fixture
116116
def argparse_app():
117117
app = ArgparseApp()
118-
app.stdout = StdOut()
118+
app.stdout = StdSim(app.stdout)
119119
return app
120120

121121

@@ -217,12 +217,12 @@ def do_base(self, args):
217217
func(self, args)
218218
else:
219219
# No subcommand was provided, so call help
220-
self.do_help('base')
220+
self.do_help(['base'])
221221

222222
@pytest.fixture
223223
def subcommand_app():
224224
app = SubcommandApp()
225-
app.stdout = StdOut()
225+
app.stdout = StdSim(app.stdout)
226226
return app
227227

228228

tests/test_autocompletion.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1+
# coding=utf-8
12
"""
23
Unit/functional testing for argparse completer in cmd2
34
45
Copyright 2018 Eric Lin <[email protected]>
56
Released under MIT license, see LICENSE file
67
"""
78
import pytest
8-
from .conftest import run_cmd, normalize, StdOut, complete_tester
9+
10+
from cmd2.utils import StdSim
11+
from .conftest import run_cmd, normalize, complete_tester
912

1013
from examples.tab_autocompletion import TabCompleteExample
1114

1215
@pytest.fixture
1316
def cmd2_app():
14-
c = TabCompleteExample()
15-
c.stdout = StdOut()
16-
17-
return c
17+
app = TabCompleteExample()
18+
app.stdout = StdSim(app.stdout)
19+
return app
1820

1921

2022
SUGGEST_HELP = '''Usage: suggest -t {movie, show} [-h] [-d DURATION{1..2}]

0 commit comments

Comments
 (0)