moving from optparse to argparse. Major difficulty is

that argparse does not have Option objects -> added class Argument
Needed explicit call of MyOptionParser.format_epilog as argparse
does not have that. The parse_arg epilog argument wraps the text,
which is not the same (could be handled with a special formatter).

- parser.parse() now returns single argument (with positional args in
  .file_or_dir)
- "file_or_dir" made a class variable Config._file_or_dir and used in help and tests
- added code for argcomplete (because of which this all started!)

addoption:
- if option type is a string ('int' or 'string', this converted to
  int resp. str
- if option type is 'count' this is changed to the type of choices[0]

testing:
- added tests for Argument
- test_mark.test_keyword_extra split as ['-k', '-mykeyword'] generates argparse
  error test split in two and one marked as fail
- testing hints, multiline and more strickt (for if someone moves format_epilog
  to epilog argument of parse_args without Formatter)
- test for destination derived from long option with internal dash
- renamed second test_parseopt.test_parse() to test_parse2 as it was
  not tested at all (the first was tested.)

--HG--
branch : argparse
This commit is contained in:
Anthon van der Neut 2013-07-25 15:33:43 +02:00
parent 9d9dd381bc
commit 15ec5a898c
11 changed files with 224 additions and 34 deletions

View File

@ -6,7 +6,7 @@ import os
def pytest_addoption(parser):
group = parser.getgroup("general")
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.")
group._addoption('-s', action="store_const", const="no", dest="capture",
help="shortcut for --capture=no.")

View File

@ -80,16 +80,20 @@ class Parser:
for group in groups:
if group.options:
desc = group.description or group.name
optgroup = py.std.optparse.OptionGroup(optparser, desc)
optgroup.add_options(group.options)
optparser.add_option_group(optgroup)
arggroup = optparser.add_argument_group(desc)
for option in group.options:
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])
def parse_setoption(self, args, option):
parsedoption, args = self.parse(args)
parsedoption = self.parse(args)
for name, value in parsedoption.__dict__.items():
setattr(option, name, value)
return args
return getattr(parsedoption, Config._file_or_dir)
def addini(self, name, help, type=None, default=None):
""" register an ini-file option.
@ -105,6 +109,132 @@ 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):
"""
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')
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':
# argparse expects a type here take it from
# the type of the first element
attrs['type'] = type(attrs['choices'][0])
else:
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'.split()
if self.dest:
attrs.append(self.dest)
for attr in attrs:
try:
self._attrs[attr] = getattr(self, attr)
except AttributeError:
pass
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:
def __init__(self, name, description="", parser=None):
@ -115,11 +245,11 @@ class OptionGroup:
def addoption(self, *optnames, **attrs):
""" add an option to this group. """
option = py.std.optparse.Option(*optnames, **attrs)
option = Argument(*optnames, **attrs)
self._addoption_instance(option, shortupper=False)
def _addoption(self, *optnames, **attrs):
option = py.std.optparse.Option(*optnames, **attrs)
option = Argument(*optnames, **attrs)
self._addoption_instance(option, shortupper=True)
def _addoption_instance(self, option, shortupper=False):
@ -132,11 +262,11 @@ class OptionGroup:
self.options.append(option)
class MyOptionParser(py.std.optparse.OptionParser):
class MyOptionParser(py.std.argparse.ArgumentParser):
def __init__(self, parser):
self._parser = parser
py.std.optparse.OptionParser.__init__(self, usage=parser._usage,
add_help_option=False)
py.std.argparse.ArgumentParser.__init__(self, usage=parser._usage,
add_help=False)
def format_epilog(self, formatter):
hints = self._parser.hints
if hints:
@ -263,12 +393,15 @@ class CmdOptions(object):
class Config(object):
""" access to configuration values, pluginmanager and plugin hooks. """
_file_or_dir = 'file_or_dir'
def __init__(self, pluginmanager=None):
#: access to command line option as attributes.
#: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead
self.option = CmdOptions()
_a = self._file_or_dir
self._parser = Parser(
usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]",
usage="%%(prog)s [options] [%s] [%s] [...]" % (_a, _a),
processopt=self._processopt,
)
#: a pluginmanager instance

View File

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

View File

@ -23,7 +23,7 @@ def pytest_cmdline_preparse(config, args):
"""modify command line arguments before option parsing. """
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
called once at the beginning of a test run.

View File

@ -35,7 +35,7 @@ def pytest_addoption(parser):
dest="exitfirst",
help="exit instantly on first error or failed test."),
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.")
group._addoption('--strict', action="store_true",
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._addoption('--pastebin', metavar="mode",
action='store', dest="pastebin", default=None,
type="choice", choices=['failed', 'all'],
choices=['failed', 'all'],
help="send failed|all info to bpaste.net pastebin service.")
def pytest_configure(__multicall__, config):

View File

@ -18,7 +18,7 @@ def pytest_namespace():
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting", "reporting", after="general")
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)."),
def pytest_terminal_summary(terminalreporter):

View File

@ -25,7 +25,7 @@ def pytest_addoption(parser):
help="(deprecated, use -r)")
group._addoption('--tb', metavar="style",
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).")
group._addoption('--fulltrace',
action="store_true", dest="fulltrace", default=False,

View File

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

View File

@ -451,13 +451,23 @@ class TestKeywordSelection:
assert 0
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)
passed, skipped, failed = reprec.countoutcomes()
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):
"""Make sure the tests do not match on magic values,
no double underscored values, like '__dict__',

View File

@ -7,7 +7,43 @@ class TestParser:
parser = parseopt.Parser(usage="xyz")
pytest.raises(SystemExit, 'parser.parse(["-h"])')
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):
parser = parseopt.Parser()
@ -36,7 +72,7 @@ class TestParser:
group = parseopt.OptionGroup("hello")
group.addoption("--option1", action="store_true")
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):
parser = parseopt.Parser()
@ -58,19 +94,19 @@ class TestParser:
def test_parse(self):
parser = parseopt.Parser()
parser.addoption("--hello", dest="hello", action="store")
option, args = parser.parse(['--hello', 'world'])
assert option.hello == "world"
assert not args
args = parser.parse(['--hello', 'world'])
assert args.hello == "world"
assert not getattr(args, parseopt.Config._file_or_dir)
def test_parse(self):
def test_parse2(self):
parser = parseopt.Parser()
option, args = parser.parse([py.path.local()])
assert args[0] == py.path.local()
args = parser.parse([py.path.local()])
assert getattr(args, parseopt.Config._file_or_dir)[0] == py.path.local()
def test_parse_will_set_default(self):
parser = parseopt.Parser()
parser.addoption("--hello", dest="hello", default="x", action="store")
option, args = parser.parse([])
option = parser.parse([])
assert option.hello == "x"
del option.hello
args = parser.parse_setoption([], option)
@ -87,28 +123,37 @@ class TestParser:
assert option.world == 42
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 defaultget(option):
if option.type == "int":
if not hasattr(option, 'type'):
return
if option.type is int:
option.default = 42
elif option.type == "string":
elif option.type is str:
option.default = "world"
parser = parseopt.Parser(processopt=defaultget)
parser.addoption("--this", dest="this", type="int", action="store")
parser.addoption("--hello", dest="hello", type="string", action="store")
parser.addoption("--no", dest="no", action="store_true")
option, args = parser.parse([])
option = parser.parse([])
assert option.hello == "world"
assert option.this == 42
assert option.no is False
@pytest.mark.skipif("sys.version_info < (2,5)")
def test_addoption_parser_epilog(testdir):
testdir.makeconftest("""
def pytest_addoption(parser):
parser.hints.append("hello world")
parser.hints.append("from me too")
""")
result = testdir.runpytest('--help')
#assert result.ret != 0
result.stdout.fnmatch_lines(["*hint: hello world*"])
result.stdout.fnmatch_lines(["hint: hello world", "hint: from me too"])