diff options
author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2018-01-22 00:08:27 -0500 |
---|---|---|
committer | Todd Leonhardt <todd.leonhardt@gmail.com> | 2018-01-22 00:08:27 -0500 |
commit | f28c10a50535f753419bd2120ac6cb0bea9f56e2 (patch) | |
tree | 102b200c4a3dff1adad4bf326ea3f6de9cd56baf | |
parent | c9f7c012bda012b4df7a8c5e853bd5d3e6d99b1b (diff) | |
download | cmd2-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-x | cmd2.py | 34 | ||||
-rwxr-xr-x | setup.py | 6 | ||||
-rw-r--r-- | tests/test_argparse.py | 40 | ||||
-rw-r--r-- | tests/test_cmd2.py | 30 | ||||
-rw-r--r-- | tests/test_completion.py | 6 |
5 files changed, 58 insertions, 58 deletions
@@ -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:]) @@ -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 = '' |