summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md5
-rwxr-xr-xREADME.md4
-rw-r--r--cmd2.pngbin294386 -> 312332 bytes
-rw-r--r--cmd2/cmd2.py92
-rw-r--r--cmd2/pyscript_bridge.py41
-rw-r--r--cmd2/transcript.py23
-rw-r--r--cmd2/utils.py58
-rw-r--r--docs/hooks.rst51
-rw-r--r--docs/integrating.rst29
-rwxr-xr-xexamples/python_scripting.py2
-rw-r--r--tests/conftest.py22
-rw-r--r--tests/test_argparse.py10
-rw-r--r--tests/test_autocompletion.py12
-rw-r--r--tests/test_cmd2.py95
-rw-r--r--tests/test_completion.py14
-rw-r--r--tests/test_plugin.py79
-rw-r--r--tests/test_pyscript.py54
-rw-r--r--tests/test_transcript.py16
-rw-r--r--tests/test_utils.py55
19 files changed, 326 insertions, 336 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bf3c961..c03a2323 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,11 @@
for formatting help/description text
* Aliases are now sorted alphabetically
* The **set** command now tab-completes settable parameter names
+* Deletions
+ * The ``preparse``, ``postparsing_precmd``, and ``postparsing_postcmd`` methods *deprecated* in the previous release
+ have been deleted
+ * The new application lifecycle hook system allows for registration of callbacks to be called at various points
+ in the lifecycle and is more powerful and flexible than the previous system
## 0.9.4 (August 21, 2018)
* Bug Fixes
diff --git a/README.md b/README.md
index 72697054..689ab1a1 100755
--- a/README.md
+++ b/README.md
@@ -14,8 +14,8 @@ applications. It provides a simple API which is an extension of Python's built-
of cmd to make your life easier and eliminates much of the boilerplate code which would be necessary
when using cmd.
-[![Screenshot](cmd2.png)](https://github.com/python-cmd2/cmd2/blob/master/cmd2.png)
-
+Click on image below to watch a short video demonstrating the capabilities of cmd2:
+[![Screenshot](cmd2.png)](https://youtu.be/DDU_JH6cFsA)
Main Features
-------------
diff --git a/cmd2.png b/cmd2.png
index 810f7c9e..e1728eec 100644
--- a/cmd2.png
+++ b/cmd2.png
Binary files differ
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 26cfbf4a..333b4706 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -1649,58 +1649,6 @@ class Cmd(cmd.Cmd):
"""
return statement
- # ----- Methods which are cmd2-specific lifecycle hooks which are not present in cmd -----
-
- # noinspection PyMethodMayBeStatic
- def preparse(self, raw: str) -> str:
- """Hook method executed before user input is parsed.
-
- WARNING: If it's a multiline command, `preparse()` may not get all the
- user input. _complete_statement() really does two things: a) parse the
- user input, and b) accept more input in case it's a multiline command
- the passed string doesn't have a terminator. `preparse()` is currently
- called before we know whether it's a multiline command, and before we
- know whether the user input includes a termination character.
-
- If you want a reliable pre parsing hook method, register a postparsing
- hook, modify the user input, and then reparse it.
-
- :param raw: raw command line input :return: potentially modified raw command line input
- :return: a potentially modified version of the raw input string
- """
- return raw
-
- # noinspection PyMethodMayBeStatic
- def postparsing_precmd(self, statement: Statement) -> Tuple[bool, Statement]:
- """This runs after parsing the command-line, but before anything else; even before adding cmd to history.
-
- NOTE: This runs before precmd() and prior to any potential output redirection or piping.
-
- If you wish to fatally fail this command and exit the application entirely, set stop = True.
-
- If you wish to just fail this command you can do so by raising an exception:
-
- - raise EmptyStatement - will silently fail and do nothing
- - raise <AnyOtherException> - will fail and print an error message
-
- :param statement: the parsed command-line statement as a Statement object
- :return: (stop, statement) containing a potentially modified version of the statement object
- """
- stop = False
- return stop, statement
-
- # noinspection PyMethodMayBeStatic
- def postparsing_postcmd(self, stop: bool) -> bool:
- """This runs after everything else, including after postcmd().
-
- It even runs when an empty line is entered. Thus, if you need to do something like update the prompt due
- to notifications from a background thread, then this is the method you want to override to do it.
-
- :param stop: True implies the entire application should exit.
- :return: True implies the entire application should exit.
- """
- return stop
-
def parseline(self, line: str) -> Tuple[str, str, str]:
"""Parse the line into a command name and a string containing the arguments.
@@ -1740,9 +1688,6 @@ class Cmd(cmd.Cmd):
data = func(data)
if data.stop:
break
- # postparsing_precmd is deprecated
- if not data.stop:
- (data.stop, data.statement) = self.postparsing_precmd(data.statement)
# unpack the data object
statement = data.statement
stop = data.stop
@@ -1807,9 +1752,7 @@ class Cmd(cmd.Cmd):
data = func(data)
# retrieve the final value of stop, ignoring any
# modifications to the statement
- stop = data.stop
- # postparsing_postcmd is deprecated
- return self.postparsing_postcmd(stop)
+ return data.stop
except Exception as ex:
self.perror(ex)
@@ -1863,9 +1806,6 @@ class Cmd(cmd.Cmd):
pipe runs out. We can't refactor it because we need to retain
backwards compatibility with the standard library version of cmd.
"""
- # preparse() is deprecated, use self.register_postparsing_hook() instead
- line = self.preparse(line)
-
while True:
try:
statement = self.statement_parser.parse(line)
@@ -2220,7 +2160,7 @@ class Cmd(cmd.Cmd):
def do_alias(self, statement: Statement) -> None:
"""Define or display aliases
-Usage: Usage: alias [name] | [<name> <value>]
+ Usage: alias [name] | [<name> <value>]
Where:
name - name of the alias being looked up, added, or replaced
value - what the alias will be resolved to (if adding or replacing)
@@ -2248,7 +2188,8 @@ Usage: Usage: alias [name] | [<name> <value>]
# If no args were given, then print a list of current aliases
if not alias_arg_list:
- for cur_alias in self.aliases:
+ sorted_aliases = utils.alphabetical_sort(list(self.aliases))
+ for cur_alias in sorted_aliases:
self.poutput("alias {} {}".format(cur_alias, self.aliases[cur_alias]))
return
@@ -2282,9 +2223,6 @@ Usage: Usage: alias [name] | [<name> <value>]
# Set the alias
self.aliases[name] = value
self.poutput("Alias {!r} created".format(name))
-
- # Keep aliases in alphabetically sorted order
- self.aliases = collections.OrderedDict(sorted(self.aliases.items()))
else:
errmsg = "Aliases can not contain: {}".format(invalidchars)
self.perror(errmsg, traceback_war=False)
@@ -2305,7 +2243,7 @@ Usage: Usage: alias [name] | [<name> <value>]
def do_unalias(self, arglist: List[str]) -> None:
"""Unsets aliases
-Usage: Usage: unalias [-a] name [name ...]
+ Usage: unalias [-a] name [name ...]
Where:
name - name of the alias being unset
@@ -2454,13 +2392,17 @@ Usage: Usage: unalias [-a] name [name ...]
doc_block = []
found_first = False
for doc_line in doc.splitlines():
- str(doc_line).strip()
- if len(doc_line.strip()) > 0:
- doc_block.append(doc_line.strip())
- found_first = True
- else:
+ stripped_line = doc_line.strip()
+
+ # Don't include :param type lines
+ if stripped_line.startswith(':'):
if found_first:
break
+ elif stripped_line:
+ doc_block.append(stripped_line)
+ found_first = True
+ elif found_first:
+ break
for doc_line in doc_block:
self.stdout.write('{: <{col_width}}{doc}\n'.format(command,
@@ -2686,9 +2628,11 @@ Usage: Usage: unalias [-a] name [name ...]
Non-python commands can be issued with ``pyscript_name("your command")``.
Run python code from external script files with ``run("script.py")``
"""
- from .pyscript_bridge import PyscriptBridge
+ from .pyscript_bridge import PyscriptBridge, CommandResult
if self._in_py:
- self.perror("Recursively entering interactive Python consoles is not allowed.", traceback_war=False)
+ err = "Recursively entering interactive Python consoles is not allowed."
+ self.perror(err, traceback_war=False)
+ self._last_result = CommandResult('', err)
return False
self._in_py = True
diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py
index 3f58ab84..d8fa5fb9 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/pyscript_bridge.py
@@ -12,15 +12,15 @@ import functools
import sys
from typing import List, Callable
+from .argparse_completer import _RangeAction
+from .utils import namedtuple_with_defaults, StdSim
+
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
if sys.version_info < (3, 5):
from contextlib2 import redirect_stdout, redirect_stderr
else:
from contextlib import redirect_stdout, redirect_stderr
-from .argparse_completer import _RangeAction
-from .utils import namedtuple_with_defaults
-
class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr', 'data'])):
"""Encapsulates the results from a command.
@@ -38,37 +38,12 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
return not self.stderr and self.data is not None
-class CopyStream(object):
- """Copies all data written to a stream"""
- def __init__(self, inner_stream, echo: bool = False) -> None:
- self.buffer = ''
- self.inner_stream = inner_stream
- self.echo = echo
-
- def write(self, s):
- self.buffer += s
- if self.echo:
- self.inner_stream.write(s)
-
- def read(self):
- raise NotImplementedError
-
- def clear(self):
- self.buffer = ''
-
- def __getattr__(self, item: str):
- if item in self.__dict__:
- return self.__dict__[item]
- else:
- return getattr(self.inner_stream, item)
-
-
def _exec_cmd(cmd2_app, func: Callable, echo: bool):
"""Helper to encapsulate executing a command and capturing the results"""
- copy_stdout = CopyStream(sys.stdout, echo)
- copy_stderr = CopyStream(sys.stderr, echo)
+ copy_stdout = StdSim(sys.stdout, echo)
+ copy_stderr = StdSim(sys.stderr, echo)
- copy_cmd_stdout = CopyStream(cmd2_app.stdout, echo)
+ copy_cmd_stdout = StdSim(cmd2_app.stdout, echo)
cmd2_app._last_result = None
@@ -81,9 +56,9 @@ def _exec_cmd(cmd2_app, func: Callable, echo: bool):
cmd2_app.stdout = copy_cmd_stdout.inner_stream
# if stderr is empty, set it to None
- stderr = copy_stderr.buffer if copy_stderr.buffer else None
+ stderr = copy_stderr.getvalue() if copy_stderr.getvalue() else None
- outbuf = copy_cmd_stdout.buffer if copy_cmd_stdout.buffer else copy_stdout.buffer
+ outbuf = copy_cmd_stdout.getvalue() if copy_cmd_stdout.getvalue() else copy_stdout.getvalue()
result = CommandResult(stdout=outbuf, stderr=stderr, data=cmd2_app._last_result)
return result
diff --git a/cmd2/transcript.py b/cmd2/transcript.py
index 8df58634..2d94f4e4 100644
--- a/cmd2/transcript.py
+++ b/cmd2/transcript.py
@@ -44,7 +44,7 @@ class Cmd2TestCase(unittest.TestCase):
# Trap stdout
self._orig_stdout = self.cmdapp.stdout
- self.cmdapp.stdout = OutputTrap()
+ self.cmdapp.stdout = utils.StdSim(self.cmdapp.stdout)
def runTest(self): # was testall
if self.cmdapp:
@@ -203,24 +203,3 @@ class Cmd2TestCase(unittest.TestCase):
if self.cmdapp:
# Restore stdout
self.cmdapp.stdout = self._orig_stdout
-
-class OutputTrap(object):
- """Instantiate an OutputTrap to divert/capture ALL stdout output.
- For use in transcript testing.
- """
-
- def __init__(self):
- self.contents = ''
-
- def write(self, txt: str):
- """Add text to the internal contents."""
- self.contents += txt
-
- def read(self) -> str:
- """Read from the internal contents and then clear them out.
-
- :return: str - text from the internal contents
- """
- result = self.contents
- self.contents = ''
- return result
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 735221c8..bdb488cc 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -246,3 +246,61 @@ def natural_sort(list_to_sort: List[str]) -> List[str]:
:return: the list sorted naturally
"""
return sorted(list_to_sort, key=natural_keys)
+
+
+class StdSim(object):
+ """Class to simulate behavior of sys.stdout or sys.stderr.
+
+ Stores contents in internal buffer and optionally echos to the inner stream it is simulating.
+ """
+ class ByteBuf(object):
+ """Inner class which stores an actual bytes buffer and does the actual output if echo is enabled."""
+ def __init__(self, inner_stream, echo: bool = False) -> None:
+ self.byte_buf = b''
+ self.inner_stream = inner_stream
+ self.echo = echo
+
+ def write(self, b: bytes) -> None:
+ """Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
+ if not isinstance(b, bytes):
+ raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
+ self.byte_buf += b
+ if self.echo:
+ self.inner_stream.buffer.write(b)
+
+ def __init__(self, inner_stream, echo: bool = False) -> None:
+ self.buffer = self.ByteBuf(inner_stream, echo)
+ self.inner_stream = inner_stream
+
+ def write(self, s: str) -> None:
+ """Add str to internal bytes buffer and if echo is True, echo contents to inner stream."""
+ if not isinstance(s, str):
+ raise TypeError('write() argument must be str, not {}'.format(type(s)))
+ b = s.encode()
+ self.buffer.write(b)
+
+ def getvalue(self) -> str:
+ """Get the internal contents as a str.
+
+ :return string from the internal contents
+ """
+ return self.buffer.byte_buf.decode()
+
+ def read(self) -> str:
+ """Read from the internal contents as a str and then clear them out.
+
+ :return: string from the internal contents
+ """
+ result = self.getvalue()
+ self.clear()
+ return result
+
+ def clear(self) -> None:
+ """Clear the internal contents."""
+ self.buffer.byte_buf = b''
+
+ def __getattr__(self, item: str):
+ if item in self.__dict__:
+ return self.__dict__[item]
+ else:
+ return getattr(self.inner_stream, item)
diff --git a/docs/hooks.rst b/docs/hooks.rst
index 2a5d7b5f..1696d365 100644
--- a/docs/hooks.rst
+++ b/docs/hooks.rst
@@ -76,24 +76,21 @@ Command Processing Loop
When you call `.cmdloop()`, the following sequence of events are repeated until
the application exits:
-1. Output the prompt
-2. Accept user input
-3. Call `preparse()` - for backwards compatibility with prior releases of cmd2, now deprecated
-4. Parse user input into `Statement` object
-5. Call methods registered with `register_postparsing_hook()`
-6. Call `postparsing_precmd()` - for backwards compatibility with prior releases of cmd2, now deprecated
-7. Redirect output, if user asked for it and it's allowed
-8. Start timer
-9. Call methods registered with `register_precmd_hook()`
-10. Call `precmd()` - for backwards compatibility with ``cmd.Cmd``
-11. Add statement to history
-12. Call `do_command` method
-13. Call methods registered with `register_postcmd_hook()`
-14. Call `postcmd(stop, statement)` - for backwards compatibility with ``cmd.Cmd``
-15. Stop timer and display the elapsed time
-16. Stop redirecting output if it was redirected
-17. Call methods registered with `register_cmdfinalization_hook()`
-18. Call `postparsing_postcmd()` - for backwards compatibility - deprecated
+#. Output the prompt
+#. Accept user input
+#. Parse user input into `Statement` object
+#. Call methods registered with `register_postparsing_hook()`
+#. Redirect output, if user asked for it and it's allowed
+#. Start timer
+#. Call methods registered with `register_precmd_hook()`
+#. Call `precmd()` - for backwards compatibility with ``cmd.Cmd``
+#. Add statement to history
+#. Call `do_command` method
+#. Call methods registered with `register_postcmd_hook()`
+#. Call `postcmd(stop, statement)` - for backwards compatibility with ``cmd.Cmd``
+#. Stop timer and display the elapsed time
+#. Stop redirecting output if it was redirected
+#. Call methods registered with `register_cmdfinalization_hook()`
By registering hook methods, steps 4, 8, 12, and 16 allow you to run code
during, and control the flow of the command processing loop. Be aware that
@@ -305,21 +302,3 @@ If any command finalization hook raises an exception, no more command
finalization hooks will be called. If the last hook to return a value returned
``True``, then the exception will be rendered, and the application will
terminate.
-
-Deprecated Command Processing Hooks
------------------------------------
-
-Inside the main loop, every time the user hits <Enter> the line is processed by the ``onecmd_plus_hooks`` method.
-
-.. automethod:: cmd2.cmd2.Cmd.onecmd_plus_hooks
-
-As the ``onecmd_plus_hooks`` name implies, there are a number of *hook* methods that can be defined in order to inject
-application-specific behavior at various points during the processing of a line of text entered by the user. ``cmd2``
-increases the 2 hooks provided by ``cmd`` (**precmd** and **postcmd**) to 6 for greater flexibility. Here are
-the various hook methods, presented in chronological order starting with the ones called earliest in the process.
-
-.. automethod:: cmd2.cmd2.Cmd.preparse
-
-.. automethod:: cmd2.cmd2.Cmd.postparsing_precmd
-
-.. automethod:: cmd2.cmd2.Cmd.postparsing_postcmd
diff --git a/docs/integrating.rst b/docs/integrating.rst
index 8f605e06..a8377fdb 100644
--- a/docs/integrating.rst
+++ b/docs/integrating.rst
@@ -135,22 +135,19 @@ script file.
The **onecmd_plus_hooks()** method will do the following to execute a single
``cmd2`` command in a normal fashion:
-1. Call `preparse()` - for backwards compatibility with prior releases of cmd2, now deprecated
-2. Parse user input into `Statement` object
-3. Call methods registered with `register_postparsing_hook()`
-4. Call `postparsing_precmd()` - for backwards compatibility with prior releases of cmd2, now deprecated
-5. Redirect output, if user asked for it and it's allowed
-6. Start timer
-7. Call methods registered with `register_precmd_hook()`
-8. Call `precmd()` - for backwards compatibility with ``cmd.Cmd``
-9. Add statement to history
-10. Call `do_command` method
-11. Call methods registered with `register_postcmd_hook()`
-12. Call `postcmd(stop, statement)` - for backwards compatibility with ``cmd.Cmd``
-13. Stop timer and display the elapsed time
-14. Stop redirecting output if it was redirected
-15. Call methods registered with `register_cmdfinalization_hook()`
-16. Call `postparsing_postcmd()` - for backwards compatibility - deprecated
+#. Parse user input into `Statement` object
+#. Call methods registered with `register_postparsing_hook()`
+#. Redirect output, if user asked for it and it's allowed
+#. Start timer
+#. Call methods registered with `register_precmd_hook()`
+#. Call `precmd()` - for backwards compatibility with ``cmd.Cmd``
+#. Add statement to history
+#. Call `do_command` method
+#. Call methods registered with `register_postcmd_hook()`
+#. Call `postcmd(stop, statement)` - for backwards compatibility with ``cmd.Cmd``
+#. Stop timer and display the elapsed time
+#. Stop redirecting output if it was redirected
+#. Call methods registered with `register_cmdfinalization_hook()`
Running in this fashion enables the ability to integrate with an external event
loop. However, how to integrate with any specific event loop is beyond the
diff --git a/examples/python_scripting.py b/examples/python_scripting.py
index ab5ecc2b..4c959f58 100755
--- a/examples/python_scripting.py
+++ b/examples/python_scripting.py
@@ -27,7 +27,7 @@ class CmdLineApp(cmd2.Cmd):
# Enable the optional ipy command if IPython is installed by setting use_ipython=True
super().__init__(use_ipython=True)
self._set_prompt()
- self.intro = 'Happy 𝛑 Day. Note the full Unicode support: 😇 (Python 3 only) 💩'
+ self.intro = 'Happy 𝛑 Day. Note the full Unicode support: 😇 💩'
self.locals_in_py = True
def _set_prompt(self):
diff --git a/tests/conftest.py b/tests/conftest.py
index a85135b9..561f281b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,6 +12,7 @@ from unittest import mock
from pytest import fixture
import cmd2
+from cmd2.utils import StdSim
# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
try:
@@ -115,21 +116,6 @@ timing: False # Report execution times
""".format(color_str)
-class StdOut(object):
- """ Toy class for replacing self.stdout in cmd2.Cmd instances for unit testing. """
- def __init__(self):
- self.buffer = ''
-
- def write(self, s):
- self.buffer += s
-
- def read(self):
- raise NotImplementedError
-
- def clear(self):
- self.buffer = ''
-
-
def normalize(block):
""" Normalize a block of text to perform comparison.
@@ -142,10 +128,10 @@ def normalize(block):
def run_cmd(app, cmd):
- """ Clear StdOut buffer, run the command, extract the buffer contents, """
+ """ Clear StdSim buffer, run the command, extract the buffer contents, """
app.stdout.clear()
app.onecmd_plus_hooks(cmd)
- out = app.stdout.buffer
+ out = app.stdout.getvalue()
app.stdout.clear()
return normalize(out)
@@ -153,7 +139,7 @@ def run_cmd(app, cmd):
@fixture
def base_app():
c = cmd2.Cmd()
- c.stdout = StdOut()
+ c.stdout = StdSim(c.stdout)
return c
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index 469cbe76..bf20710b 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -6,9 +6,9 @@ import argparse
import pytest
import cmd2
-from unittest import mock
+from cmd2.utils import StdSim
-from .conftest import run_cmd, StdOut
+from .conftest import run_cmd
# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
try:
@@ -115,7 +115,7 @@ class ArgparseApp(cmd2.Cmd):
@pytest.fixture
def argparse_app():
app = ArgparseApp()
- app.stdout = StdOut()
+ app.stdout = StdSim(app.stdout)
return app
@@ -217,12 +217,12 @@ class SubcommandApp(cmd2.Cmd):
func(self, args)
else:
# No subcommand was provided, so call help
- self.do_help('base')
+ self.do_help(['base'])
@pytest.fixture
def subcommand_app():
app = SubcommandApp()
- app.stdout = StdOut()
+ app.stdout = StdSim(app.stdout)
return app
diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py
index 8aa26e0e..8035587a 100644
--- a/tests/test_autocompletion.py
+++ b/tests/test_autocompletion.py
@@ -1,3 +1,4 @@
+# coding=utf-8
"""
Unit/functional testing for argparse completer in cmd2
@@ -5,16 +6,17 @@ Copyright 2018 Eric Lin <anselor@gmail.com>
Released under MIT license, see LICENSE file
"""
import pytest
-from .conftest import run_cmd, normalize, StdOut, complete_tester
+
+from cmd2.utils import StdSim
+from .conftest import run_cmd, normalize, complete_tester
from examples.tab_autocompletion import TabCompleteExample
@pytest.fixture
def cmd2_app():
- c = TabCompleteExample()
- c.stdout = StdOut()
-
- return c
+ app = TabCompleteExample()
+ app.stdout = StdSim(app.stdout)
+ return app
SUGGEST_HELP = '''Usage: suggest -t {movie, show} [-h] [-d DURATION{1..2}]
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 1e7e2c3f..e3992c7b 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -25,7 +25,7 @@ import cmd2
from cmd2 import clipboard
from cmd2 import utils
from .conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \
- HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, StdOut
+ HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG
def test_version(base_app):
@@ -46,6 +46,11 @@ def test_base_help_verbose(base_app):
expected = normalize(BASE_HELP_VERBOSE)
assert out == expected
+ # Make sure :param type lines are filtered out of help summary
+ help_doc = base_app.do_help.__func__.__doc__
+ help_doc += "\n:param fake param"
+ base_app.do_help.__func__.__doc__ = help_doc
+
out = run_cmd(base_app, 'help --verbose')
assert out == expected
@@ -215,13 +220,13 @@ def test_base_run_pyscript(base_app, capsys, request):
out, err = capsys.readouterr()
assert out == expected
-def test_recursive_pyscript_not_allowed(base_app, capsys, request):
+def test_recursive_pyscript_not_allowed(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
python_script = os.path.join(test_dir, 'scripts', 'recursive.py')
- expected = 'ERROR: Recursively entering interactive Python consoles is not allowed.\n'
+ expected = 'Recursively entering interactive Python consoles is not allowed.'
run_cmd(base_app, "pyscript {}".format(python_script))
- out, err = capsys.readouterr()
+ err = base_app._last_result.stderr
assert err == expected
def test_pyscript_with_nonexist_file(base_app, capsys):
@@ -930,7 +935,7 @@ def test_base_cmdloop_with_queue():
app.use_rawinput = True
intro = 'Hello World, this is an intro ...'
app.cmdqueue.append('quit\n')
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
# Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
testargs = ["prog"]
@@ -938,7 +943,7 @@ def test_base_cmdloop_with_queue():
with mock.patch.object(sys, 'argv', testargs):
# Run the command loop with custom intro
app.cmdloop(intro=intro)
- out = app.stdout.buffer
+ out = app.stdout.getvalue()
assert out == expected
@@ -947,7 +952,7 @@ def test_base_cmdloop_without_queue():
app = cmd2.Cmd()
app.use_rawinput = True
app.intro = 'Hello World, this is an intro ...'
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
# Mock out the input call so we don't actually wait for a user's response on stdin
m = mock.MagicMock(name='input', return_value='quit')
@@ -959,7 +964,7 @@ def test_base_cmdloop_without_queue():
with mock.patch.object(sys, 'argv', testargs):
# Run the command loop
app.cmdloop()
- out = app.stdout.buffer
+ out = app.stdout.getvalue()
assert out == expected
@@ -969,7 +974,7 @@ def test_cmdloop_without_rawinput():
app.use_rawinput = False
app.echo = False
app.intro = 'Hello World, this is an intro ...'
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
# Mock out the input call so we don't actually wait for a user's response on stdin
m = mock.MagicMock(name='input', return_value='quit')
@@ -981,22 +986,25 @@ def test_cmdloop_without_rawinput():
with mock.patch.object(sys, 'argv', testargs):
# Run the command loop
app.cmdloop()
- out = app.stdout.buffer
+ out = app.stdout.getvalue()
assert out == expected
class HookFailureApp(cmd2.Cmd):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+ # register a postparsing hook method
+ self.register_postparsing_hook(self.postparsing_precmd)
- def postparsing_precmd(self, statement):
+ def postparsing_precmd(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
"""Simulate precmd hook failure."""
- return True, statement
+ data.stop = True
+ return data
@pytest.fixture
def hook_failure():
app = HookFailureApp()
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
return app
def test_precmd_hook_success(base_app):
@@ -1020,7 +1028,7 @@ class SayApp(cmd2.Cmd):
def say_app():
app = SayApp()
app.allow_cli_args = False
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
return app
def test_interrupt_quit(say_app):
@@ -1034,7 +1042,7 @@ def test_interrupt_quit(say_app):
say_app.cmdloop()
# And verify the expected output to stdout
- out = say_app.stdout.buffer
+ out = say_app.stdout.getvalue()
assert out == 'hello\n'
def test_interrupt_noquit(say_app):
@@ -1048,7 +1056,7 @@ def test_interrupt_noquit(say_app):
say_app.cmdloop()
# And verify the expected output to stdout
- out = say_app.stdout.buffer
+ out = say_app.stdout.getvalue()
assert out == 'hello\n^C\ngoodbye\n'
@@ -1060,7 +1068,7 @@ class ShellApp(cmd2.Cmd):
@pytest.fixture
def shell_app():
app = ShellApp()
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
return app
def test_default_to_shell_unknown(shell_app):
@@ -1140,7 +1148,7 @@ class HelpApp(cmd2.Cmd):
@pytest.fixture
def help_app():
app = HelpApp()
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
return app
def test_custom_command_help(help_app):
@@ -1203,7 +1211,7 @@ class HelpCategoriesApp(cmd2.Cmd):
@pytest.fixture
def helpcat_app():
app = HelpCategoriesApp()
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
return app
def test_help_cat_base(helpcat_app):
@@ -1296,7 +1304,7 @@ class SelectApp(cmd2.Cmd):
@pytest.fixture
def select_app():
app = SelectApp()
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
return app
def test_select_options(select_app):
@@ -1461,7 +1469,7 @@ class MultilineApp(cmd2.Cmd):
@pytest.fixture
def multiline_app():
app = MultilineApp()
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
return app
def test_multiline_complete_empty_statement_raises_exception(multiline_app):
@@ -1522,7 +1530,7 @@ class CommandResultApp(cmd2.Cmd):
@pytest.fixture
def commandresult_app():
app = CommandResultApp()
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
return app
def test_commandresult_truthy(commandresult_app):
@@ -1628,7 +1636,7 @@ def piped_rawinput_true(capsys, echo, command):
# run the cmdloop, which should pull input from our mock
app._cmdloop()
out, err = capsys.readouterr()
- return (app, out)
+ return app, out
# using the decorator puts the original input function back when this unit test returns
@mock.patch('builtins.input', mock.MagicMock(name='input', side_effect=['set', EOFError]))
@@ -1658,7 +1666,7 @@ def piped_rawinput_false(capsys, echo, command):
app.echo = echo
app._cmdloop()
out, err = capsys.readouterr()
- return (app, out)
+ return app, out
def test_pseudo_raw_input_piped_rawinput_false_echo_true(capsys):
command = 'set'
@@ -1715,28 +1723,28 @@ def test_empty_stdin_input():
def test_poutput_string(base_app):
msg = 'This is a test'
base_app.poutput(msg)
- out = base_app.stdout.buffer
+ out = base_app.stdout.getvalue()
expected = msg + '\n'
assert out == expected
def test_poutput_zero(base_app):
msg = 0
base_app.poutput(msg)
- out = base_app.stdout.buffer
+ out = base_app.stdout.getvalue()
expected = str(msg) + '\n'
assert out == expected
def test_poutput_empty_string(base_app):
msg = ''
base_app.poutput(msg)
- out = base_app.stdout.buffer
+ out = base_app.stdout.getvalue()
expected = msg
assert out == expected
def test_poutput_none(base_app):
msg = None
base_app.poutput(msg)
- out = base_app.stdout.buffer
+ out = base_app.stdout.getvalue()
expected = ''
assert out == expected
@@ -1820,13 +1828,28 @@ def test_complete_unalias(base_app):
# Validate that there are now completions
expected = ['fake', 'fall']
- assert base_app.complete_unalias(text, line, begidx, endidx) == expected
+ result = base_app.complete_unalias(text, line, begidx, endidx)
+ assert sorted(expected) == sorted(result)
+
+def test_multiple_aliases(base_app):
+ alias1 = 'h1'
+ alias2 = 'h2'
+ run_cmd(base_app, 'alias {} help'.format(alias1))
+ run_cmd(base_app, 'alias {} help -v'.format(alias2))
+ out = run_cmd(base_app, alias1)
+ expected = normalize(BASE_HELP)
+ assert out == expected
+
+ out = run_cmd(base_app, alias2)
+ expected = normalize(BASE_HELP_VERBOSE)
+ assert out == expected
+
def test_ppaged(base_app):
msg = 'testing...'
end = '\n'
base_app.ppaged(msg)
- out = base_app.stdout.buffer
+ out = base_app.stdout.getvalue()
assert out == msg + end
# we override cmd.parseline() so we always get consistent
@@ -1859,14 +1882,14 @@ def test_readline_remove_history_item(base_app):
def test_onecmd_raw_str_continue(base_app):
line = "help"
stop = base_app.onecmd(line)
- out = base_app.stdout.buffer
+ out = base_app.stdout.getvalue()
assert not stop
assert out.strip() == BASE_HELP.strip()
def test_onecmd_raw_str_quit(base_app):
line = "quit"
stop = base_app.onecmd(line)
- out = base_app.stdout.buffer
+ out = base_app.stdout.getvalue()
assert stop
assert out == ''
@@ -1996,7 +2019,7 @@ def test_exit_code_default(exit_code_repl):
# Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
app = exit_code_repl
app.use_rawinput = True
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
# Mock out the input call so we don't actually wait for a user's response on stdin
m = mock.MagicMock(name='input', return_value='exit')
@@ -2008,14 +2031,14 @@ def test_exit_code_default(exit_code_repl):
with mock.patch.object(sys, 'argv', testargs):
# Run the command loop
app.cmdloop()
- out = app.stdout.buffer
+ out = app.stdout.getvalue()
assert out == expected
def test_exit_code_nonzero(exit_code_repl):
# Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
app = exit_code_repl
app.use_rawinput = True
- app.stdout = StdOut()
+ app.stdout = utils.StdSim(app.stdout)
# Mock out the input call so we don't actually wait for a user's response on stdin
m = mock.MagicMock(name='input', return_value='exit 23')
@@ -2028,5 +2051,5 @@ def test_exit_code_nonzero(exit_code_repl):
# Run the command loop
with pytest.raises(SystemExit):
app.cmdloop()
- out = app.stdout.buffer
+ out = app.stdout.getvalue()
assert out == expected
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 00a120cc..40299954 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -15,7 +15,7 @@ import sys
import pytest
import cmd2
from cmd2 import utils
-from .conftest import complete_tester, StdOut
+from .conftest import complete_tester
from examples.subcommands import SubcommandsExample
# List of strings used with completion functions
@@ -114,13 +114,6 @@ def test_complete_bogus_command(cmd2_app):
assert first_match is None
-def test_cmd2_command_completion_single(cmd2_app):
- text = 'hel'
- line = text
- endidx = len(line)
- begidx = endidx - len(text)
- assert cmd2_app.completenames(text, line, begidx, endidx) == ['help']
-
def test_cmd2_command_completion_multiple(cmd2_app):
text = 'h'
line = text
@@ -694,8 +687,7 @@ def test_add_opening_quote_delimited_space_in_prefix(cmd2_app):
@pytest.fixture
def sc_app():
c = SubcommandsExample()
- c.stdout = StdOut()
-
+ c.stdout = utils.StdSim(c.stdout)
return c
def test_cmd2_subcommand_completion_single_end(sc_app):
@@ -843,7 +835,7 @@ class SubcommandsWithUnknownExample(cmd2.Cmd):
func(self, args)
else:
# No subcommand was provided, so call help
- self.do_help('base')
+ self.do_help(['base'])
@pytest.fixture
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index e401e837..81dd7683 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -5,18 +5,14 @@ Test plugin infrastructure and hooks.
Copyright 2018 Jared Crapo <jared@kotfu.net>
Released under MIT license, see LICENSE file
"""
-
-from typing import Tuple
-
import pytest
import cmd2
from cmd2 import plugin
-from .conftest import StdOut
class Plugin:
- "A mixin class for testing hook registration and calling"
+ """A mixin class for testing hook registration and calling"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.reset_counters()
@@ -35,19 +31,19 @@ class Plugin:
#
###
def prepost_hook_one(self) -> None:
- "Method used for preloop or postloop hooks"
+ """Method used for preloop or postloop hooks"""
self.poutput("one")
def prepost_hook_two(self) -> None:
- "Another method used for preloop or postloop hooks"
+ """Another method used for preloop or postloop hooks"""
self.poutput("two")
def prepost_hook_too_many_parameters(self, param) -> None:
- "A preloop or postloop hook with too many parameters"
+ """A preloop or postloop hook with too many parameters"""
pass
def prepost_hook_with_wrong_return_annotation(self) -> bool:
- "A preloop or postloop hook with incorrect return type"
+ """A preloop or postloop hook with incorrect return type"""
pass
###
@@ -55,10 +51,10 @@ class Plugin:
# preparse hook
#
###
- def preparse(self, line: str) -> str:
- "Preparsing hook"
+ def preparse(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
+ """Preparsing hook"""
self.called_preparse += 1
- return line
+ return data
###
#
@@ -66,44 +62,44 @@ class Plugin:
#
###
def postparse_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
- "A postparsing hook"
+ """A postparsing hook"""
self.called_postparsing += 1
return data
def postparse_hook_stop(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
- "A postparsing hook with requests application exit"
+ """A postparsing hook with requests application exit"""
self.called_postparsing += 1
data.stop = True
return data
def postparse_hook_emptystatement(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
- "A postparsing hook with raises an EmptyStatement exception"
+ """A postparsing hook with raises an EmptyStatement exception"""
self.called_postparsing += 1
raise cmd2.EmptyStatement
def postparse_hook_exception(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
- "A postparsing hook which raises an exception"
+ """A postparsing hook which raises an exception"""
self.called_postparsing += 1
raise ValueError
def postparse_hook_too_many_parameters(self, data1, data2) -> cmd2.plugin.PostparsingData:
- "A postparsing hook with too many parameters"
+ """A postparsing hook with too many parameters"""
pass
def postparse_hook_undeclared_parameter_annotation(self, data) -> cmd2.plugin.PostparsingData:
- "A postparsing hook with an undeclared parameter type"
+ """A postparsing hook with an undeclared parameter type"""
pass
def postparse_hook_wrong_parameter_annotation(self, data: str) -> cmd2.plugin.PostparsingData:
- "A postparsing hook with the wrong parameter type"
+ """A postparsing hook with the wrong parameter type"""
pass
def postparse_hook_undeclared_return_annotation(self, data: cmd2.plugin.PostparsingData):
- "A postparsing hook with an undeclared return type"
+ """A postparsing hook with an undeclared return type"""
pass
def postparse_hook_wrong_return_annotation(self, data: cmd2.plugin.PostparsingData) -> str:
- "A postparsing hook with the wrong return type"
+ """A postparsing hook with the wrong return type"""
pass
###
@@ -112,43 +108,43 @@ class Plugin:
#
###
def precmd(self, statement: cmd2.Statement) -> cmd2.Statement:
- "Override cmd.Cmd method"
+ """Override cmd.Cmd method"""
self.called_precmd += 1
return statement
def precmd_hook(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
- "A precommand hook"
+ """A precommand hook"""
self.called_precmd += 1
return data
def precmd_hook_emptystatement(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
- "A precommand hook which raises an EmptyStatement exception"
+ """A precommand hook which raises an EmptyStatement exception"""
self.called_precmd += 1
raise cmd2.EmptyStatement
def precmd_hook_exception(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
- "A precommand hook which raises an exception"
+ """A precommand hook which raises an exception"""
self.called_precmd += 1
raise ValueError
def precmd_hook_not_enough_parameters(self) -> plugin.PrecommandData:
- "A precommand hook with no parameters"
+ """A precommand hook with no parameters"""
pass
def precmd_hook_too_many_parameters(self, one: plugin.PrecommandData, two: str) -> plugin.PrecommandData:
- "A precommand hook with too many parameters"
+ """A precommand hook with too many parameters"""
return one
def precmd_hook_no_parameter_annotation(self, data) -> plugin.PrecommandData:
- "A precommand hook with no type annotation on the parameter"
+ """A precommand hook with no type annotation on the parameter"""
return data
def precmd_hook_wrong_parameter_annotation(self, data: str) -> plugin.PrecommandData:
- "A precommand hook with the incorrect type annotation on the parameter"
+ """A precommand hook with the incorrect type annotation on the parameter"""
return data
def precmd_hook_no_return_annotation(self, data: plugin.PrecommandData):
- "A precommand hook with no type annotation on the return value"
+ """A precommand hook with no type annotation on the return value"""
return data
def precmd_hook_wrong_return_annotation(self, data: plugin.PrecommandData) -> cmd2.Statement:
@@ -160,38 +156,38 @@ class Plugin:
#
###
def postcmd(self, stop: bool, statement: cmd2.Statement) -> bool:
- "Override cmd.Cmd method"
+ """Override cmd.Cmd method"""
self.called_postcmd += 1
return stop
def postcmd_hook(self, data: plugin.PostcommandData) -> plugin.PostcommandData:
- "A postcommand hook"
+ """A postcommand hook"""
self.called_postcmd += 1
return data
def postcmd_hook_exception(self, data: plugin.PostcommandData) -> plugin.PostcommandData:
- "A postcommand hook with raises an exception"
+ """A postcommand hook with raises an exception"""
self.called_postcmd += 1
raise ZeroDivisionError
def postcmd_hook_not_enough_parameters(self) -> plugin.PostcommandData:
- "A precommand hook with no parameters"
+ """A precommand hook with no parameters"""
pass
def postcmd_hook_too_many_parameters(self, one: plugin.PostcommandData, two: str) -> plugin.PostcommandData:
- "A precommand hook with too many parameters"
+ """A precommand hook with too many parameters"""
return one
def postcmd_hook_no_parameter_annotation(self, data) -> plugin.PostcommandData:
- "A precommand hook with no type annotation on the parameter"
+ """A precommand hook with no type annotation on the parameter"""
return data
def postcmd_hook_wrong_parameter_annotation(self, data: str) -> plugin.PostcommandData:
- "A precommand hook with the incorrect type annotation on the parameter"
+ """A precommand hook with the incorrect type annotation on the parameter"""
return data
def postcmd_hook_no_return_annotation(self, data: plugin.PostcommandData):
- "A precommand hook with no type annotation on the return value"
+ """A precommand hook with no type annotation on the return value"""
return data
def postcmd_hook_wrong_return_annotation(self, data: plugin.PostcommandData) -> cmd2.Statement:
@@ -208,13 +204,13 @@ class Plugin:
return data
def cmdfinalization_hook_stop(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData:
- "A postparsing hook which requests application exit"
+ """A postparsing hook which requests application exit"""
self.called_cmdfinalization += 1
data.stop = True
return data
def cmdfinalization_hook_exception(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData:
- "A postparsing hook which raises an exception"
+ """A postparsing hook which raises an exception"""
self.called_cmdfinalization += 1
raise ValueError
@@ -244,7 +240,7 @@ class Plugin:
class PluggedApp(Plugin, cmd2.Cmd):
- "A sample app with a plugin mixed in"
+ """A sample app with a plugin mixed in"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -326,6 +322,7 @@ def test_postloop_hooks(capsys):
###
def test_preparse(capsys):
app = PluggedApp()
+ app.register_postparsing_hook(app.preparse)
app.onecmd_plus_hooks('say hello')
out, err = capsys.readouterr()
assert out == 'hello\n'
diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py
index 73c1a62a..d5e5a4fb 100644
--- a/tests/test_pyscript.py
+++ b/tests/test_pyscript.py
@@ -1,3 +1,4 @@
+# coding=utf-8
"""
Unit/functional testing for argparse completer in cmd2
@@ -8,26 +9,27 @@ import os
import pytest
from cmd2.cmd2 import Cmd, with_argparser
from cmd2 import argparse_completer
-from .conftest import run_cmd, StdOut
-from cmd2.utils import namedtuple_with_defaults
+from .conftest import run_cmd
+from cmd2.utils import namedtuple_with_defaults, StdSim
+
class PyscriptExample(Cmd):
ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17']
def _do_media_movies(self, args) -> None:
if not args.command:
- self.do_help('media movies')
+ self.do_help(['media movies'])
else:
- print('media movies ' + str(args.__dict__))
+ self.poutput('media movies ' + str(args.__dict__))
def _do_media_shows(self, args) -> None:
if not args.command:
- self.do_help('media shows')
+ self.do_help(['media shows'])
if not args.command:
- self.do_help('media shows')
+ self.do_help(['media shows'])
else:
- print('media shows ' + str(args.__dict__))
+ self.poutput('media shows ' + str(args.__dict__))
media_parser = argparse_completer.ACArgumentParser(prog='media')
@@ -70,7 +72,7 @@ class PyscriptExample(Cmd):
func(self, args)
else:
# No subcommand was provided, so call help
- self.do_help('media')
+ self.do_help(['media'])
foo_parser = argparse_completer.ACArgumentParser(prog='foo')
foo_parser.add_argument('-c', dest='counter', action='count')
@@ -82,7 +84,7 @@ class PyscriptExample(Cmd):
@with_argparser(foo_parser)
def do_foo(self, args):
- print('foo ' + str(args.__dict__))
+ self.poutput('foo ' + str(args.__dict__))
if self._in_py:
FooResult = namedtuple_with_defaults('FooResult',
['counter', 'trueval', 'constval',
@@ -108,14 +110,13 @@ class PyscriptExample(Cmd):
out += '{'
for key in keys:
out += "'{}':'{}'".format(key, arg_dict[key])
- print(out)
+ self.poutput(out)
@pytest.fixture
def ps_app():
c = PyscriptExample()
- c.stdout = StdOut()
-
+ c.stdout = StdSim(c.stdout)
return c
@@ -125,14 +126,13 @@ class PyscriptCustomNameExample(Cmd):
self.pyscript_name = 'custom'
def do_echo(self, out):
- print(out)
+ self.poutput(out)
@pytest.fixture
def ps_echo():
c = PyscriptCustomNameExample()
- c.stdout = StdOut()
-
+ c.stdout = StdSim(c.stdout)
return c
@@ -140,7 +140,7 @@ def ps_echo():
('help', 'help.py'),
('help media', 'help_media.py'),
])
-def test_pyscript_help(ps_app, capsys, request, command, pyscript_file):
+def test_pyscript_help(ps_app, request, command, pyscript_file):
test_dir = os.path.dirname(request.module.__file__)
python_script = os.path.join(test_dir, 'pyscript', pyscript_file)
expected = run_cmd(ps_app, command)
@@ -169,16 +169,14 @@ def test_pyscript_help(ps_app, capsys, request, command, pyscript_file):
('foo 11 22 33 44 55 66 -ccc', 'foo3.py'),
('bar 11 22', 'bar1.py'),
])
-def test_pyscript_out(ps_app, capsys, request, command, pyscript_file):
+def test_pyscript_out(ps_app, request, command, pyscript_file):
test_dir = os.path.dirname(request.module.__file__)
python_script = os.path.join(test_dir, 'pyscript', pyscript_file)
- run_cmd(ps_app, command)
- expected, _ = capsys.readouterr()
+ expected = run_cmd(ps_app, command)
+ assert expected
- assert len(expected) > 0
- run_cmd(ps_app, 'pyscript {}'.format(python_script))
- out, _ = capsys.readouterr()
- assert len(out) > 0
+ out = run_cmd(ps_app, 'pyscript {}'.format(python_script))
+ assert out
assert out == expected
@@ -227,14 +225,12 @@ def test_pyscript_dir(ps_app, capsys, request, expected, pyscript_file):
assert out == expected
-def test_pyscript_custom_name(ps_echo, capsys, request):
+def test_pyscript_custom_name(ps_echo, request):
message = 'blah!'
test_dir = os.path.dirname(request.module.__file__)
python_script = os.path.join(test_dir, 'pyscript', 'custom_echo.py')
- run_cmd(ps_echo, 'pyscript {}'.format(python_script))
- expected, _ = capsys.readouterr()
- assert len(expected) > 0
- expected = expected.splitlines()
- assert message == expected[0]
+ out = run_cmd(ps_echo, 'pyscript {}'.format(python_script))
+ assert out
+ assert message == out[0]
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 3caf6a37..f854241b 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -16,8 +16,10 @@ from unittest import mock
import pytest
import cmd2
-from .conftest import run_cmd, StdOut
+from .conftest import run_cmd
from cmd2 import transcript
+from cmd2.utils import StdSim
+
class CmdLineApp(cmd2.Cmd):
@@ -83,9 +85,9 @@ def test_commands_at_invocation():
expected = "This is an intro banner ...\nhello\nGracie\n"
with mock.patch.object(sys, 'argv', testargs):
app = CmdLineApp()
- app.stdout = StdOut()
+ app.stdout = StdSim(app.stdout)
app.cmdloop()
- out = app.stdout.buffer
+ out = app.stdout.getvalue()
assert out == expected
@pytest.mark.parametrize('filename,feedback_to_output', [
@@ -129,7 +131,7 @@ def test_transcript(request, capsys, filename, feedback_to_output):
def test_history_transcript(request, capsys):
app = CmdLineApp()
- app.stdout = StdOut()
+ app.stdout = StdSim(app.stdout)
run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
run_cmd(app, 'speak /tmp/file.txt is not a regex')
@@ -150,13 +152,13 @@ this is a \/multiline\/ command
# read in the transcript created by the history command
with open(history_fname) as f:
- transcript = f.read()
+ xscript = f.read()
- assert transcript == expected
+ assert xscript == expected
def test_history_transcript_bad_filename(request, capsys):
app = CmdLineApp()
- app.stdout = StdOut()
+ app.stdout = StdSim(app.stdout)
run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
run_cmd(app, 'speak /tmp/file.txt is not a regex')
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 8c8daa39..53031567 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -5,6 +5,10 @@ Unit testing for cmd2/utils.py module.
Copyright 2018 Todd Leonhardt <todd.leonhardt@gmail.com>
Released under MIT license, see LICENSE file
"""
+import sys
+
+import pytest
+
from colorama import Fore
import cmd2.utils as cu
@@ -110,4 +114,55 @@ def test_quot_string_if_needed_no():
assert cu.quote_string_if_needed(your_str) == your_str
+@pytest.fixture
+def stdout_sim():
+ stdsim = cu.StdSim(sys.stdout)
+ return stdsim
+
+def test_stdsim_write_str(stdout_sim):
+ my_str = 'Hello World'
+ stdout_sim.write(my_str)
+ assert stdout_sim.getvalue() == my_str
+
+def test_stdsim_write_bytes(stdout_sim):
+ b_str = b'Hello World'
+ with pytest.raises(TypeError):
+ stdout_sim.write(b_str)
+
+def test_stdsim_buffer_write_bytes(stdout_sim):
+ b_str = b'Hello World'
+ stdout_sim.buffer.write(b_str)
+ assert stdout_sim.getvalue() == b_str.decode()
+
+def test_stdsim_buffer_write_str(stdout_sim):
+ my_str = 'Hello World'
+ with pytest.raises(TypeError):
+ stdout_sim.buffer.write(my_str)
+
+def test_stdsim_read(stdout_sim):
+ my_str = 'Hello World'
+ stdout_sim.write(my_str)
+ # getvalue() returns the value and leaves it unaffected internally
+ assert stdout_sim.getvalue() == my_str
+ # read() returns the value and then clears the internal buffer
+ assert stdout_sim.read() == my_str
+ assert stdout_sim.getvalue() == ''
+
+def test_stdsim_clear(stdout_sim):
+ my_str = 'Hello World'
+ stdout_sim.write(my_str)
+ assert stdout_sim.getvalue() == my_str
+ stdout_sim.clear()
+ assert stdout_sim.getvalue() == ''
+
+def test_stdsim_getattr_exist(stdout_sim):
+ # Here the StdSim getattr is allowing us to access methods within StdSim
+ my_str = 'Hello World'
+ stdout_sim.write(my_str)
+ val_func = getattr(stdout_sim, 'getvalue')
+ assert val_func() == my_str
+
+def test_stdsim_getattr_noexist(stdout_sim):
+ # Here the StdSim getattr is allowing us to access methods defined by the inner stream
+ assert not stdout_sim.isatty()