Merge pull request #4651 from blueyed/help-with-argumenterror

Display --help/--version with ArgumentErrors
This commit is contained in:
Bruno Oliveira 2019-02-12 16:01:49 -02:00 committed by GitHub
commit d03444db4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 135 additions and 33 deletions

View File

@ -0,0 +1 @@
Usage errors from argparse are mapped to pytest's ``UsageError``.

View File

@ -0,0 +1 @@
``--help`` and ``--version`` are handled with ``UsageError``.

View File

@ -648,8 +648,27 @@ class Config(object):
return self.pluginmanager.get_plugin("terminalreporter")._tw
def pytest_cmdline_parse(self, pluginmanager, args):
# REF1 assert self == pluginmanager.config, (self, pluginmanager.config)
self.parse(args)
try:
self.parse(args)
except UsageError:
# Handle --version and --help here in a minimal fashion.
# This gets done via helpconfig normally, but its
# pytest_cmdline_main is not called in case of errors.
if getattr(self.option, "version", False) or "--version" in args:
from _pytest.helpconfig import showversion
showversion(self)
elif (
getattr(self.option, "help", False) or "--help" in args or "-h" in args
):
self._parser._getparser().print_help()
sys.stdout.write(
"\nNOTE: displaying only minimal help due to UsageError.\n\n"
)
raise
return self
def notify_exception(self, excinfo, option=None):
@ -760,21 +779,32 @@ class Config(object):
for name in _iter_rewritable_modules(package_files):
hook.mark_rewrite(name)
def _validate_args(self, args):
def _validate_args(self, args, via):
"""Validate known args."""
self._parser.parse_known_and_unknown_args(
args, namespace=copy.copy(self.option)
)
self._parser._config_source_hint = via
try:
self._parser.parse_known_and_unknown_args(
args, namespace=copy.copy(self.option)
)
finally:
del self._parser._config_source_hint
return args
def _preparse(self, args, addopts=True):
if addopts:
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
if len(env_addopts):
args[:] = self._validate_args(shlex.split(env_addopts)) + args
args[:] = (
self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
+ args
)
self._initini(args)
if addopts:
args[:] = self._validate_args(self.getini("addopts")) + args
args[:] = (
self._validate_args(self.getini("addopts"), "via addopts config") + args
)
self._checkversion()
self._consider_importhook(args)
self.pluginmanager.consider_preparse(args)

View File

@ -1,12 +1,10 @@
import argparse
import sys as _sys
import warnings
from gettext import gettext as _
import py
import six
from ..main import EXIT_USAGEERROR
from _pytest.config.exceptions import UsageError
FILE_OR_DIR = "file_or_dir"
@ -337,14 +335,13 @@ class MyOptionParser(argparse.ArgumentParser):
self.extra_info = extra_info
def error(self, message):
"""error(message: string)
"""Transform argparse error message into UsageError."""
msg = "%s: error: %s" % (self.prog, message)
Prints a usage message incorporating the message to stderr and
exits.
Overrides the method in parent class to change exit code"""
self.print_usage(_sys.stderr)
args = {"prog": self.prog, "message": message}
self.exit(EXIT_USAGEERROR, _("%(prog)s: error: %(message)s\n") % args)
if hasattr(self._parser, "_config_source_hint"):
msg = "%s (%s)" % (msg, self._parser._config_source_hint)
raise UsageError(self.format_usage() + msg)
def parse_args(self, args=None, namespace=None):
"""allow splitting of positional arguments"""

View File

@ -118,16 +118,20 @@ def pytest_cmdline_parse():
config.add_cleanup(unset_tracing)
def showversion(config):
p = py.path.local(pytest.__file__)
sys.stderr.write(
"This is pytest version %s, imported from %s\n" % (pytest.__version__, p)
)
plugininfo = getpluginversioninfo(config)
if plugininfo:
for line in plugininfo:
sys.stderr.write(line + "\n")
def pytest_cmdline_main(config):
if config.option.version:
p = py.path.local(pytest.__file__)
sys.stderr.write(
"This is pytest version %s, imported from %s\n" % (pytest.__version__, p)
)
plugininfo = getpluginversioninfo(config)
if plugininfo:
for line in plugininfo:
sys.stderr.write(line + "\n")
showversion(config)
return 0
elif config.option.help:
config._do_configure()

View File

@ -8,10 +8,12 @@ import textwrap
import _pytest._code
import pytest
from _pytest.config import _iter_rewritable_modules
from _pytest.config.exceptions import UsageError
from _pytest.config.findpaths import determine_setup
from _pytest.config.findpaths import get_common_ancestor
from _pytest.config.findpaths import getcfg
from _pytest.main import EXIT_NOTESTSCOLLECTED
from _pytest.main import EXIT_USAGEERROR
class TestParseIni(object):
@ -1031,9 +1033,12 @@ class TestOverrideIniArgs(object):
monkeypatch.setenv("PYTEST_ADDOPTS", "-o")
config = get_config()
with pytest.raises(SystemExit) as excinfo:
with pytest.raises(UsageError) as excinfo:
config._preparse(["cache_dir=ignored"], addopts=True)
assert excinfo.value.args[0] == _pytest.main.EXIT_USAGEERROR
assert (
"error: argument -o/--override-ini: expected one argument (via PYTEST_ADDOPTS)"
in excinfo.value.args[0]
)
def test_addopts_from_ini_not_concatenated(self, testdir):
"""addopts from ini should not take values from normal args (#4265)."""
@ -1046,7 +1051,7 @@ class TestOverrideIniArgs(object):
result = testdir.runpytest("cache_dir=ignored")
result.stderr.fnmatch_lines(
[
"%s: error: argument -o/--override-ini: expected one argument"
"%s: error: argument -o/--override-ini: expected one argument (via addopts config)"
% (testdir.request.config._parser.optparser.prog,)
]
)
@ -1083,3 +1088,68 @@ class TestOverrideIniArgs(object):
result = testdir.runpytest("-o", "foo=1", "-o", "bar=0", "test_foo.py")
assert "ERROR:" not in result.stderr.str()
result.stdout.fnmatch_lines(["collected 1 item", "*= 1 passed in *="])
def test_help_via_addopts(testdir):
testdir.makeini(
"""
[pytest]
addopts = --unknown-option-should-allow-for-help --help
"""
)
result = testdir.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines(
[
"usage: *",
"positional arguments:",
# Displays full/default help.
"to see available markers type: pytest --markers",
]
)
def test_help_and_version_after_argument_error(testdir):
testdir.makeconftest(
"""
def validate(arg):
raise argparse.ArgumentTypeError("argerror")
def pytest_addoption(parser):
group = parser.getgroup('cov')
group.addoption(
"--invalid-option-should-allow-for-help",
type=validate,
)
"""
)
testdir.makeini(
"""
[pytest]
addopts = --invalid-option-should-allow-for-help
"""
)
result = testdir.runpytest("--help")
result.stdout.fnmatch_lines(
[
"usage: *",
"positional arguments:",
"NOTE: displaying only minimal help due to UsageError.",
]
)
result.stderr.fnmatch_lines(
[
"ERROR: usage: *",
"%s: error: argument --invalid-option-should-allow-for-help: expected one argument"
% (testdir.request.config._parser.optparser.prog,),
]
)
# Does not display full/default help.
assert "to see available markers type: pytest --markers" not in result.stdout.lines
assert result.ret == EXIT_USAGEERROR
result = testdir.runpytest("--version")
result.stderr.fnmatch_lines(
["*pytest*{}*imported from*".format(pytest.__version__)]
)
assert result.ret == EXIT_USAGEERROR

View File

@ -11,6 +11,7 @@ import py
import pytest
from _pytest.config import argparsing as parseopt
from _pytest.config.exceptions import UsageError
@pytest.fixture
@ -19,11 +20,9 @@ def parser():
class TestParser(object):
def test_no_help_by_default(self, capsys):
def test_no_help_by_default(self):
parser = parseopt.Parser(usage="xyz")
pytest.raises(SystemExit, lambda: parser.parse(["-h"]))
out, err = capsys.readouterr()
assert err.find("error: unrecognized arguments") != -1
pytest.raises(UsageError, lambda: parser.parse(["-h"]))
def test_custom_prog(self, parser):
"""Custom prog can be set for `argparse.ArgumentParser`."""