diff --git a/_pytest/config.py b/_pytest/config.py index 544c260eea4..a9ded208eb3 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -1188,19 +1188,15 @@ def _getconftest_pathlist(self, name, path): def _get_override_ini_value(self, name): value = None - # override_ini is a list of list, to support both -o foo1=bar1 foo2=bar2 and - # and -o foo1=bar1 -o foo2=bar2 options - # always use the last item if multiple value set for same ini-name, + # override_ini is a list of "ini=value" options + # always use the last item if multiple values are set for same ini-name, # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2 - first_override_set = False - for ini_config_list in self._override_ini: - for ini_config in ini_config_list: - try: - (key, user_ini_value) = ini_config.split("=", 1) - first_override_set = True - except ValueError: - if not first_override_set: - raise UsageError("-o/--override-ini expects option=value style.") + for ini_config in self._override_ini: + try: + key, user_ini_value = ini_config.split("=", 1) + except ValueError: + raise UsageError("-o/--override-ini expects option=value style.") + else: if key == name: value = user_ini_value return value diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py index e744637f866..5a81a5bd3b8 100644 --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -57,9 +57,9 @@ def pytest_addoption(parser): action="store_true", dest="debug", default=False, help="store internal tracing debug information in 'pytestdebug.log'.") group._addoption( - '-o', '--override-ini', nargs='*', dest="override_ini", + '-o', '--override-ini', dest="override_ini", action="append", - help="override config option with option=value style, e.g. `-o xfail_strict=True`.") + help='override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`.') @pytest.hookimpl(hookwrapper=True) diff --git a/changelog/3103.bugfix b/changelog/3103.bugfix index ec493eb4efd..4bdb23820e8 100644 --- a/changelog/3103.bugfix +++ b/changelog/3103.bugfix @@ -1 +1 @@ -Fix ``UsageError`` being raised when specifying ``-o/--override`` command-line option followed by a test path. +**Incompatible change**: ``-o/--override`` option no longer eats all the remaining options, which can lead to surprising behavior: for example, ``pytest -o foo=1 /path/to/test.py`` would fail because ``/path/to/test.py`` would be considered as part of the ``-o`` command-line argument. One consequence of this is that now multiple configuration overrides need multiple ``-o`` flags: ``pytest -o foo=1 -o bar=2``. diff --git a/doc/en/customize.rst b/doc/en/customize.rst index 8133704a52c..6edeedf98fb 100644 --- a/doc/en/customize.rst +++ b/doc/en/customize.rst @@ -152,11 +152,25 @@ above will show verbose output because ``-v`` overwrites ``-q``. Builtin configuration file options ---------------------------------------------- +Here is a list of builtin configuration options that may be written in a ``pytest.ini``, ``tox.ini`` or ``setup.cfg`` +file, usually located at the root of your repository. All options must be under a ``[pytest]`` section +(``[tool:pytest]`` for ``setup.cfg`` files). + +Configuration file options may be overwritten in the command-line by using ``-o/--override``, which can also be +passed multiple times. The expected format is ``name=value``. For example:: + + pytest -o console_output_style=classic -o cache_dir=/tmp/mycache + + .. confval:: minversion Specifies a minimal pytest version required for running tests. - minversion = 2.1 # will fail if we run with pytest-2.0 + .. code-block:: ini + + # content of pytest.ini + [pytest] + minversion = 3.0 # will fail if we run with pytest-2.8 .. confval:: addopts @@ -165,6 +179,7 @@ Builtin configuration file options .. code-block:: ini + # content of pytest.ini [pytest] addopts = --maxfail=2 -rf # exit after 2 failures, report fail info diff --git a/testing/test_config.py b/testing/test_config.py index 6ae42a82930..f5b11775af0 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -781,16 +781,18 @@ def pytest_addoption(parser): testdir.makeini(""" [pytest] custom_option_1=custom_option_1 - custom_option_2=custom_option_2""") + custom_option_2=custom_option_2 + """) testdir.makepyfile(""" def test_multiple_options(pytestconfig): prefix = "custom_option" for x in range(1, 5): ini_value=pytestconfig.getini("%s_%d" % (prefix, x)) - print('\\nini%d:%s' % (x, ini_value))""") + print('\\nini%d:%s' % (x, ini_value)) + """) result = testdir.runpytest( "--override-ini", 'custom_option_1=fulldir=/tmp/user1', - 'custom_option_2=url=/tmp/user2?a=b&d=e', + '-o', 'custom_option_2=url=/tmp/user2?a=b&d=e', "-o", 'custom_option_3=True', "-o", 'custom_option_4=no', "-s") result.stdout.fnmatch_lines(["ini1:fulldir=/tmp/user1", @@ -853,24 +855,42 @@ def test_with_existing_file_in_subdir(self, tmpdir): assert rootdir == tmpdir assert inifile is None - def test_addopts_before_initini(self, testdir, tmpdir, monkeypatch): + def test_addopts_before_initini(self, monkeypatch): cache_dir = '.custom_cache' monkeypatch.setenv('PYTEST_ADDOPTS', '-o cache_dir=%s' % cache_dir) from _pytest.config import get_config config = get_config() config._preparse([], addopts=True) - assert config._override_ini == [['cache_dir=%s' % cache_dir]] + assert config._override_ini == ['cache_dir=%s' % cache_dir] - def test_no_error_if_true_first_key_value_pair(self, testdir, request): + def test_override_ini_does_not_contain_paths(self): + """Check that -o no longer swallows all options after it (#3103)""" + from _pytest.config import get_config + config = get_config() + config._preparse(['-o', 'cache_dir=/cache', '/some/test/path']) + assert config._override_ini == ['cache_dir=/cache'] + + def test_multiple_override_ini_options(self, testdir, request): """Ensure a file path following a '-o' option does not generate an error (#3103)""" - testdir.makeini(""" - [pytest] - xdist_strict=False - """) - testdir.makepyfile(""" - def test(): - pass - """) - result = testdir.runpytest('--override-ini', 'xdist_strict=True', '{}.py'.format(request.node.name)) + testdir.makepyfile(**{ + "conftest.py": """ + def pytest_addoption(parser): + parser.addini('foo', default=None, help='some option') + parser.addini('bar', default=None, help='some option') + """, + "test_foo.py": """ + def test(pytestconfig): + assert pytestconfig.getini('foo') == '1' + assert pytestconfig.getini('bar') == '0' + """, + "test_bar.py": """ + def test(): + assert False + """, + }) + result = testdir.runpytest('-o', 'foo=1', '-o', 'bar=0', 'test_foo.py') assert 'ERROR:' not in result.stderr.str() - result.stdout.fnmatch_lines('* 1 passed in *') + result.stdout.fnmatch_lines([ + 'collected 1 item', + '*= 1 passed in *=', + ])