From 15ec5a898c5e6ba289213ba1ca53564f73783bb5 Mon Sep 17 00:00:00 2001 From: Anthon van der Neut Date: Thu, 25 Jul 2013 15:33:43 +0200 Subject: [PATCH] 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 --- _pytest/capture.py | 2 +- _pytest/config.py | 155 ++++++++++++++++++++++++++++++++++++--- _pytest/helpconfig.py | 1 + _pytest/hookspec.py | 2 +- _pytest/main.py | 2 +- _pytest/pastebin.py | 2 +- _pytest/runner.py | 2 +- _pytest/terminal.py | 2 +- pytest.py | 1 + testing/test_mark.py | 16 +++- testing/test_parseopt.py | 73 ++++++++++++++---- 11 files changed, 224 insertions(+), 34 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index b3d425b04..3c2874684 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -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.") diff --git a/_pytest/config.py b/_pytest/config.py index 87d3e0a2a..73a95d81d 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -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,7 +109,133 @@ 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): self.name = name @@ -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 diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py index 5bbaf0422..08bd9f720 100644 --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -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") diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index 7c0027849..ab77fbca8 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -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 ` and is called once at the beginning of a test run. diff --git a/_pytest/main.py b/_pytest/main.py index 775294c7d..6a39cb3bc 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -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.") diff --git a/_pytest/pastebin.py b/_pytest/pastebin.py index 46a696d3b..cc9d0b5f0 100644 --- a/_pytest/pastebin.py +++ b/_pytest/pastebin.py @@ -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): diff --git a/_pytest/runner.py b/_pytest/runner.py index 763f695ce..c79690d78 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -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): diff --git a/_pytest/terminal.py b/_pytest/terminal.py index abcd0504e..4be2078d1 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -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, diff --git a/pytest.py b/pytest.py index d93766d4b..9897780b2 100644 --- a/pytest.py +++ b/pytest.py @@ -1,3 +1,4 @@ +# PYTHON_ARGCOMPLETE_OK """ pytest: unit and functional testing with Python. """ diff --git a/testing/test_mark.py b/testing/test_mark.py index 3caf625b2..eeea4f023 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -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__', diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index d4ea789ad..c5d821c23 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -7,8 +7,44 @@ 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() group = parser.getgroup("hello", description="desc") @@ -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"])