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 <TAB> 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
This commit is contained in:
Anthon van der Neut 2013-07-30 11:26:15 +02:00
parent 377f63085a
commit 87860600fb
5 changed files with 154 additions and 9 deletions

68
_pytest/_argcomplete.py Normal file
View File

@ -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<TAB>"
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

View File

@ -4,6 +4,7 @@ import py
import sys, os import sys, os
from _pytest.core import PluginManager from _pytest.core import PluginManager
import pytest import pytest
from _argcomplete import try_argcomplete, filescompleter
# enable after some grace period for plugin writers # enable after some grace period for plugin writers
TYPE_WARN = False TYPE_WARN = False
@ -91,7 +92,9 @@ class Parser:
n = option.names() n = option.names()
a = option.attrs() a = option.attrs()
arggroup.add_argument(*n, **a) 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) try_argcomplete(self.optparser)
return self.optparser.parse_args([str(x) for x in args]) return self.optparser.parse_args([str(x) for x in args])
@ -115,13 +118,6 @@ class Parser:
self._inidict[name] = (help, type, default) self._inidict[name] = (help, type, default)
self._ininames.append(name) self._ininames.append(name)
def try_argcomplete(parser):
try:
import argcomplete
except ImportError:
pass
else:
argcomplete.autocomplete(parser)
class ArgumentError(Exception): class ArgumentError(Exception):
""" """
@ -304,6 +300,7 @@ class MyOptionParser(py.std.argparse.ArgumentParser):
self._parser = parser self._parser = parser
py.std.argparse.ArgumentParser.__init__(self, usage=parser._usage, py.std.argparse.ArgumentParser.__init__(self, usage=parser._usage,
add_help=False) add_help=False)
def format_epilog(self, formatter): def format_epilog(self, formatter):
hints = self._parser.hints hints = self._parser.hints
if hints: if hints:
@ -312,6 +309,18 @@ class MyOptionParser(py.std.argparse.ArgumentParser):
return s return s
return "" 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): class Conftest(object):
""" the single place for accessing values and interacting """ the single place for accessing values and interacting
towards conftest modules from py.test objects. towards conftest modules from py.test objects.

View File

@ -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)"

View File

@ -130,6 +130,21 @@ class TestParser:
args = parser.parse(['--ultimate-answer', '42']) args = parser.parse(['--ultimate-answer', '42'])
assert args.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 test_parse_defaultgetter(self):
def defaultget(option): def defaultget(option):
if not hasattr(option, 'type'): if not hasattr(option, 'type'):
@ -158,3 +173,28 @@ def test_addoption_parser_epilog(testdir):
#assert result.ret != 0 #assert result.ret != 0
result.stdout.fnmatch_lines(["hint: hello world", "hint: from me too"]) 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"])

View File

@ -43,7 +43,7 @@ changedir=.
deps=twisted deps=twisted
pexpect pexpect
commands= commands=
py.test -rsxf testing/test_unittest.py \ py.test -rsxf \
--junitxml={envlogdir}/junit-{envname}.xml {posargs:testing/test_unittest.py} --junitxml={envlogdir}/junit-{envname}.xml {posargs:testing/test_unittest.py}
[testenv:doctest] [testenv:doctest]
changedir=. changedir=.