From 5b88f77293e074af48c401fef7a6b1ca2cba3e3a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 16 Oct 2019 00:19:18 +0200 Subject: [PATCH] config: addini: support choices --- src/_pytest/config/__init__.py | 35 ++++++++++++++++++++++-- src/_pytest/config/argparsing.py | 24 ++++++++++------- src/_pytest/helpconfig.py | 2 +- testing/test_config.py | 46 ++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 12 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 3164c81ba93..c8dc695d82e 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -915,6 +915,25 @@ def _preparse(self, args, addopts=True): else: raise + if ns.override_ini: + for ini_config in ns.override_ini: + try: + key, user_ini_value = ini_config.split("=", 1) + except ValueError: + raise UsageError("-o/--override-ini expects option=value style.") + else: + try: + _, type, _, choices = self._parser._inidict[key] + except KeyError: + raise UsageError( + "unknown configurations ini value: {!r}".format(key) + ) + if type == "choice": + if user_ini_value not in choices: + raise UsageError( + "invalid value for {}: {!r}".format(key, user_ini_value) + ) + def _checkversion(self): import pytest @@ -978,7 +997,7 @@ def getini(self, name: str): def _getini(self, name: str) -> Any: try: - description, type, default = self._parser._inidict[name] + description, type, default, choices = self._parser._inidict[name] except KeyError: raise ValueError("unknown configuration value: {!r}".format(name)) value = self._get_override_ini_value(name) @@ -1003,6 +1022,8 @@ def _getini(self, name: str) -> Any: return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] elif type == "bool": return bool(_strtobool(value.strip())) + elif type == "choice": + return self._validate_ini_value(name, value) else: assert type is None return value @@ -1033,7 +1054,17 @@ def _get_override_ini_value(self, name: str) -> Optional[str]: raise UsageError("-o/--override-ini expects option=value style.") else: if key == name: - value = user_ini_value + value = self._validate_ini_value(name, user_ini_value) + return value + + def _validate_ini_value(self, name: str, value: str) -> str: + try: + _, type, _, choices = self._parser._inidict[name] + except KeyError: + raise UsageError("unknown configurations ini value: {!r}".format(name)) + if type == "choice": + if value not in choices: + raise UsageError("invalid value for {}: {!r}".format(name, value)) return value def getoption(self, name: str, default=notset, skip: bool = False): diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 9b526ff3e1f..ad2093e68a6 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -29,7 +29,9 @@ def __init__(self, usage=None, processopt=None): self._groups = [] # type: List[OptionGroup] self._processopt = processopt self._usage = usage - self._inidict = {} # type: Dict[str, Tuple[str, Optional[str], Any]] + self._inidict = ( + {} + ) # type: Dict[str, Tuple[str, Optional[str], Any, Optional[List]]] self._ininames = [] # type: List[str] self.extra_info = {} # type: Dict[str, Any] @@ -65,9 +67,8 @@ def addoption(self, *opts, **attrs): """ register a command line option. :opts: option names, can be short or long options. - :attrs: same attributes which the ``add_option()`` function of the - `argparse library - `_ + :attrs: same attributes which the ``add_argument()`` function of the + `argparse library `_ accepts. After command line parsing options are available on the pytest config @@ -127,19 +128,24 @@ def parse_known_and_unknown_args( args = [str(x) if isinstance(x, py.path.local) else x for x in args] return optparser.parse_known_args(args, namespace=namespace) - def addini(self, name, help, type=None, default=None): + def addini(self, name, help, type=None, default=None, choices=None): """ register an ini-file option. :name: name of the ini-variable - :type: type of the variable, can be ``pathlist``, ``args``, ``linelist`` - or ``bool``. + :type: type of the variable, can be ``pathlist``, ``args``, ``linelist``, + ``bool``, or ``type``. :default: default value if no ini-file option exists but is queried. The value of ini-variables can be retrieved via a call to :py:func:`config.getini(name) <_pytest.config.Config.getini>`. """ - assert type in (None, "pathlist", "args", "linelist", "bool") - self._inidict[name] = (help, type, default) + # TODO: callable type? + if type == "choice": + if choices is None: + raise ValueError("need to pass choices with type=choice") + else: + assert type in (None, "pathlist", "args", "linelist", "bool") + self._inidict[name] = (help, type, default, choices) self._ininames.append(name) diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 50acc2d7d86..f2b125941c2 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -152,7 +152,7 @@ def showhelp(config): indent_len = 24 # based on argparse's max_help_position=24 indent = " " * indent_len for name in config._parser._ininames: - help, type, default = config._parser._inidict[name] + help, type, default, choices = config._parser._inidict[name] if type is None: type = "string" spec = "{} ({}):".format(name, type) diff --git a/testing/test_config.py b/testing/test_config.py index 71dae5c4cdb..a59fa6f1b8c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -365,6 +365,52 @@ def pytest_addoption(parser): config = testdir.parseconfig() assert config.getini("strip") is bool_val + def test_addini_choices(self, testdir): + testdir.makeconftest( + """ + def pytest_addoption(parser): + parser.addini("mychoice", "", type="choice", default="def", choices=["one", "two"]) + """ + ) + config = testdir.parseconfig() + assert config.getini("mychoice") == "def" + + testdir.makeini( + """ + [pytest] + mychoice=invalid + """ + ) + config = testdir.parseconfig() + with pytest.raises(UsageError, match="invalid value for mychoice: 'invalid'"): + config.getini("mychoice") + + p1 = testdir.makepyfile( + """ + def test_pass(pytestconfig): + ini_val = pytestconfig.getini("mychoice") + print('\\nmychoice:%s\\n' % ini_val)""" + ) + result = testdir.runpytest("-s", str(p1)) + result.stdout.fnmatch_lines( + ["E *UsageError: invalid value for mychoice: 'invalid'", "*= 1 failed in *"] + ) + assert result.ret == ExitCode.TESTS_FAILED + + p1 = testdir.makepyfile( + """ + def test_pass(pytestconfig): + ini_val = pytestconfig.getini("mychoice") + print('\\nmychoice:%s\\n' % ini_val)""" + ) + result = testdir.runpytest("-s", "-o", "mychoice=one", str(p1)) + result.stdout.fnmatch_lines(["mychoice:one"]) + assert result.ret == ExitCode.OK + + result = testdir.runpytest("-o", "mychoice=invalid2", str(p1)) + result.stderr.fnmatch_lines(["ERROR: invalid value for mychoice: 'invalid2'"]) + assert result.ret == ExitCode.USAGE_ERROR + def test_addinivalue_line_existing(self, testdir): testdir.makeconftest( """