Merged in anthon_van_der_neut/pytest/argparse (pull request #46)

argparse / argcomplete
This commit is contained in:
holger krekel 2013-07-26 07:41:43 +02:00
commit 4b87810fc2
11 changed files with 261 additions and 34 deletions

View File

@ -6,7 +6,7 @@ import os
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("general") group = parser.getgroup("general")
group._addoption('--capture', action="store", default=None, group._addoption('--capture', action="store", default=None,
metavar="method", type="choice", choices=['fd', 'sys', 'no'], metavar="method", choices=['fd', 'sys', 'no'],
help="per-test capturing method: one of fd (default)|sys|no.") help="per-test capturing method: one of fd (default)|sys|no.")
group._addoption('-s', action="store_const", const="no", dest="capture", group._addoption('-s', action="store_const", const="no", dest="capture",
help="shortcut for --capture=no.") help="shortcut for --capture=no.")

View File

@ -5,6 +5,12 @@ import sys, os
from _pytest.core import PluginManager from _pytest.core import PluginManager
import pytest import pytest
# enable after some grace period for plugin writers
TYPE_WARN = False
if TYPE_WARN:
import warnings
def pytest_cmdline_parse(pluginmanager, args): def pytest_cmdline_parse(pluginmanager, args):
config = Config(pluginmanager) config = Config(pluginmanager)
config.parse(args) config.parse(args)
@ -80,16 +86,20 @@ class Parser:
for group in groups: for group in groups:
if group.options: if group.options:
desc = group.description or group.name desc = group.description or group.name
optgroup = py.std.optparse.OptionGroup(optparser, desc) arggroup = optparser.add_argument_group(desc)
optgroup.add_options(group.options) for option in group.options:
optparser.add_option_group(optgroup) n = option.names()
a = option.attrs()
arggroup.add_argument(*n, **a)
optparser.add_argument(Config._file_or_dir, nargs='*')
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])
def parse_setoption(self, args, option): def parse_setoption(self, args, option):
parsedoption, args = self.parse(args) parsedoption = self.parse(args)
for name, value in parsedoption.__dict__.items(): for name, value in parsedoption.__dict__.items():
setattr(option, name, value) setattr(option, name, value)
return args return getattr(parsedoption, Config._file_or_dir)
def addini(self, name, help, type=None, default=None): def addini(self, name, help, type=None, default=None):
""" register an ini-file option. """ register an ini-file option.
@ -105,7 +115,164 @@ 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):
"""
Raised if an Argument instance is created with invalid or
inconsistent arguments.
"""
def __init__(self, msg, option):
self.msg = msg
self.option_id = str(option)
def __str__(self):
if self.option_id:
return "option %s: %s" % (self.option_id, self.msg)
else:
return self.msg
class Argument:
"""class that mimics the necessary behaviour of py.std.optparse.Option """
_typ_map = {
'int': int,
'string': str,
}
def __init__(self, *names, **attrs):
"""store parms in private vars for use in add_argument"""
self._attrs = attrs
self._short_opts = []
self._long_opts = []
self.dest = attrs.get('dest')
if TYPE_WARN:
try:
help = attrs['help']
if '%default' in help:
warnings.warn(
'py.test now uses argparse. "%default" should be'
' changed to "%(default)s" ',
FutureWarning,
stacklevel=3)
except KeyError:
pass
try:
typ = attrs['type']
except KeyError:
pass
else:
# this might raise a keyerror as well, don't want to catch that
if isinstance(typ, str):
if typ == 'choice':
if TYPE_WARN:
warnings.warn(
'type argument to addoption() is a string %r.'
' For parsearg this is optional and when supplied '
' should be a type.'
' (options: %s)' % (typ, names),
FutureWarning,
stacklevel=3)
# argparse expects a type here take it from
# the type of the first element
attrs['type'] = type(attrs['choices'][0])
else:
if TYPE_WARN:
warnings.warn(
'type argument to addoption() is a string %r.'
' For parsearg this should be a type.'
' (options: %s)' % (typ, names),
FutureWarning,
stacklevel=3)
attrs['type'] = Argument._typ_map[typ]
# used in test_parseopt -> test_parse_defaultgetter
self.type = attrs['type']
else:
self.type = typ
try:
# attribute existence is tested in Config._processopt
self.default = attrs['default']
except KeyError:
pass
self._set_opt_strings(names)
if not self.dest:
if self._long_opts:
self.dest = self._long_opts[0][2:].replace('-', '_')
else:
try:
self.dest = self._short_opts[0][1:]
except IndexError:
raise ArgumentError(
'need a long or short option', self)
def names(self):
return self._short_opts + self._long_opts
def attrs(self):
# update any attributes set by processopt
attrs = 'default dest help'.split()
if self.dest:
attrs.append(self.dest)
for attr in attrs:
try:
self._attrs[attr] = getattr(self, attr)
except AttributeError:
pass
if self._attrs.get('help'):
a = self._attrs['help']
a = a.replace('%default', '%(default)s')
#a = a.replace('%prog', '%(prog)s')
self._attrs['help'] = a
return self._attrs
def _set_opt_strings(self, opts):
"""directly from optparse
might not be necessary as this is passed to argparse later on"""
for opt in opts:
if len(opt) < 2:
raise ArgumentError(
"invalid option string %r: "
"must be at least two characters long" % opt, self)
elif len(opt) == 2:
if not (opt[0] == "-" and opt[1] != "-"):
raise ArgumentError(
"invalid short option string %r: "
"must be of the form -x, (x any non-dash char)" % opt,
self)
self._short_opts.append(opt)
else:
if not (opt[0:2] == "--" and opt[2] != "-"):
raise ArgumentError(
"invalid long option string %r: "
"must start with --, followed by non-dash" % opt,
self)
self._long_opts.append(opt)
def __repr__(self):
retval = 'Argument('
if self._short_opts:
retval += '_short_opts: ' + repr(self._short_opts) + ', '
if self._long_opts:
retval += '_long_opts: ' + repr(self._long_opts) + ', '
retval += 'dest: ' + repr(self.dest) + ', '
if hasattr(self, 'type'):
retval += 'type: ' + repr(self.type) + ', '
if hasattr(self, 'default'):
retval += 'default: ' + repr(self.default) + ', '
if retval[-2:] == ', ': # always long enough to test ("Argument(" )
retval = retval[:-2]
retval += ')'
return retval
class OptionGroup: class OptionGroup:
def __init__(self, name, description="", parser=None): def __init__(self, name, description="", parser=None):
self.name = name self.name = name
@ -115,11 +282,11 @@ class OptionGroup:
def addoption(self, *optnames, **attrs): def addoption(self, *optnames, **attrs):
""" add an option to this group. """ """ add an option to this group. """
option = py.std.optparse.Option(*optnames, **attrs) option = Argument(*optnames, **attrs)
self._addoption_instance(option, shortupper=False) self._addoption_instance(option, shortupper=False)
def _addoption(self, *optnames, **attrs): def _addoption(self, *optnames, **attrs):
option = py.std.optparse.Option(*optnames, **attrs) option = Argument(*optnames, **attrs)
self._addoption_instance(option, shortupper=True) self._addoption_instance(option, shortupper=True)
def _addoption_instance(self, option, shortupper=False): def _addoption_instance(self, option, shortupper=False):
@ -132,11 +299,11 @@ class OptionGroup:
self.options.append(option) self.options.append(option)
class MyOptionParser(py.std.optparse.OptionParser): class MyOptionParser(py.std.argparse.ArgumentParser):
def __init__(self, parser): def __init__(self, parser):
self._parser = parser self._parser = parser
py.std.optparse.OptionParser.__init__(self, usage=parser._usage, py.std.argparse.ArgumentParser.__init__(self, usage=parser._usage,
add_help_option=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:
@ -263,12 +430,15 @@ class CmdOptions(object):
class Config(object): class Config(object):
""" access to configuration values, pluginmanager and plugin hooks. """ """ access to configuration values, pluginmanager and plugin hooks. """
_file_or_dir = 'file_or_dir'
def __init__(self, pluginmanager=None): def __init__(self, pluginmanager=None):
#: access to command line option as attributes. #: access to command line option as attributes.
#: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead #: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead
self.option = CmdOptions() self.option = CmdOptions()
_a = self._file_or_dir
self._parser = Parser( self._parser = Parser(
usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]", usage="%%(prog)s [options] [%s] [%s] [...]" % (_a, _a),
processopt=self._processopt, processopt=self._processopt,
) )
#: a pluginmanager instance #: a pluginmanager instance

View File

@ -62,6 +62,7 @@ def pytest_cmdline_main(config):
def showhelp(config): def showhelp(config):
tw = py.io.TerminalWriter() tw = py.io.TerminalWriter()
tw.write(config._parser.optparser.format_help()) tw.write(config._parser.optparser.format_help())
tw.write(config._parser.optparser.format_epilog(None))
tw.line() tw.line()
tw.line() tw.line()
#tw.sep( "=", "config file settings") #tw.sep( "=", "config file settings")

View File

@ -23,7 +23,7 @@ def pytest_cmdline_preparse(config, args):
"""modify command line arguments before option parsing. """ """modify command line arguments before option parsing. """
def pytest_addoption(parser): def pytest_addoption(parser):
"""register optparse-style options and ini-style config values. """register argparse-style options and ini-style config values.
This function must be implemented in a :ref:`plugin <pluginorder>` and is This function must be implemented in a :ref:`plugin <pluginorder>` and is
called once at the beginning of a test run. called once at the beginning of a test run.

View File

@ -35,7 +35,7 @@ def pytest_addoption(parser):
dest="exitfirst", dest="exitfirst",
help="exit instantly on first error or failed test."), help="exit instantly on first error or failed test."),
group._addoption('--maxfail', metavar="num", group._addoption('--maxfail', metavar="num",
action="store", type="int", dest="maxfail", default=0, action="store", type=int, dest="maxfail", default=0,
help="exit after first num failures or errors.") help="exit after first num failures or errors.")
group._addoption('--strict', action="store_true", group._addoption('--strict', action="store_true",
help="run pytest in strict mode, warnings become errors.") help="run pytest in strict mode, warnings become errors.")

View File

@ -10,7 +10,7 @@ def pytest_addoption(parser):
group = parser.getgroup("terminal reporting") group = parser.getgroup("terminal reporting")
group._addoption('--pastebin', metavar="mode", group._addoption('--pastebin', metavar="mode",
action='store', dest="pastebin", default=None, action='store', dest="pastebin", default=None,
type="choice", choices=['failed', 'all'], choices=['failed', 'all'],
help="send failed|all info to bpaste.net pastebin service.") help="send failed|all info to bpaste.net pastebin service.")
def pytest_configure(__multicall__, config): def pytest_configure(__multicall__, config):

View File

@ -18,7 +18,7 @@ def pytest_namespace():
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("terminal reporting", "reporting", after="general") group = parser.getgroup("terminal reporting", "reporting", after="general")
group.addoption('--durations', group.addoption('--durations',
action="store", type="int", default=None, metavar="N", action="store", type=int, default=None, metavar="N",
help="show N slowest setup/test durations (N=0 for all)."), help="show N slowest setup/test durations (N=0 for all)."),
def pytest_terminal_summary(terminalreporter): def pytest_terminal_summary(terminalreporter):

View File

@ -25,7 +25,7 @@ def pytest_addoption(parser):
help="(deprecated, use -r)") help="(deprecated, use -r)")
group._addoption('--tb', metavar="style", group._addoption('--tb', metavar="style",
action="store", dest="tbstyle", default='long', action="store", dest="tbstyle", default='long',
type="choice", choices=['long', 'short', 'no', 'line', 'native'], choices=['long', 'short', 'no', 'line', 'native'],
help="traceback print mode (long/short/line/native/no).") help="traceback print mode (long/short/line/native/no).")
group._addoption('--fulltrace', group._addoption('--fulltrace',
action="store_true", dest="fulltrace", default=False, action="store_true", dest="fulltrace", default=False,

View File

@ -1,3 +1,4 @@
# PYTHON_ARGCOMPLETE_OK
""" """
pytest: unit and functional testing with Python. pytest: unit and functional testing with Python.
""" """

View File

@ -451,13 +451,23 @@ class TestKeywordSelection:
assert 0 assert 0
test_one.mykeyword = True test_one.mykeyword = True
""") """)
reprec = testdir.inline_run("-k", "-mykeyword", p)
passed, skipped, failed = reprec.countoutcomes()
assert passed + skipped + failed == 0
reprec = testdir.inline_run("-k", "mykeyword", p) reprec = testdir.inline_run("-k", "mykeyword", p)
passed, skipped, failed = reprec.countoutcomes() passed, skipped, failed = reprec.countoutcomes()
assert failed == 1 assert failed == 1
@pytest.mark.xfail
def test_keyword_extra_dash(self, testdir):
p = testdir.makepyfile("""
def test_one():
assert 0
test_one.mykeyword = True
""")
# with argparse the argument to an option cannot
# start with '-'
reprec = testdir.inline_run("-k", "-mykeyword", p)
passed, skipped, failed = reprec.countoutcomes()
assert passed + skipped + failed == 0
def test_no_magic_values(self, testdir): def test_no_magic_values(self, testdir):
"""Make sure the tests do not match on magic values, """Make sure the tests do not match on magic values,
no double underscored values, like '__dict__', no double underscored values, like '__dict__',

View File

@ -7,8 +7,44 @@ class TestParser:
parser = parseopt.Parser(usage="xyz") parser = parseopt.Parser(usage="xyz")
pytest.raises(SystemExit, 'parser.parse(["-h"])') pytest.raises(SystemExit, 'parser.parse(["-h"])')
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert err.find("no such option") != -1 assert err.find("error: unrecognized arguments") != -1
def test_argument(self):
with pytest.raises(parseopt.ArgumentError):
# need a short or long option
argument = parseopt.Argument()
argument = parseopt.Argument('-t')
assert argument._short_opts == ['-t']
assert argument._long_opts == []
assert argument.dest == 't'
argument = parseopt.Argument('-t', '--test')
assert argument._short_opts == ['-t']
assert argument._long_opts == ['--test']
assert argument.dest == 'test'
argument = parseopt.Argument('-t', '--test', dest='abc')
assert argument.dest == 'abc'
def test_argument_type(self):
argument = parseopt.Argument('-t', dest='abc', type='int')
assert argument.type is int
argument = parseopt.Argument('-t', dest='abc', type='string')
assert argument.type is str
argument = parseopt.Argument('-t', dest='abc', type=float)
assert argument.type is float
with pytest.raises(KeyError):
argument = parseopt.Argument('-t', dest='abc', type='choice')
argument = parseopt.Argument('-t', dest='abc', type='choice',
choices=['red', 'blue'])
assert argument.type is str
def test_argument_processopt(self):
argument = parseopt.Argument('-t', type=int)
argument.default = 42
argument.dest = 'abc'
res = argument.attrs()
assert res['default'] == 42
assert res['dest'] == 'abc'
def test_group_add_and_get(self): def test_group_add_and_get(self):
parser = parseopt.Parser() parser = parseopt.Parser()
group = parser.getgroup("hello", description="desc") group = parser.getgroup("hello", description="desc")
@ -36,7 +72,7 @@ class TestParser:
group = parseopt.OptionGroup("hello") group = parseopt.OptionGroup("hello")
group.addoption("--option1", action="store_true") group.addoption("--option1", action="store_true")
assert len(group.options) == 1 assert len(group.options) == 1
assert isinstance(group.options[0], py.std.optparse.Option) assert isinstance(group.options[0], parseopt.Argument)
def test_group_shortopt_lowercase(self): def test_group_shortopt_lowercase(self):
parser = parseopt.Parser() parser = parseopt.Parser()
@ -58,19 +94,19 @@ class TestParser:
def test_parse(self): def test_parse(self):
parser = parseopt.Parser() parser = parseopt.Parser()
parser.addoption("--hello", dest="hello", action="store") parser.addoption("--hello", dest="hello", action="store")
option, args = parser.parse(['--hello', 'world']) args = parser.parse(['--hello', 'world'])
assert option.hello == "world" assert args.hello == "world"
assert not args assert not getattr(args, parseopt.Config._file_or_dir)
def test_parse(self): def test_parse2(self):
parser = parseopt.Parser() parser = parseopt.Parser()
option, args = parser.parse([py.path.local()]) args = parser.parse([py.path.local()])
assert args[0] == py.path.local() assert getattr(args, parseopt.Config._file_or_dir)[0] == py.path.local()
def test_parse_will_set_default(self): def test_parse_will_set_default(self):
parser = parseopt.Parser() parser = parseopt.Parser()
parser.addoption("--hello", dest="hello", default="x", action="store") parser.addoption("--hello", dest="hello", default="x", action="store")
option, args = parser.parse([]) option = parser.parse([])
assert option.hello == "x" assert option.hello == "x"
del option.hello del option.hello
args = parser.parse_setoption([], option) args = parser.parse_setoption([], option)
@ -87,28 +123,37 @@ class TestParser:
assert option.world == 42 assert option.world == 42
assert not args assert not args
def test_parse_special_destination(self):
parser = parseopt.Parser()
x = parser.addoption("--ultimate-answer", type=int)
args = parser.parse(['--ultimate-answer', '42'])
assert args.ultimate_answer == 42
def test_parse_defaultgetter(self): def test_parse_defaultgetter(self):
def defaultget(option): def defaultget(option):
if option.type == "int": if not hasattr(option, 'type'):
return
if option.type is int:
option.default = 42 option.default = 42
elif option.type == "string": elif option.type is str:
option.default = "world" option.default = "world"
parser = parseopt.Parser(processopt=defaultget) parser = parseopt.Parser(processopt=defaultget)
parser.addoption("--this", dest="this", type="int", action="store") parser.addoption("--this", dest="this", type="int", action="store")
parser.addoption("--hello", dest="hello", type="string", action="store") parser.addoption("--hello", dest="hello", type="string", action="store")
parser.addoption("--no", dest="no", action="store_true") parser.addoption("--no", dest="no", action="store_true")
option, args = parser.parse([]) option = parser.parse([])
assert option.hello == "world" assert option.hello == "world"
assert option.this == 42 assert option.this == 42
assert option.no is False
@pytest.mark.skipif("sys.version_info < (2,5)") @pytest.mark.skipif("sys.version_info < (2,5)")
def test_addoption_parser_epilog(testdir): def test_addoption_parser_epilog(testdir):
testdir.makeconftest(""" testdir.makeconftest("""
def pytest_addoption(parser): def pytest_addoption(parser):
parser.hints.append("hello world") parser.hints.append("hello world")
parser.hints.append("from me too")
""") """)
result = testdir.runpytest('--help') result = testdir.runpytest('--help')
#assert result.ret != 0 #assert result.ret != 0
result.stdout.fnmatch_lines(["*hint: hello world*"]) result.stdout.fnmatch_lines(["hint: hello world", "hint: from me too"])