diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2018-09-25 18:16:22 -0400 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2018-09-25 18:16:22 -0400 |
commit | 45d2398b3023543ed1615115db5d35b1fd65dc5b (patch) | |
tree | c2af42bab5f3aacdd507dcaea801581a306d0a92 | |
parent | 7cc94ab98c007210b344fbb1af965570fae63959 (diff) | |
parent | dac03119e5932f8a34a1318122e2f45fa875084c (diff) | |
download | cmd2-git-45d2398b3023543ed1615115db5d35b1fd65dc5b.tar.gz |
Merge branch 'master' into command_help_noflag
-rw-r--r-- | CHANGELOG.md | 5 | ||||
-rwxr-xr-x | README.md | 4 | ||||
-rw-r--r-- | cmd2.png | bin | 294386 -> 312332 bytes | |||
-rw-r--r-- | cmd2/cmd2.py | 92 | ||||
-rw-r--r-- | cmd2/pyscript_bridge.py | 41 | ||||
-rw-r--r-- | cmd2/transcript.py | 23 | ||||
-rw-r--r-- | cmd2/utils.py | 58 | ||||
-rw-r--r-- | docs/hooks.rst | 51 | ||||
-rw-r--r-- | docs/integrating.rst | 29 | ||||
-rwxr-xr-x | examples/python_scripting.py | 2 | ||||
-rw-r--r-- | tests/conftest.py | 22 | ||||
-rw-r--r-- | tests/test_argparse.py | 10 | ||||
-rw-r--r-- | tests/test_autocompletion.py | 12 | ||||
-rw-r--r-- | tests/test_cmd2.py | 95 | ||||
-rw-r--r-- | tests/test_completion.py | 14 | ||||
-rw-r--r-- | tests/test_plugin.py | 79 | ||||
-rw-r--r-- | tests/test_pyscript.py | 54 | ||||
-rw-r--r-- | tests/test_transcript.py | 16 | ||||
-rw-r--r-- | tests/test_utils.py | 55 |
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 @@ -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. -[](https://github.com/python-cmd2/cmd2/blob/master/cmd2.png) - +Click on image below to watch a short video demonstrating the capabilities of cmd2: +[](https://youtu.be/DDU_JH6cFsA) Main Features ------------- Binary files differdiff --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() |