From 87860600fbf63b625bf8c72830fa7b2df4769d13 Mon Sep 17 00:00:00 2001 From: Anthon van der Neut Date: Tue, 30 Jul 2013 11:26:15 +0200 Subject: [PATCH] Fixes for argcomplete - separate out most argcomplete related stuff in new file _argcomplete.py (could probably be in the py library) - allow positional arguments to be interspaced with optional arguments ( + test in test_parseopt.py ) - removed double argument in tox.ini - add documentation on installing argcomplete (>=0.5.7 as needed for Python 3), might need improving/incorporation in index. This does not work on 2.5 yet. I have patches for argcomplete (with/print()/"".format) but I am not sure they will be accepted. Agreed with hpk not to push for that. Removing argcomplete and leaving completion code active now works by early exit, so no longer re-runs the programs without parameters (which took long for py.test) test calls bash with a script that redirects filedescriptor 8 (as used by argcomplete), so the result can be tested. --HG-- branch : argcomplete --- _pytest/_argcomplete.py | 68 ++++++++++++++++++++++++++++++++++++++ _pytest/config.py | 25 +++++++++----- doc/en/bash-completion.txt | 28 ++++++++++++++++ testing/test_parseopt.py | 40 ++++++++++++++++++++++ tox.ini | 2 +- 5 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 _pytest/_argcomplete.py create mode 100644 doc/en/bash-completion.txt diff --git a/_pytest/_argcomplete.py b/_pytest/_argcomplete.py new file mode 100644 index 000000000..2a836a766 --- /dev/null +++ b/_pytest/_argcomplete.py @@ -0,0 +1,68 @@ + +"""allow bash-completion for argparse with argcomplete if installed +needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail +to find the magic string, so _ARGCOMPLETE env. var is never set, and +this does not need special code. + +argcomplete does not support python 2.5 (although the changes for that +are minor). + +Function try_argcomplete(parser) should be called directly before +the call to ArgumentParser.parse_args(). + +The filescompleter is what you normally would use on the positional +arguments specification, in order to get "dirname/" after "dirn" +instead of the default "dirname ": + + optparser.add_argument(Config._file_or_dir, nargs='*' + ).completer=filescompleter + +Other, application specific, completers should go in the file +doing the add_argument calls as they need to be specified as .completer +attributes as well. (If argcomplete is not installed, the function the +attribute points to will not be used). + +--- +To include this support in another application that has setup.py generated +scripts: +- add the line: + # PYTHON_ARGCOMPLETE_OK + near the top of the main python entry point +- include in the file calling parse_args(): + from _argcomplete import try_argcomplete, filescompleter + , call try_argcomplete just before parse_args(), and optionally add + filescompleter to the positional arguments' add_argument() +If things do not work right away: +- switch on argcomplete debugging with (also helpful when doing custom + completers): + export _ARC_DEBUG=1 +- run: + python-argcomplete-check-easy-install-script $(which appname) + echo $? + will echo 0 if the magic line has been found, 1 if not +- sometimes it helps to find early on errors using: + _ARGCOMPLETE=1 _ARC_DEBUG=1 appname + which should throw a KeyError: 'COMPLINE' (which is properly set by the + global argcomplete script). + +""" + +import sys +import os + +if os.environ.get('_ARGCOMPLETE'): + # argcomplete 0.5.6 is not compatible with python 2.5.6: print/with/format + if sys.version_info[:2] < (2, 6): + sys.exit(1) + try: + import argcomplete + import argcomplete.completers + except ImportError: + sys.exit(-1) + filescompleter = argcomplete.completers.FilesCompleter() + + def try_argcomplete(parser): + argcomplete.autocomplete(parser) +else: + def try_argcomplete(parser): pass + filescompleter = None diff --git a/_pytest/config.py b/_pytest/config.py index 984d9a195..82c224bff 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -4,6 +4,7 @@ import py import sys, os from _pytest.core import PluginManager import pytest +from _argcomplete import try_argcomplete, filescompleter # enable after some grace period for plugin writers TYPE_WARN = False @@ -91,7 +92,9 @@ class Parser: n = option.names() a = option.attrs() arggroup.add_argument(*n, **a) - optparser.add_argument(Config._file_or_dir, nargs='*') + # bash like autocompletion for dirs (appending '/') + optparser.add_argument(Config._file_or_dir, nargs='*' + ).completer=filescompleter try_argcomplete(self.optparser) return self.optparser.parse_args([str(x) for x in args]) @@ -115,13 +118,6 @@ class Parser: self._inidict[name] = (help, type, default) self._ininames.append(name) -def try_argcomplete(parser): - try: - import argcomplete - except ImportError: - pass - else: - argcomplete.autocomplete(parser) class ArgumentError(Exception): """ @@ -304,6 +300,7 @@ class MyOptionParser(py.std.argparse.ArgumentParser): self._parser = parser py.std.argparse.ArgumentParser.__init__(self, usage=parser._usage, add_help=False) + def format_epilog(self, formatter): hints = self._parser.hints if hints: @@ -312,6 +309,18 @@ class MyOptionParser(py.std.argparse.ArgumentParser): return s return "" + def parse_args(self, args=None, namespace=None): + """allow splitting of positional arguments""" + args, argv = self.parse_known_args(args, namespace) + if argv: + for arg in argv: + if arg and arg[0] == '-': + msg = py.std.argparse._('unrecognized arguments: %s') + self.error(msg % ' '.join(argv)) + getattr(args, Config._file_or_dir).extend(argv) + return args + + class Conftest(object): """ the single place for accessing values and interacting towards conftest modules from py.test objects. diff --git a/doc/en/bash-completion.txt b/doc/en/bash-completion.txt new file mode 100644 index 000000000..0fbe83f38 --- /dev/null +++ b/doc/en/bash-completion.txt @@ -0,0 +1,28 @@ + +.. _bash_completion: + +Setting up bash completion +========================== + +When using bash as your shell, ``py.test`` can use argcomplete +(https://argcomplete.readthedocs.org/) for auto-completion. +For this ``argcomplete`` needs to be installed **and** enabled. + +Install argcomplete using:: + + sudo pip install 'argcomplete>=0.5.7' + +For global activation of all argcomplete enabled python applications run:: + + sudo activate-global-python-argcomplete + +For permanent (but not global) ``py.test`` activation, use:: + + register-python-argcomplete py.test >> ~/.bashrc + +For one-time activation of argcomplete for ``py.test`` only, use:: + + eval "$(register-python-argcomplete py.test)" + + + diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 787284cb8..97b0c4d3a 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -130,6 +130,21 @@ class TestParser: args = parser.parse(['--ultimate-answer', '42']) assert args.ultimate_answer == 42 + def test_parse_split_positional_arguments(self): + parser = parseopt.Parser() + parser.addoption("-R", action='store_true') + parser.addoption("-S", action='store_false') + args = parser.parse(['-R', '4', '2', '-S']) + assert getattr(args, parseopt.Config._file_or_dir) == ['4', '2'] + args = parser.parse(['-R', '-S', '4', '2', '-R']) + assert getattr(args, parseopt.Config._file_or_dir) == ['4', '2'] + assert args.R == True + assert args.S == False + args = parser.parse(['-R', '4', '-S', '2']) + assert getattr(args, parseopt.Config._file_or_dir) == ['4', '2'] + assert args.R == True + assert args.S == False + def test_parse_defaultgetter(self): def defaultget(option): if not hasattr(option, 'type'): @@ -158,3 +173,28 @@ def test_addoption_parser_epilog(testdir): #assert result.ret != 0 result.stdout.fnmatch_lines(["hint: hello world", "hint: from me too"]) +@pytest.mark.skipif("sys.version_info < (2,5)") +def test_argcomplete(testdir): + import os + p = py.path.local.make_numbered_dir(prefix="test_argcomplete-", + keep=None, rootdir=testdir.tmpdir) + script = p._fastjoin('test_argcomplete') + with open(str(script), 'w') as fp: + # redirect output from argcomplete to stdin and stderr is not trivial + # http://stackoverflow.com/q/12589419/1307905 + # so we use bash + fp.write('COMP_WORDBREAKS="$COMP_WORDBREAKS" $(which py.test) ' + '8>&1 9>&2') + os.environ['_ARGCOMPLETE'] = "1" + os.environ['_ARGCOMPLETE_IFS'] = "\x0b" + os.environ['COMP_LINE'] = "py.test --fu" + os.environ['COMP_POINT'] = "12" + os.environ['COMP_WORDBREAKS'] = ' \\t\\n"\\\'><=;|&(:' + + result = testdir.run('bash', str(script), '--fu') + print dir(result), result.ret + if result.ret == 255: + # argcomplete not found + assert True + else: + result.stdout.fnmatch_lines(["--funcargs", "--fulltrace"]) diff --git a/tox.ini b/tox.ini index 426d7f939..756f64e79 100644 --- a/tox.ini +++ b/tox.ini @@ -43,7 +43,7 @@ changedir=. deps=twisted pexpect commands= - py.test -rsxf testing/test_unittest.py \ + py.test -rsxf \ --junitxml={envlogdir}/junit-{envname}.xml {posargs:testing/test_unittest.py} [testenv:doctest] changedir=.