3 new commits in pytest: https://bitbucket.org/hpk42/pytest/commits/a3f34a534d44/ Changeset: a3f34a534d44 Branch: argparse User: Anthon van der Neut Date: 2013-07-25 15:33:43 Summary: 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.) Affected #: 11 files diff -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r a3f34a534d44feee1c77f5122402c761259a11d9 _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -6,7 +6,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r a3f34a534d44feee1c77f5122402c761259a11d9 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -80,16 +80,20 @@ 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 @@ 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 @@ 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 @@ 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 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r a3f34a534d44feee1c77f5122402c761259a11d9 _pytest/helpconfig.py --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -62,6 +62,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r a3f34a534d44feee1c77f5122402c761259a11d9 _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -23,7 +23,7 @@ """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. diff -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r a3f34a534d44feee1c77f5122402c761259a11d9 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -35,7 +35,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r a3f34a534d44feee1c77f5122402c761259a11d9 _pytest/pastebin.py --- a/_pytest/pastebin.py +++ b/_pytest/pastebin.py @@ -10,7 +10,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r a3f34a534d44feee1c77f5122402c761259a11d9 _pytest/runner.py --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -18,7 +18,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r a3f34a534d44feee1c77f5122402c761259a11d9 _pytest/terminal.py --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -25,7 +25,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r a3f34a534d44feee1c77f5122402c761259a11d9 pytest.py --- a/pytest.py +++ b/pytest.py @@ -1,3 +1,4 @@ +# PYTHON_ARGCOMPLETE_OK """ pytest: unit and functional testing with Python. """ diff -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r a3f34a534d44feee1c77f5122402c761259a11d9 testing/test_mark.py --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -451,12 +451,22 @@ assert 0 test_one.mykeyword = True """) + 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 - reprec = testdir.inline_run("-k", "mykeyword", p) - passed, skipped, failed = reprec.countoutcomes() - assert failed == 1 def test_no_magic_values(self, testdir): """Make sure the tests do not match on magic values, diff -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r a3f34a534d44feee1c77f5122402c761259a11d9 testing/test_parseopt.py --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -7,8 +7,44 @@ 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 @@ 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 @@ 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 @@ 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"]) https://bitbucket.org/hpk42/pytest/commits/8379d69dcb3f/ Changeset: 8379d69dcb3f Branch: argparse User: Anthon van der Neut Date: 2013-07-25 17:26:48 Summary: auto change %default -> %(default)s in help parameter string (on retrieval) added code for warnings on optparse arguments (type, help), which can be easily switched on with TYPE_WARN = True in config.py installed and tested ( py.test --help ) pytest-quickcheck-0.7 pytest-gae-0.2.2 pytest-growl-0.1 pytest-bdd-0.4.7 pytest-bdd-splinter-0.4.4 pytest-cache-1.0 pytest-capturelog-0.7 pytest-codecheckers-0.2 pytest-contextfixture-0.1.1 pytest-cov-1.6 pytest-flakes-0.1 pytest-incremental-0.3.0 pytest-xdist-1.8 pytest-localserver-0.1.5 pytest-monkeyplus-1.1.0 pytest-oerp-0.2.0 pytest-pep8-1.0.4 pytest-pydev-0.1 pytest-rage-0.1 pytest-runfailed-0.3 pytest-timeout-0.3 pytest-xprocess-0.7 pytest-browsermob-proxy-0.1 pytest-mozwebqa-1.1.1 pytest-random-0.02 pytest-rerunfailures-0.03 pytest-zap-0.1 pytest-blockage-0.1 pytest-django-2.3.0 pytest-figleaf-1.0 pytest-greendots-0.1 pytest-instafail-0.1.0 pytest-konira-0.2 pytest-marker-bugzilla-0.06 pytest-marks-0.4 pytest-poo-0.2 pytest-twisted-1.4 pytest-yamlwsgi-0.6 Affected #: 1 file diff -r a3f34a534d44feee1c77f5122402c761259a11d9 -r 8379d69dcb3fbb3349daf1ae0a6d5441f8289a18 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -5,6 +5,12 @@ from _pytest.core import PluginManager import pytest +# enable after some grace period for plugin writers +TYPE_WARN = False +if TYPE_WARN: + import warnings + + def pytest_cmdline_parse(pluginmanager, args): config = Config(pluginmanager) config.parse(args) @@ -147,6 +153,17 @@ 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: @@ -155,10 +172,25 @@ # 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'] @@ -185,7 +217,7 @@ def attrs(self): # update any attributes set by processopt - attrs = 'default dest'.split() + attrs = 'default dest help'.split() if self.dest: attrs.append(self.dest) for attr in attrs: @@ -193,6 +225,11 @@ 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): https://bitbucket.org/hpk42/pytest/commits/4405d5fd6cae/ Changeset: 4405d5fd6cae User: hpk42 Date: 2013-07-26 07:41:43 Summary: Merged in anthon_van_der_neut/pytest/argparse (pull request #46) argparse / argcomplete Affected #: 11 files diff -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r 4405d5fd6caec4072177911d9af4e1c57fe66cec _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -6,7 +6,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r 4405d5fd6caec4072177911d9af4e1c57fe66cec _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -5,6 +5,12 @@ from _pytest.core import PluginManager import pytest +# enable after some grace period for plugin writers +TYPE_WARN = False +if TYPE_WARN: + import warnings + + def pytest_cmdline_parse(pluginmanager, args): config = Config(pluginmanager) config.parse(args) @@ -80,16 +86,20 @@ 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 +115,164 @@ 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') + 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: def __init__(self, name, description="", parser=None): self.name = name @@ -115,11 +282,11 @@ 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 +299,11 @@ 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 +430,15 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r 4405d5fd6caec4072177911d9af4e1c57fe66cec _pytest/helpconfig.py --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -62,6 +62,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r 4405d5fd6caec4072177911d9af4e1c57fe66cec _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -23,7 +23,7 @@ """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. diff -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r 4405d5fd6caec4072177911d9af4e1c57fe66cec _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -35,7 +35,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r 4405d5fd6caec4072177911d9af4e1c57fe66cec _pytest/pastebin.py --- a/_pytest/pastebin.py +++ b/_pytest/pastebin.py @@ -10,7 +10,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r 4405d5fd6caec4072177911d9af4e1c57fe66cec _pytest/runner.py --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -18,7 +18,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r 4405d5fd6caec4072177911d9af4e1c57fe66cec _pytest/terminal.py --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -25,7 +25,7 @@ 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 -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r 4405d5fd6caec4072177911d9af4e1c57fe66cec pytest.py --- a/pytest.py +++ b/pytest.py @@ -1,3 +1,4 @@ +# PYTHON_ARGCOMPLETE_OK """ pytest: unit and functional testing with Python. """ diff -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r 4405d5fd6caec4072177911d9af4e1c57fe66cec testing/test_mark.py --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -451,12 +451,22 @@ assert 0 test_one.mykeyword = True """) + 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 - reprec = testdir.inline_run("-k", "mykeyword", p) - passed, skipped, failed = reprec.countoutcomes() - assert failed == 1 def test_no_magic_values(self, testdir): """Make sure the tests do not match on magic values, diff -r abfc7dc64ed4f5a50e917ac8a30863be7e2361a7 -r 4405d5fd6caec4072177911d9af4e1c57fe66cec testing/test_parseopt.py --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -7,8 +7,44 @@ 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 @@ 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 @@ 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 @@ 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"]) Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. _______________________________________________ pytest-commit mailing list pytest-commit@python.org http://mail.python.org/mailman/listinfo/pytest-commit