summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2018-01-22 00:08:27 -0500
committerTodd Leonhardt <todd.leonhardt@gmail.com>2018-01-22 00:08:27 -0500
commitf28c10a50535f753419bd2120ac6cb0bea9f56e2 (patch)
tree102b200c4a3dff1adad4bf326ea3f6de9cd56baf
parentc9f7c012bda012b4df7a8c5e853bd5d3e6d99b1b (diff)
downloadcmd2-git-f28c10a50535f753419bd2120ac6cb0bea9f56e2.tar.gz
help command temporarily redirects sys.stdout and sys.stderr to self.stdout for argparse commands
In order to make "help" behave more consistently for decorated and undecorated commands, argparse output is temporarily redirected to self.stdout. So doing "help history" is similar to "help load". However, when using the "-h" with argparse commands without using the "help" command, the output from argparse isn't redirected to self.stdout. Fixing this would be rather difficult and would essentially involve creating a pyparsing rule to detect it at the parser level.
-rwxr-xr-xcmd2.py34
-rwxr-xr-xsetup.py6
-rw-r--r--tests/test_argparse.py40
-rw-r--r--tests/test_cmd2.py30
-rw-r--r--tests/test_completion.py6
5 files changed, 58 insertions, 58 deletions
diff --git a/cmd2.py b/cmd2.py
index aee6f958..c397bb7c 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -92,8 +92,12 @@ except ImportError:
# BrokenPipeError is only in Python 3. Use IOError for Python 2.
if six.PY3:
BROKEN_PIPE_ERROR = BrokenPipeError
+
+ # redirect_stdout and redirect_stderr weren't added to contextlib until Python 3.4
+ from contextlib import redirect_stdout, redirect_stderr
else:
BROKEN_PIPE_ERROR = IOError
+ from contextlib2 import redirect_stdout, redirect_stderr
# On some systems, pyperclip will import gtk for its clipboard functionality.
# The following code is a workaround for gtk interfering with printing from a background
@@ -668,6 +672,9 @@ class Cmd(cmd.Cmd):
# Used when piping command output to a shell command
self.pipe_proc = None
+ # Used by complete() for readline tab completion
+ self.completion_matches = []
+
# ----- Methods related to presenting output to the user -----
@property
@@ -771,13 +778,14 @@ class Cmd(cmd.Cmd):
return cmd_completion
+ # noinspection PyUnusedLocal
def complete_subcommand(self, text, line, begidx, endidx):
"""Readline tab-completion method for completing argparse sub-command names."""
- cmd, args, foo = self.parseline(line)
+ command, args, foo = self.parseline(line)
arglist = args.split()
- if len(arglist) <= 1 and cmd + ' ' + args == line:
- funcname = self._func_named(cmd)
+ if len(arglist) <= 1 and command + ' ' + args == line:
+ funcname = self._func_named(command)
if funcname:
# Check to see if this function was decorated with an argparse ArgumentParser
func = getattr(self, funcname)
@@ -799,7 +807,7 @@ class Cmd(cmd.Cmd):
return []
def complete(self, text, state):
- """Override of cmd method which returns the next possible completion for 'text'.
+ """Override of command method which returns the next possible completion for 'text'.
If a command has not been entered, then complete against command list.
Otherwise try to call complete_<command> to get list of completions.
@@ -819,17 +827,17 @@ class Cmd(cmd.Cmd):
stripped = len(origline) - len(line)
begidx = readline.get_begidx() - stripped
endidx = readline.get_endidx() - stripped
- if begidx>0:
- cmd, args, foo = self.parseline(line)
- if cmd == '':
+ if begidx > 0:
+ command, args, foo = self.parseline(line)
+ if command == '':
compfunc = self.completedefault
else:
arglist = args.split()
compfunc = None
# If the user has entered no more than a single argument after the command name
- if len(arglist) <= 1 and cmd + ' ' + args == line:
- funcname = self._func_named(cmd)
+ if len(arglist) <= 1 and command + ' ' + args == line:
+ funcname = self._func_named(command)
if funcname:
# Check to see if this function was decorated with an argparse ArgumentParser
func = getattr(self, funcname)
@@ -842,7 +850,7 @@ class Cmd(cmd.Cmd):
if compfunc is None:
# This command either doesn't have sub-commands or the user is past the point of entering one
try:
- compfunc = getattr(self, 'complete_' + cmd)
+ compfunc = getattr(self, 'complete_' + command)
except AttributeError:
compfunc = self.completedefault
else:
@@ -1319,7 +1327,11 @@ class Cmd(cmd.Cmd):
# Function has an argparser, so get help based on all the arguments in case there are sub-commands
new_arglist = arglist[1:]
new_arglist.append('-h')
- func(new_arglist)
+
+ # Temporarily redirect all argparse output to both sys.stdout and sys.stderr to self.stdout
+ with redirect_stdout(self.stdout):
+ with redirect_stderr(self.stdout):
+ func(new_arglist)
else:
# No special behavior needed, delegate to cmd base class do_help()
cmd.Cmd.do_help(self, funcname[3:])
diff --git a/setup.py b/setup.py
index f56555db..aea5ab92 100755
--- a/setup.py
+++ b/setup.py
@@ -62,9 +62,15 @@ Topic :: Software Development :: Libraries :: Python Modules
""".splitlines())))
INSTALL_REQUIRES = ['pyparsing >= 2.0.1', 'pyperclip', 'six']
+
+# Windows also requires pyreadline to ensure tab completion works
if sys.platform.startswith('win'):
INSTALL_REQUIRES += ['pyreadline']
+# Python 2.7 also requires contextlib2 for temporarily redirecting stdout and stderr and subprocess32
+if sys.version_info < (3, 0):
+ INSTALL_REQUIRES += ['contextlib2', 'subprocess32']
+
# unittest.mock was added in Python 3.3. mock is a backport of unittest.mock to all versions of Python
TESTS_REQUIRE = ['mock', 'pytest']
DOCS_REQUIRE = ['sphinx', 'sphinx_rtd_theme', 'pyparsing', 'pyperclip', 'six']
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index ecaa1049..d3646046 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -139,26 +139,20 @@ def test_argparse_quoted_arguments_posix_multiple(argparse_app):
out = run_cmd(argparse_app, 'tag strong this "should be" loud')
assert out == ['<strong>this should be loud</strong>']
-def test_argparse_help_docstring(argparse_app, capsys):
- run_cmd(argparse_app, 'help say')
- out, err = capsys.readouterr()
- out = out.splitlines()
+def test_argparse_help_docstring(argparse_app):
+ out = run_cmd(argparse_app, 'help say')
assert out[0].startswith('usage: say')
assert out[1] == ''
assert out[2] == 'Repeat what you tell me to.'
-def test_argparse_help_description(argparse_app, capsys):
- run_cmd(argparse_app, 'help tag')
- out, err = capsys.readouterr()
- out = out.splitlines()
+def test_argparse_help_description(argparse_app):
+ out = run_cmd(argparse_app, 'help tag')
assert out[0].startswith('usage: tag')
assert out[1] == ''
assert out[2] == 'create a html tag'
-def test_argparse_prog(argparse_app, capsys):
- run_cmd(argparse_app, 'help tag')
- out, err = capsys.readouterr()
- out = out.splitlines()
+def test_argparse_prog(argparse_app):
+ out = run_cmd(argparse_app, 'help tag')
progname = out[0].split(' ')[1]
assert progname == 'tag'
@@ -237,26 +231,20 @@ def test_subcommand_invalid(subcommand_app, capsys):
assert err[0].startswith('usage: base')
assert err[1].startswith("base: error: invalid choice: 'baz'")
-def test_subcommand_base_help(subcommand_app, capsys):
- run_cmd(subcommand_app, 'help base')
- out, err = capsys.readouterr()
- out = out.splitlines()
+def test_subcommand_base_help(subcommand_app):
+ out = run_cmd(subcommand_app, 'help base')
assert out[0].startswith('usage: base')
assert out[1] == ''
assert out[2] == 'Base command help'
-def test_subcommand_help(subcommand_app, capsys):
- run_cmd(subcommand_app, 'help base foo')
- out, err = capsys.readouterr()
- out = out.splitlines()
+def test_subcommand_help(subcommand_app):
+ out = run_cmd(subcommand_app, 'help base foo')
assert out[0].startswith('usage: base foo')
assert out[1] == ''
assert out[2] == 'positional arguments:'
-def test_subcommand_invalid_help(subcommand_app, capsys):
- run_cmd(subcommand_app, 'help base baz')
- out, err = capsys.readouterr()
- err = err.splitlines()
- assert err[0].startswith('usage: base')
- assert err[1].startswith("base: error: invalid choice: 'baz'")
+def test_subcommand_invalid_help(subcommand_app):
+ out = run_cmd(subcommand_app, 'help base baz')
+ assert out[0].startswith('usage: base')
+ assert out[1].startswith("base: error: invalid choice: 'baz'")
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 9fb9f2d9..186def65 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -39,26 +39,22 @@ def test_base_help(base_app):
assert out == expected
-def test_base_help_history(base_app, capsys):
- run_cmd(base_app, 'help history')
- out, err = capsys.readouterr()
- assert out == HELP_HISTORY
- assert err == ''
+def test_base_help_history(base_app):
+ out = run_cmd(base_app, 'help history')
+ assert out == normalize(HELP_HISTORY)
def test_base_argparse_help(base_app, capsys):
# Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense
run_cmd(base_app, 'set -h')
- out1, err1 = capsys.readouterr()
+ out, err = capsys.readouterr()
+ out1 = out.splitlines()
- run_cmd(base_app, 'help set')
- out2, err2 = capsys.readouterr()
+ out2 = run_cmd(base_app, 'help set')
assert out1 == out2
- assert err1 == err2
- out = out1.splitlines()
- assert out[0].startswith('usage: set')
- assert out[1] == ''
- assert out[2].startswith('Sets a settable parameter')
+ assert out1[0].startswith('usage: set')
+ assert out1[1] == ''
+ assert out1[2].startswith('Sets a settable parameter')
def test_base_invalid_option(base_app, capsys):
run_cmd(base_app, 'set -z')
@@ -606,17 +602,15 @@ def test_allow_redirection(base_app):
assert not os.path.exists(filename)
-def test_input_redirection(base_app, request, capsys):
+def test_input_redirection(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
filename = os.path.join(test_dir, 'redirect.txt')
# NOTE: File 'redirect.txt" contains 1 word "history"
# Verify that redirecting input ffom a file works
- run_cmd(base_app, 'help < {}'.format(filename))
- out, err = capsys.readouterr()
- assert out == HELP_HISTORY
- assert err == ''
+ out = run_cmd(base_app, 'help < {}'.format(filename))
+ assert out == normalize(HELP_HISTORY)
def test_pipe_to_shell(base_app, capsys):
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 05774df9..70f77d0a 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -80,9 +80,9 @@ def test_complete_command_invalid_state(cmd2_app):
with mock.patch.object(readline, 'get_line_buffer', get_line):
with mock.patch.object(readline, 'get_begidx', get_begidx):
with mock.patch.object(readline, 'get_endidx', get_endidx):
- with pytest.raises(AttributeError):
- # Run the readline tab-completion function with readline mocks in place and cause an exception
- completion = cmd2_app.complete(text, state)
+ # Run the readline tab-completion function with readline mocks in place get None
+ completion = cmd2_app.complete(text, state)
+ assert completion is None
def test_complete_empty_arg(cmd2_app):
text = ''