Skip to content

Commit f28c10a

Browse files
committed
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.
1 parent c9f7c01 commit f28c10a

File tree

5 files changed

+58
-58
lines changed

5 files changed

+58
-58
lines changed

cmd2.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,12 @@
9292
# BrokenPipeError is only in Python 3. Use IOError for Python 2.
9393
if six.PY3:
9494
BROKEN_PIPE_ERROR = BrokenPipeError
95+
96+
# redirect_stdout and redirect_stderr weren't added to contextlib until Python 3.4
97+
from contextlib import redirect_stdout, redirect_stderr
9598
else:
9699
BROKEN_PIPE_ERROR = IOError
100+
from contextlib2 import redirect_stdout, redirect_stderr
97101

98102
# On some systems, pyperclip will import gtk for its clipboard functionality.
99103
# The following code is a workaround for gtk interfering with printing from a background
@@ -668,6 +672,9 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
668672
# Used when piping command output to a shell command
669673
self.pipe_proc = None
670674

675+
# Used by complete() for readline tab completion
676+
self.completion_matches = []
677+
671678
# ----- Methods related to presenting output to the user -----
672679

673680
@property
@@ -771,13 +778,14 @@ def completenames(self, text, line, begidx, endidx):
771778

772779
return cmd_completion
773780

781+
# noinspection PyUnusedLocal
774782
def complete_subcommand(self, text, line, begidx, endidx):
775783
"""Readline tab-completion method for completing argparse sub-command names."""
776-
cmd, args, foo = self.parseline(line)
784+
command, args, foo = self.parseline(line)
777785
arglist = args.split()
778786

779-
if len(arglist) <= 1 and cmd + ' ' + args == line:
780-
funcname = self._func_named(cmd)
787+
if len(arglist) <= 1 and command + ' ' + args == line:
788+
funcname = self._func_named(command)
781789
if funcname:
782790
# Check to see if this function was decorated with an argparse ArgumentParser
783791
func = getattr(self, funcname)
@@ -799,7 +807,7 @@ def complete_subcommand(self, text, line, begidx, endidx):
799807
return []
800808

801809
def complete(self, text, state):
802-
"""Override of cmd method which returns the next possible completion for 'text'.
810+
"""Override of command method which returns the next possible completion for 'text'.
803811
804812
If a command has not been entered, then complete against command list.
805813
Otherwise try to call complete_<command> to get list of completions.
@@ -819,17 +827,17 @@ def complete(self, text, state):
819827
stripped = len(origline) - len(line)
820828
begidx = readline.get_begidx() - stripped
821829
endidx = readline.get_endidx() - stripped
822-
if begidx>0:
823-
cmd, args, foo = self.parseline(line)
824-
if cmd == '':
830+
if begidx > 0:
831+
command, args, foo = self.parseline(line)
832+
if command == '':
825833
compfunc = self.completedefault
826834
else:
827835
arglist = args.split()
828836

829837
compfunc = None
830838
# If the user has entered no more than a single argument after the command name
831-
if len(arglist) <= 1 and cmd + ' ' + args == line:
832-
funcname = self._func_named(cmd)
839+
if len(arglist) <= 1 and command + ' ' + args == line:
840+
funcname = self._func_named(command)
833841
if funcname:
834842
# Check to see if this function was decorated with an argparse ArgumentParser
835843
func = getattr(self, funcname)
@@ -842,7 +850,7 @@ def complete(self, text, state):
842850
if compfunc is None:
843851
# This command either doesn't have sub-commands or the user is past the point of entering one
844852
try:
845-
compfunc = getattr(self, 'complete_' + cmd)
853+
compfunc = getattr(self, 'complete_' + command)
846854
except AttributeError:
847855
compfunc = self.completedefault
848856
else:
@@ -1319,7 +1327,11 @@ def do_help(self, arglist):
13191327
# Function has an argparser, so get help based on all the arguments in case there are sub-commands
13201328
new_arglist = arglist[1:]
13211329
new_arglist.append('-h')
1322-
func(new_arglist)
1330+
1331+
# Temporarily redirect all argparse output to both sys.stdout and sys.stderr to self.stdout
1332+
with redirect_stdout(self.stdout):
1333+
with redirect_stderr(self.stdout):
1334+
func(new_arglist)
13231335
else:
13241336
# No special behavior needed, delegate to cmd base class do_help()
13251337
cmd.Cmd.do_help(self, funcname[3:])

setup.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,15 @@
6262
""".splitlines())))
6363

6464
INSTALL_REQUIRES = ['pyparsing >= 2.0.1', 'pyperclip', 'six']
65+
66+
# Windows also requires pyreadline to ensure tab completion works
6567
if sys.platform.startswith('win'):
6668
INSTALL_REQUIRES += ['pyreadline']
6769

70+
# Python 2.7 also requires contextlib2 for temporarily redirecting stdout and stderr and subprocess32
71+
if sys.version_info < (3, 0):
72+
INSTALL_REQUIRES += ['contextlib2', 'subprocess32']
73+
6874
# unittest.mock was added in Python 3.3. mock is a backport of unittest.mock to all versions of Python
6975
TESTS_REQUIRE = ['mock', 'pytest']
7076
DOCS_REQUIRE = ['sphinx', 'sphinx_rtd_theme', 'pyparsing', 'pyperclip', 'six']

tests/test_argparse.py

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -139,26 +139,20 @@ def test_argparse_quoted_arguments_posix_multiple(argparse_app):
139139
out = run_cmd(argparse_app, 'tag strong this "should be" loud')
140140
assert out == ['<strong>this should be loud</strong>']
141141

142-
def test_argparse_help_docstring(argparse_app, capsys):
143-
run_cmd(argparse_app, 'help say')
144-
out, err = capsys.readouterr()
145-
out = out.splitlines()
142+
def test_argparse_help_docstring(argparse_app):
143+
out = run_cmd(argparse_app, 'help say')
146144
assert out[0].startswith('usage: say')
147145
assert out[1] == ''
148146
assert out[2] == 'Repeat what you tell me to.'
149147

150-
def test_argparse_help_description(argparse_app, capsys):
151-
run_cmd(argparse_app, 'help tag')
152-
out, err = capsys.readouterr()
153-
out = out.splitlines()
148+
def test_argparse_help_description(argparse_app):
149+
out = run_cmd(argparse_app, 'help tag')
154150
assert out[0].startswith('usage: tag')
155151
assert out[1] == ''
156152
assert out[2] == 'create a html tag'
157153

158-
def test_argparse_prog(argparse_app, capsys):
159-
run_cmd(argparse_app, 'help tag')
160-
out, err = capsys.readouterr()
161-
out = out.splitlines()
154+
def test_argparse_prog(argparse_app):
155+
out = run_cmd(argparse_app, 'help tag')
162156
progname = out[0].split(' ')[1]
163157
assert progname == 'tag'
164158

@@ -237,26 +231,20 @@ def test_subcommand_invalid(subcommand_app, capsys):
237231
assert err[0].startswith('usage: base')
238232
assert err[1].startswith("base: error: invalid choice: 'baz'")
239233

240-
def test_subcommand_base_help(subcommand_app, capsys):
241-
run_cmd(subcommand_app, 'help base')
242-
out, err = capsys.readouterr()
243-
out = out.splitlines()
234+
def test_subcommand_base_help(subcommand_app):
235+
out = run_cmd(subcommand_app, 'help base')
244236
assert out[0].startswith('usage: base')
245237
assert out[1] == ''
246238
assert out[2] == 'Base command help'
247239

248-
def test_subcommand_help(subcommand_app, capsys):
249-
run_cmd(subcommand_app, 'help base foo')
250-
out, err = capsys.readouterr()
251-
out = out.splitlines()
240+
def test_subcommand_help(subcommand_app):
241+
out = run_cmd(subcommand_app, 'help base foo')
252242
assert out[0].startswith('usage: base foo')
253243
assert out[1] == ''
254244
assert out[2] == 'positional arguments:'
255245

256246

257-
def test_subcommand_invalid_help(subcommand_app, capsys):
258-
run_cmd(subcommand_app, 'help base baz')
259-
out, err = capsys.readouterr()
260-
err = err.splitlines()
261-
assert err[0].startswith('usage: base')
262-
assert err[1].startswith("base: error: invalid choice: 'baz'")
247+
def test_subcommand_invalid_help(subcommand_app):
248+
out = run_cmd(subcommand_app, 'help base baz')
249+
assert out[0].startswith('usage: base')
250+
assert out[1].startswith("base: error: invalid choice: 'baz'")

tests/test_cmd2.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,26 +39,22 @@ def test_base_help(base_app):
3939
assert out == expected
4040

4141

42-
def test_base_help_history(base_app, capsys):
43-
run_cmd(base_app, 'help history')
44-
out, err = capsys.readouterr()
45-
assert out == HELP_HISTORY
46-
assert err == ''
42+
def test_base_help_history(base_app):
43+
out = run_cmd(base_app, 'help history')
44+
assert out == normalize(HELP_HISTORY)
4745

4846
def test_base_argparse_help(base_app, capsys):
4947
# Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense
5048
run_cmd(base_app, 'set -h')
51-
out1, err1 = capsys.readouterr()
49+
out, err = capsys.readouterr()
50+
out1 = out.splitlines()
5251

53-
run_cmd(base_app, 'help set')
54-
out2, err2 = capsys.readouterr()
52+
out2 = run_cmd(base_app, 'help set')
5553

5654
assert out1 == out2
57-
assert err1 == err2
58-
out = out1.splitlines()
59-
assert out[0].startswith('usage: set')
60-
assert out[1] == ''
61-
assert out[2].startswith('Sets a settable parameter')
55+
assert out1[0].startswith('usage: set')
56+
assert out1[1] == ''
57+
assert out1[2].startswith('Sets a settable parameter')
6258

6359
def test_base_invalid_option(base_app, capsys):
6460
run_cmd(base_app, 'set -z')
@@ -606,17 +602,15 @@ def test_allow_redirection(base_app):
606602
assert not os.path.exists(filename)
607603

608604

609-
def test_input_redirection(base_app, request, capsys):
605+
def test_input_redirection(base_app, request):
610606
test_dir = os.path.dirname(request.module.__file__)
611607
filename = os.path.join(test_dir, 'redirect.txt')
612608

613609
# NOTE: File 'redirect.txt" contains 1 word "history"
614610

615611
# Verify that redirecting input ffom a file works
616-
run_cmd(base_app, 'help < {}'.format(filename))
617-
out, err = capsys.readouterr()
618-
assert out == HELP_HISTORY
619-
assert err == ''
612+
out = run_cmd(base_app, 'help < {}'.format(filename))
613+
assert out == normalize(HELP_HISTORY)
620614

621615

622616
def test_pipe_to_shell(base_app, capsys):

tests/test_completion.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ def get_endidx():
8080
with mock.patch.object(readline, 'get_line_buffer', get_line):
8181
with mock.patch.object(readline, 'get_begidx', get_begidx):
8282
with mock.patch.object(readline, 'get_endidx', get_endidx):
83-
with pytest.raises(AttributeError):
84-
# Run the readline tab-completion function with readline mocks in place and cause an exception
85-
completion = cmd2_app.complete(text, state)
83+
# Run the readline tab-completion function with readline mocks in place get None
84+
completion = cmd2_app.complete(text, state)
85+
assert completion is None
8686

8787
def test_complete_empty_arg(cmd2_app):
8888
text = ''

0 commit comments

Comments
 (0)