diff --git a/AUTHORS b/AUTHORS index 8d981ae9ad5..b01f4bd85a7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,6 +18,7 @@ Anthon van der Neut Anthony Sottile Antony Lee Armin Rigo +Aron Coyle Aron Curzon Aviv Palivoda Barney Gale diff --git a/_pytest/config.py b/_pytest/config.py index 22bf6c60c1d..a9ded208eb3 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -1188,16 +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 - for ini_config_list in self._override_ini: - for ini_config in ini_config_list: - try: - (key, user_ini_value) = ini_config.split("=", 1) - except ValueError: - 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 new file mode 100644 index 00000000000..4bdb23820e8 --- /dev/null +++ b/changelog/3103.bugfix @@ -0,0 +1 @@ +**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 44b8c317a28..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,10 +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_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.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([ + 'collected 1 item', + '*= 1 passed in *=', + ])