From 2ccda38acbc3df03961de843f44296a44c1dca54 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 23 May 2020 13:48:37 -0300 Subject: [PATCH 01/16] Refactor getcfg function for clarity This makes it clear each type of file that it is supported. Also dropped 'config' parameter as it is no longer used. --- src/_pytest/config/__init__.py | 11 +--- src/_pytest/config/findpaths.py | 99 +++++++++++++++++++++++---------- testing/test_config.py | 6 +- 3 files changed, 77 insertions(+), 39 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 343cdd960ff..f8628498a14 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1064,13 +1064,8 @@ def _checkversion(self): if Version(minver) > Version(pytest.__version__): raise pytest.UsageError( - "%s:%d: requires pytest-%s, actual pytest-%s'" - % ( - self.inicfg.config.path, - self.inicfg.lineof("minversion"), - minver, - pytest.__version__, - ) + "%s: 'minversion' requires pytest-%s, actual pytest-%s'" + % (self.inifile, minver, pytest.__version__,) ) def _validatekeys(self): @@ -1144,7 +1139,7 @@ def _getini(self, name: str) -> Any: return "" return [] if type == "pathlist": - dp = py.path.local(self.inicfg.config.path).dirpath() + dp = py.path.local(self.inifile).dirpath() values = [] for relpath in shlex.split(value): values.append(dp.join(relpath, abs=True)) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 2b252c4f474..2feddd6eace 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -1,5 +1,6 @@ import os from typing import Any +from typing import Dict from typing import Iterable from typing import List from typing import Optional @@ -24,44 +25,84 @@ def exists(path, ignore=OSError): return False -def getcfg(args, config=None): +def _parse_ini_config(path: py.path.local) -> py.iniconfig.IniConfig: + """Parses the given generic '.ini' file using legacy IniConfig parser, returning + the parsed object. + + Raises UsageError if the file cannot be parsed. + """ + try: + return py.iniconfig.IniConfig(path) + except py.iniconfig.ParseError as exc: + raise UsageError(str(exc)) + + +def _get_ini_config_from_pytest_ini(path: py.path.local) -> Optional[Dict[str, Any]]: + """Parses and validates a 'pytest.ini' file. + + If present, 'pytest.ini' files are always considered the source of truth of pytest + configuration, even if empty or without a "[pytest]" section. + """ + iniconfig = _parse_ini_config(path) + if "pytest" in iniconfig: + return dict(iniconfig["pytest"].items()) + else: + return {} + + +def _get_ini_config_from_tox_ini(path: py.path.local) -> Optional[Dict[str, Any]]: + """Parses and validates a 'tox.ini' file for pytest configuration. + + 'tox.ini' files are only considered for pytest configuration if they contain a "[pytest]" + section. + """ + iniconfig = _parse_ini_config(path) + if "pytest" in iniconfig: + return dict(iniconfig["pytest"].items()) + else: + return None + + +def _get_ini_config_from_setup_cfg(path: py.path.local) -> Optional[Dict[str, Any]]: + """Parses and validates a 'setup.cfg' file for pytest configuration. + + 'setup.cfg' files are only considered for pytest configuration if they contain a "[tool:pytest]" + section. + + If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that + plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). + """ + iniconfig = _parse_ini_config(path) + + if "tool:pytest" in iniconfig.sections: + return dict(iniconfig["tool:pytest"].items()) + elif "pytest" in iniconfig.sections: + fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) + return None + + +def getcfg(args): """ Search the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict). - - note: config is optional and used only to issue warnings explicitly (#2891). """ - inibasenames = ["pytest.ini", "tox.ini", "setup.cfg"] + ini_names_and_handlers = [ + ("pytest.ini", _get_ini_config_from_pytest_ini), + ("tox.ini", _get_ini_config_from_tox_ini), + ("setup.cfg", _get_ini_config_from_setup_cfg), + ] args = [x for x in args if not str(x).startswith("-")] if not args: args = [py.path.local()] for arg in args: arg = py.path.local(arg) for base in arg.parts(reverse=True): - for inibasename in inibasenames: + for inibasename, handler in ini_names_and_handlers: p = base.join(inibasename) - if exists(p): - try: - iniconfig = IniConfig(p) - except ParseError as exc: - raise UsageError(str(exc)) - - if ( - inibasename == "setup.cfg" - and "tool:pytest" in iniconfig.sections - ): - return base, p, iniconfig["tool:pytest"] - elif "pytest" in iniconfig.sections: - if inibasename == "setup.cfg" and config is not None: - - fail( - CFG_PYTEST_SECTION.format(filename=inibasename), - pytrace=False, - ) - return base, p, iniconfig["pytest"] - elif inibasename == "pytest.ini": - # allowed to be empty - return base, p, {} + if p.isfile(): + ini_config = handler(p) + if ini_config is not None: + return base, p, ini_config return None, None, None @@ -140,7 +181,7 @@ def determine_setup( rootdir = get_common_ancestor(dirs) else: ancestor = get_common_ancestor(dirs) - rootdir, inifile, inicfg = getcfg([ancestor], config=config) + rootdir, inifile, inicfg = getcfg([ancestor]) if rootdir is None and rootdir_cmd_arg is None: for possible_rootdir in ancestor.parts(reverse=True): if possible_rootdir.join("setup.py").exists(): @@ -148,7 +189,7 @@ def determine_setup( break else: if dirs != [ancestor]: - rootdir, inifile, inicfg = getcfg(dirs, config=config) + rootdir, inifile, inicfg = getcfg(dirs) if rootdir is None: if config is not None: cwd = config.invocation_dir diff --git a/testing/test_config.py b/testing/test_config.py index c102202edd8..519652b23ea 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -82,12 +82,14 @@ def test_tox_ini_wrong_version(self, testdir): ".ini", tox=""" [pytest] - minversion=9.0 + minversion=999.0 """, ) result = testdir.runpytest() assert result.ret != 0 - result.stderr.fnmatch_lines(["*tox.ini:2*requires*9.0*actual*"]) + result.stderr.fnmatch_lines( + ["*tox.ini: 'minversion' requires pytest-999.0, actual pytest-*"] + ) @pytest.mark.parametrize( "section, name", From d37b01f92593fb97921fc5492b6b8678084d2101 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 23 May 2020 14:48:43 -0300 Subject: [PATCH 02/16] Add support for configuration in pyproject.toml Fix #1556 --- setup.cfg | 1 + src/_pytest/config/__init__.py | 18 ++++++--- src/_pytest/config/findpaths.py | 28 +++++++++++++ src/_pytest/pytester.py | 7 ++++ testing/test_config.py | 71 +++++++++++++++++++++++++++++---- 5 files changed, 112 insertions(+), 13 deletions(-) diff --git a/setup.cfg b/setup.cfg index a7dd6d1c310..6831c4b1535 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,7 @@ install_requires = colorama;sys_platform=="win32" importlib-metadata>=0.12;python_version<"3.8" pathlib2>=2.2.0;python_version<"3.6" + toml python_requires = >=3.5 package_dir = =src diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index f8628498a14..fe904842713 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1138,16 +1138,22 @@ def _getini(self, name: str) -> Any: if type is None: return "" return [] + # coerce the values based on types + # note: some coercions are only required if we are reading from .ini files, because + # the file format doesn't contain type information; toml files however support + # data types and complex types such as lists directly, so many conversions are not + # necessary if type == "pathlist": dp = py.path.local(self.inifile).dirpath() - values = [] - for relpath in shlex.split(value): - values.append(dp.join(relpath, abs=True)) - return values + input_values = shlex.split(value) if isinstance(value, str) else value + return [dp.join(x, abs=True) for x in input_values] elif type == "args": - return shlex.split(value) + return shlex.split(value) if isinstance(value, str) else value elif type == "linelist": - return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] + if isinstance(value, str): + return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] + else: + return value elif type == "bool": return bool(_strtobool(value.strip())) else: diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 2feddd6eace..94d7cf74486 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -81,6 +81,33 @@ def _get_ini_config_from_setup_cfg(path: py.path.local) -> Optional[Dict[str, An return None +def _get_ini_config_from_pyproject_toml( + path: py.path.local, +) -> Optional[Dict[str, Any]]: + """Parses and validates a 'setup.cfg' file for pytest configuration. + + 'setup.cfg' files are only considered for pytest configuration if they contain a "[tool:pytest]" + section. + + If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that + plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). + """ + import toml + + config = toml.load(path) + + result = config.get("tool", {}).get("pytest", {}).get("ini", None) + if result is not None: + # convert all scalar values to strings for compatibility with other ini formats + # conversion to actual useful values is made by Config._getini + def make_scalar(v): + return v if isinstance(v, (list, tuple)) else str(v) + + return {k: make_scalar(v) for k, v in result.items()} + else: + return None + + def getcfg(args): """ Search the list of arguments for a valid ini-file for pytest, @@ -90,6 +117,7 @@ def getcfg(args): ("pytest.ini", _get_ini_config_from_pytest_ini), ("tox.ini", _get_ini_config_from_tox_ini), ("setup.cfg", _get_ini_config_from_setup_cfg), + ("pyproject.toml", _get_ini_config_from_pyproject_toml), ] args = [x for x in args if not str(x).startswith("-")] if not args: diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 3c81dd759bc..6c56518cd60 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -686,6 +686,13 @@ def getinicfg(self, source): p = self.makeini(source) return IniConfig(p)["pytest"] + def makepyprojecttoml(self, source): + """Write a pyproject.toml file with 'source' as contents. + + .. versionadded:: 6.0 + """ + return self.makefile(".toml", pyproject=source) + def makepyfile(self, *args, **kwargs): r"""Shortcut for .makefile() with a .py extension. Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting diff --git a/testing/test_config.py b/testing/test_config.py index 519652b23ea..864ea5abab8 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -109,6 +109,16 @@ def test_ini_names(self, testdir, name, section): config = testdir.parseconfig() assert config.getini("minversion") == "1.0" + def test_pyproject_toml(self, testdir): + testdir.makepyprojecttoml( + """ + [tool.pytest.ini] + minversion = "1.0" + """ + ) + config = testdir.parseconfig() + assert config.getini("minversion") == "1.0" + def test_toxini_before_lower_pytestini(self, testdir): sub = testdir.tmpdir.mkdir("sub") sub.join("tox.ini").write( @@ -349,7 +359,7 @@ def pytest_addoption(parser): assert val == "hello" pytest.raises(ValueError, config.getini, "other") - def test_addini_pathlist(self, testdir): + def make_conftest_for_pathlist(self, testdir): testdir.makeconftest( """ def pytest_addoption(parser): @@ -357,20 +367,36 @@ def pytest_addoption(parser): parser.addini("abc", "abc value") """ ) + + def test_addini_pathlist_ini_files(self, testdir): + self.make_conftest_for_pathlist(testdir) p = testdir.makeini( """ [pytest] paths=hello world/sub.py """ ) + self.check_config_pathlist(testdir, p) + + def test_addini_pathlist_pyproject_toml(self, testdir): + self.make_conftest_for_pathlist(testdir) + p = testdir.makepyprojecttoml( + """ + [tool.pytest.ini] + paths=["hello", "world/sub.py"] + """ + ) + self.check_config_pathlist(testdir, p) + + def check_config_pathlist(self, testdir, config_path): config = testdir.parseconfig() values = config.getini("paths") assert len(values) == 2 - assert values[0] == p.dirpath("hello") - assert values[1] == p.dirpath("world/sub.py") + assert values[0] == config_path.dirpath("hello") + assert values[1] == config_path.dirpath("world/sub.py") pytest.raises(ValueError, config.getini, "other") - def test_addini_args(self, testdir): + def make_conftest_for_args(self, testdir): testdir.makeconftest( """ def pytest_addoption(parser): @@ -378,20 +404,35 @@ def pytest_addoption(parser): parser.addini("a2", "", "args", default="1 2 3".split()) """ ) + + def test_addini_args_ini_files(self, testdir): + self.make_conftest_for_args(testdir) testdir.makeini( """ [pytest] args=123 "123 hello" "this" - """ + """ ) + self.check_config_args(testdir) + + def test_addini_args_pyproject_toml(self, testdir): + self.make_conftest_for_args(testdir) + testdir.makepyprojecttoml( + """ + [tool.pytest.ini] + args = ["123", "123 hello", "this"] + """ + ) + self.check_config_args(testdir) + + def check_config_args(self, testdir): config = testdir.parseconfig() values = config.getini("args") - assert len(values) == 3 assert values == ["123", "123 hello", "this"] values = config.getini("a2") assert values == list("123") - def test_addini_linelist(self, testdir): + def make_conftest_for_linelist(self, testdir): testdir.makeconftest( """ def pytest_addoption(parser): @@ -399,6 +440,9 @@ def pytest_addoption(parser): parser.addini("a2", "", "linelist") """ ) + + def test_addini_linelist_ini_files(self, testdir): + self.make_conftest_for_linelist(testdir) testdir.makeini( """ [pytest] @@ -406,6 +450,19 @@ def pytest_addoption(parser): second line """ ) + self.check_config_linelist(testdir) + + def test_addini_linelist_pprojecttoml(self, testdir): + self.make_conftest_for_linelist(testdir) + testdir.makepyprojecttoml( + """ + [tool.pytest.ini] + xy = ["123 345", "second line"] + """ + ) + self.check_config_linelist(testdir) + + def check_config_linelist(self, testdir): config = testdir.parseconfig() values = config.getini("xy") assert len(values) == 2 From b5fce14366ed6c63284f1b263f2bc6cdae7125eb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 1 Jun 2020 21:26:14 -0300 Subject: [PATCH 03/16] Adjust to tool.pytest.ini_options --- src/_pytest/config/findpaths.py | 13 +++++-------- testing/test_config.py | 8 ++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 94d7cf74486..d6bf6e9b774 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -84,19 +84,16 @@ def _get_ini_config_from_setup_cfg(path: py.path.local) -> Optional[Dict[str, An def _get_ini_config_from_pyproject_toml( path: py.path.local, ) -> Optional[Dict[str, Any]]: - """Parses and validates a 'setup.cfg' file for pytest configuration. - - 'setup.cfg' files are only considered for pytest configuration if they contain a "[tool:pytest]" - section. + """Parses and validates a ``pyproject.toml`` file for pytest configuration. - If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that - plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). + The ``[tool.pytest]`` table is used by pytest. If the file contains that section, + it is used as the config file. """ import toml config = toml.load(path) - result = config.get("tool", {}).get("pytest", {}).get("ini", None) + result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) if result is not None: # convert all scalar values to strings for compatibility with other ini formats # conversion to actual useful values is made by Config._getini @@ -115,9 +112,9 @@ def getcfg(args): """ ini_names_and_handlers = [ ("pytest.ini", _get_ini_config_from_pytest_ini), + ("pyproject.toml", _get_ini_config_from_pyproject_toml), ("tox.ini", _get_ini_config_from_tox_ini), ("setup.cfg", _get_ini_config_from_setup_cfg), - ("pyproject.toml", _get_ini_config_from_pyproject_toml), ] args = [x for x in args if not str(x).startswith("-")] if not args: diff --git a/testing/test_config.py b/testing/test_config.py index 864ea5abab8..319d594cef7 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -112,7 +112,7 @@ def test_ini_names(self, testdir, name, section): def test_pyproject_toml(self, testdir): testdir.makepyprojecttoml( """ - [tool.pytest.ini] + [tool.pytest.ini_options] minversion = "1.0" """ ) @@ -382,7 +382,7 @@ def test_addini_pathlist_pyproject_toml(self, testdir): self.make_conftest_for_pathlist(testdir) p = testdir.makepyprojecttoml( """ - [tool.pytest.ini] + [tool.pytest.ini_options] paths=["hello", "world/sub.py"] """ ) @@ -419,7 +419,7 @@ def test_addini_args_pyproject_toml(self, testdir): self.make_conftest_for_args(testdir) testdir.makepyprojecttoml( """ - [tool.pytest.ini] + [tool.pytest.ini_options] args = ["123", "123 hello", "this"] """ ) @@ -456,7 +456,7 @@ def test_addini_linelist_pprojecttoml(self, testdir): self.make_conftest_for_linelist(testdir) testdir.makepyprojecttoml( """ - [tool.pytest.ini] + [tool.pytest.ini_options] xy = ["123 345", "second line"] """ ) From d99d22c8e20f785c8af5c576bf7d6779fe7ee32a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 1 Jun 2020 21:29:10 -0300 Subject: [PATCH 04/16] Change "inifile:" to "configfile:" in the terminal While setup.cfg might be considered an "inifile", "pyproject.toml" definitely is not. --- src/_pytest/terminal.py | 2 +- testing/test_config.py | 2 +- testing/test_terminal.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index e384e02b204..ed635916c54 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -643,7 +643,7 @@ def pytest_report_header(self, config): line = "rootdir: %s" % config.rootdir if config.inifile: - line += ", inifile: " + config.rootdir.bestrelpath(config.inifile) + line += ", configfile: " + config.rootdir.bestrelpath(config.inifile) testpaths = config.getini("testpaths") if testpaths and config.args == testpaths: diff --git a/testing/test_config.py b/testing/test_config.py index 319d594cef7..8f5cc79c2db 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -58,7 +58,7 @@ def test_setupcfg_uses_toolpytest_with_pytest(self, testdir): % p1.basename, ) result = testdir.runpytest() - result.stdout.fnmatch_lines(["*, inifile: setup.cfg, *", "* 1 passed in *"]) + result.stdout.fnmatch_lines(["*, configfile: setup.cfg, *", "* 1 passed in *"]) assert result.ret == 0 def test_append_parse_args(self, testdir, tmpdir, monkeypatch): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 17fd29238f7..5dab9618817 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -703,10 +703,10 @@ def test_header(self, testdir): result = testdir.runpytest() result.stdout.fnmatch_lines(["rootdir: *test_header0"]) - # with inifile + # with configfile testdir.makeini("""[pytest]""") result = testdir.runpytest() - result.stdout.fnmatch_lines(["rootdir: *test_header0, inifile: tox.ini"]) + result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"]) # with testpaths option, and not passing anything in the command-line testdir.makeini( @@ -717,12 +717,12 @@ def test_header(self, testdir): ) result = testdir.runpytest() result.stdout.fnmatch_lines( - ["rootdir: *test_header0, inifile: tox.ini, testpaths: tests, gui"] + ["rootdir: *test_header0, configfile: tox.ini, testpaths: tests, gui"] ) # with testpaths option, passing directory in command-line: do not show testpaths then result = testdir.runpytest("tests") - result.stdout.fnmatch_lines(["rootdir: *test_header0, inifile: tox.ini"]) + result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"]) def test_showlocals(self, testdir): p1 = testdir.makepyfile( From 2a16e63b50c225f8b19e948d8b147443a7e7f36b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 1 Jun 2020 21:34:00 -0300 Subject: [PATCH 05/16] Use pyproject.toml for configuration in pytest itself --- pyproject.toml | 43 +++++++++++++++++++++++++++++++++++++++++++ tox.ini | 42 ------------------------------------------ 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aa57762e75d..493213d841e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,49 @@ requires = [ ] build-backend = "setuptools.build_meta" +[tool.pytest.ini_options] +minversion = "2.0" +addopts = "-rfEX -p pytester --strict-markers" +python_files = ["test_*.py", "*_test.py", "testing/*/*.py"] +python_classes = ["Test", "Acceptance"] +python_functions = ["test"] +# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting". +testpaths = ["testing"] +norecursedirs = ["testing/example_scripts"] +xfail_strict = true +filterwarnings = [ + "error", + "default:Using or importing the ABCs:DeprecationWarning:unittest2.*", + "default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*", + "ignore:Module already imported so cannot be rewritten:pytest.PytestWarning", + # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8)." + "ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest))", + # produced by pytest-xdist + "ignore:.*type argument to addoption.*:DeprecationWarning", + # produced by python >=3.5 on execnet (pytest-xdist) + "ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning", + # pytest's own futurewarnings + "ignore::pytest.PytestExperimentalApiWarning", + # Do not cause SyntaxError for invalid escape sequences in py37. + # Those are caught/handled by pyupgrade, and not easy to filter with the + # module being the filename (with .py removed). + "default:invalid escape sequence:DeprecationWarning", + # ignore use of unregistered marks, because we use many to test the implementation + "ignore::_pytest.warning_types.PytestUnknownMarkWarning", +] +pytester_example_dir = "testing/example_scripts" +markers = [ + # dummy markers for testing + "foo", + "bar", + "baz", + # conftest.py reorders tests moving slow ones to the end of the list + "slow", + # experimental mark for all tests using pexpect + "uses_pexpect", +] + + [tool.towncrier] package = "pytest" package_dir = "src" diff --git a/tox.ini b/tox.ini index 8e1a51ca760..affb4a7a92c 100644 --- a/tox.ini +++ b/tox.ini @@ -152,48 +152,6 @@ deps = pypandoc commands = python scripts/publish-gh-release-notes.py {posargs} - -[pytest] -minversion = 2.0 -addopts = -rfEX -p pytester --strict-markers -rsyncdirs = tox.ini doc src testing -python_files = test_*.py *_test.py testing/*/*.py -python_classes = Test Acceptance -python_functions = test -# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting". -testpaths = testing -norecursedirs = testing/example_scripts -xfail_strict=true -filterwarnings = - error - default:Using or importing the ABCs:DeprecationWarning:unittest2.* - default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.* - ignore:Module already imported so cannot be rewritten:pytest.PytestWarning - # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8). - ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest)) - # produced by pytest-xdist - ignore:.*type argument to addoption.*:DeprecationWarning - # produced by python >=3.5 on execnet (pytest-xdist) - ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning - # pytest's own futurewarnings - ignore::pytest.PytestExperimentalApiWarning - # Do not cause SyntaxError for invalid escape sequences in py37. - # Those are caught/handled by pyupgrade, and not easy to filter with the - # module being the filename (with .py removed). - default:invalid escape sequence:DeprecationWarning - # ignore use of unregistered marks, because we use many to test the implementation - ignore::_pytest.warning_types.PytestUnknownMarkWarning -pytester_example_dir = testing/example_scripts -markers = - # dummy markers for testing - foo - bar - baz - # conftest.py reorders tests moving slow ones to the end of the list - slow - # experimental mark for all tests using pexpect - uses_pexpect - [flake8] max-line-length = 120 extend-ignore = E203 From 9aa068c0488b0f7cc3e09635779d908b04fc53c3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 1 Jun 2020 22:26:01 -0300 Subject: [PATCH 06/16] Add docs for pyproject.toml --- doc/en/customize.rst | 209 +++++++++++++++++----------- doc/en/example/pythoncollection.rst | 7 +- doc/en/example/simple.rst | 44 ++++++ doc/en/reference.rst | 14 +- src/_pytest/config/__init__.py | 2 +- 5 files changed, 182 insertions(+), 94 deletions(-) diff --git a/doc/en/customize.rst b/doc/en/customize.rst index 9554ab7b518..71e3e6642d7 100644 --- a/doc/en/customize.rst +++ b/doc/en/customize.rst @@ -14,15 +14,102 @@ configurations files by using the general help option: This will display command line and configuration file settings which were registered by installed plugins. +.. _`config file formats`: + +Configuration file formats +-------------------------- + +Many :ref:`pytest settings ` can be set in a *configuration file*, which +by convention resides on the root of your repository or in your +tests folder. + +A quick example of the configuration files supported by pytest: + +pytest.ini +~~~~~~~~~~ + +``pytest.ini`` files take precedence over other files, even when empty. + +.. code-block:: ini + + # pytest.ini + [pytest] + minversion = 6.0 + addopts = -ra -q + testpaths = + tests + integration + + +pyproject.toml +~~~~~~~~~~~~~~ + +.. versionadded:: 6.0 + +``pyproject.toml`` are considered for configuration when they contain a ``tool.pytest.ini_options`` table. + +.. code-block:: toml + + # pyproject.toml + [tool.pytest.ini_options] + minversion = "6.0" + addopts = "-ra -q" + testpaths = [ + "tests", + "integration", + ] + +tox.ini +~~~~~~~ + +``tox.ini`` files are the configuration files of the `tox `__ project, +and can also be used to hold pytest configuration if they have a ``[pytest]`` section. + +.. code-block:: ini + + # tox.ini + [pytest] + minversion = 6.0 + addopts = -ra -q + testpaths = + tests + integration + + +setup.cfg +~~~~~~~~~ + +``setup.cfg`` files are general purpose configuration files, used originally by `distutils `__, and can also be used to hold pytest configuration +if they have a ``[tool:pytest]`` section. + +.. code-block:: ini + + # setup.cfg + [tool:pytest] + minversion = 6.0 + addopts = -ra -q + testpaths = + tests + integration + +.. warning:: + + Usage of ``setup.cfg`` is not recommended unless for very simple use cases. ``.cfg`` + files use a different parser than ``pytest.ini`` and ``tox.ini`` which might cause hard to track + down problems. + When possible, it is recommended to use the latter files, or ``pyproject.toml``, to hold your + pytest configuration. + + .. _rootdir: -.. _inifiles: +.. _configfiles: -Initialization: determining rootdir and inifile ------------------------------------------------ +Initialization: determining rootdir and configfile +-------------------------------------------------- pytest determines a ``rootdir`` for each test run which depends on the command line arguments (specified test files, paths) and on -the existence of *ini-files*. The determined ``rootdir`` and *ini-file* are +the existence of configuration files. The determined ``rootdir`` and ``configfile`` are printed as part of the pytest header during startup. Here's a summary what ``pytest`` uses ``rootdir`` for: @@ -47,48 +134,47 @@ Finding the ``rootdir`` Here is the algorithm which finds the rootdir from ``args``: -- determine the common ancestor directory for the specified ``args`` that are +- Determine the common ancestor directory for the specified ``args`` that are recognised as paths that exist in the file system. If no such paths are found, the common ancestor directory is set to the current working directory. -- look for ``pytest.ini``, ``tox.ini`` and ``setup.cfg`` files in the ancestor - directory and upwards. If one is matched, it becomes the ini-file and its - directory becomes the rootdir. +- Look for ``pytest.ini``, ``pyproject.toml``, ``tox.ini``, and ``setup.cfg`` files in the ancestor + directory and upwards. If one is matched, it becomes the ``configfile`` and its + directory becomes the ``rootdir``. -- if no ini-file was found, look for ``setup.py`` upwards from the common +- If no configuration file was found, look for ``setup.py`` upwards from the common ancestor directory to determine the ``rootdir``. -- if no ``setup.py`` was found, look for ``pytest.ini``, ``tox.ini`` and +- If no ``setup.py`` was found, look for ``pytest.ini``, ``pyproject.toml``, ``tox.ini``, and ``setup.cfg`` in each of the specified ``args`` and upwards. If one is - matched, it becomes the ini-file and its directory becomes the rootdir. + matched, it becomes the ``configfile`` and its directory becomes the ``rootdir``. -- if no ini-file was found, use the already determined common ancestor as root +- If no ``configfile`` was found, use the already determined common ancestor as root directory. This allows the use of pytest in structures that are not part of - a package and don't have any particular ini-file configuration. + a package and don't have any particular configuration file. If no ``args`` are given, pytest collects test below the current working -directory and also starts determining the rootdir from there. +directory and also starts determining the ``rootdir`` from there. -:warning: custom pytest plugin commandline arguments may include a path, as in - ``pytest --log-output ../../test.log args``. Then ``args`` is mandatory, - otherwise pytest uses the folder of test.log for rootdir determination - (see also `issue 1435 `_). - A dot ``.`` for referencing to the current working directory is also - possible. +Files will only be matched for configuration if: + +* ``pytest.ini``: will always match and take precedence, even if empty. +* ``pyproject.toml``: contains a ``[tool.pytest.ini_options]`` table. +* ``tox.ini``: contains a ``[pytest]`` section. +* ``setup.cfg``: contains a ``[tool:pytest]`` section. -Note that an existing ``pytest.ini`` file will always be considered a match, -whereas ``tox.ini`` and ``setup.cfg`` will only match if they contain a -``[pytest]`` or ``[tool:pytest]`` section, respectively. Options from multiple ini-files candidates are never -merged - the first one wins (``pytest.ini`` always wins, even if it does not -contain a ``[pytest]`` section). +The files are considered in the order above. Options from multiple ``configfiles`` candidates +are never merged - the first match wins. -The ``config`` object will subsequently carry these attributes: +The internal :class:`Config <_pytest.config.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture) +will subsequently carry these attributes: - ``config.rootdir``: the determined root directory, guaranteed to exist. -- ``config.inifile``: the determined ini-file, may be ``None``. +- ``config.inifile``: the determined ``configfile``, may be ``None`` (it is named ``inifile`` + for historical reasons). -The rootdir is used as a reference directory for constructing test +The ``rootdir`` is used as a reference directory for constructing test addresses ("nodeids") and can be used also by plugins for storing per-testrun information. @@ -99,73 +185,36 @@ Example: pytest path/to/testdir path/other/ will determine the common ancestor as ``path`` and then -check for ini-files as follows: +check for configuration files as follows: .. code-block:: text # first look for pytest.ini files path/pytest.ini - path/tox.ini # must also contain [pytest] section to match - path/setup.cfg # must also contain [tool:pytest] section to match + path/pyproject.toml # must contain a [tool.pytest.ini_options] table to match + path/tox.ini # must contain [pytest] section to match + path/setup.cfg # must contain [tool:pytest] section to match pytest.ini - ... # all the way down to the root + ... # all the way up to the root # now look for setup.py path/setup.py setup.py - ... # all the way down to the root - - -.. _`how to change command line options defaults`: -.. _`adding default options`: - - - -How to change command line options defaults ------------------------------------------------- - -It can be tedious to type the same series of command line options -every time you use ``pytest``. For example, if you always want to see -detailed info on skipped and xfailed tests, as well as have terser "dot" -progress output, you can write it into a configuration file: - -.. code-block:: ini - - # content of pytest.ini or tox.ini - [pytest] - addopts = -ra -q - - # content of setup.cfg - [tool:pytest] - addopts = -ra -q + ... # all the way up to the root -Alternatively, you can set a ``PYTEST_ADDOPTS`` environment variable to add command -line options while the environment is in use: -.. code-block:: bash - - export PYTEST_ADDOPTS="-v" - -Here's how the command-line is built in the presence of ``addopts`` or the environment variable: - -.. code-block:: text - - $PYTEST_ADDOPTS - -So if the user executes in the command-line: - -.. code-block:: bash - - pytest -m slow +.. warning:: -The actual command line executed is: - -.. code-block:: bash + Custom pytest plugin commandline arguments may include a path, as in + ``pytest --log-output ../../test.log args``. Then ``args`` is mandatory, + otherwise pytest uses the folder of test.log for rootdir determination + (see also `issue 1435 `_). + A dot ``.`` for referencing to the current working directory is also + possible. - pytest -ra -q -v -m slow -Note that as usual for other command-line applications, in case of conflicting options the last one wins, so the example -above will show verbose output because ``-v`` overwrites ``-q``. +.. _`how to change command line options defaults`: +.. _`adding default options`: Builtin configuration file options diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index d8261a94928..30d106adab6 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -115,15 +115,13 @@ Changing naming conventions You can configure different naming conventions by setting the :confval:`python_files`, :confval:`python_classes` and -:confval:`python_functions` configuration options. +:confval:`python_functions` in your :ref:`configuration file `. Here is an example: .. code-block:: ini # content of pytest.ini # Example 1: have pytest look for "check" instead of "test" - # can also be defined in tox.ini or setup.cfg file, although the section - # name in setup.cfg files should be "tool:pytest" [pytest] python_files = check_*.py python_classes = Check @@ -165,8 +163,7 @@ You can check for multiple glob patterns by adding a space between the patterns: .. code-block:: ini # Example 2: have pytest look for files with "test" and "example" - # content of pytest.ini, tox.ini, or setup.cfg file (replace "pytest" - # with "tool:pytest" for setup.cfg) + # content of pytest.ini [pytest] python_files = test_*.py example_*.py diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 3282bbda584..d1a1ecdfc9d 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -3,6 +3,50 @@ Basic patterns and examples ========================================================== +How to change command line options defaults +------------------------------------------- + +It can be tedious to type the same series of command line options +every time you use ``pytest``. For example, if you always want to see +detailed info on skipped and xfailed tests, as well as have terser "dot" +progress output, you can write it into a configuration file: + +.. code-block:: ini + + # content of pytest.ini + [pytest] + addopts = -ra -q + + +Alternatively, you can set a ``PYTEST_ADDOPTS`` environment variable to add command +line options while the environment is in use: + +.. code-block:: bash + + export PYTEST_ADDOPTS="-v" + +Here's how the command-line is built in the presence of ``addopts`` or the environment variable: + +.. code-block:: text + + $PYTEST_ADDOPTS + +So if the user executes in the command-line: + +.. code-block:: bash + + pytest -m slow + +The actual command line executed is: + +.. code-block:: bash + + pytest -ra -q -v -m slow + +Note that as usual for other command-line applications, in case of conflicting options the last one wins, so the example +above will show verbose output because ``-v`` overwrites ``-q``. + + .. _request example: Pass different values to a test function, depending on command line options diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 7348636a2dd..326b3e52add 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1019,17 +1019,17 @@ UsageError Configuration 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). +Here is a list of builtin configuration options that may be written in a ``pytest.ini``, ``pyproject.toml``, ``tox.ini`` or ``setup.cfg`` +file, usually located at the root of your repository. To see each file format in details, see +:ref:`config file formats`. .. warning:: - Usage of ``setup.cfg`` is not recommended unless for very simple use cases. ``.cfg`` + Usage of ``setup.cfg`` is not recommended except for very simple use cases. ``.cfg`` files use a different parser than ``pytest.ini`` and ``tox.ini`` which might cause hard to track down problems. - When possible, it is recommended to use the latter files to hold your pytest configuration. + When possible, it is recommended to use the latter files, or ``pyproject.toml``, to hold your pytest configuration. -Configuration file options may be overwritten in the command-line by using ``-o/--override-ini``, which can also be +Configuration options may be overwritten in the command-line by using ``-o/--override-ini``, 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 @@ -1057,8 +1057,6 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: cache_dir - - Sets a directory where stores content of cache plugin. Default directory is ``.pytest_cache`` which is created in :ref:`rootdir `. Directory may be relative or absolute path. If setting relative path, then directory is created diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index fe904842713..2a689f43af8 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1113,7 +1113,7 @@ def addinivalue_line(self, name, line): x.append(line) # modifies the cached list inline def getini(self, name: str): - """ return configuration value from an :ref:`ini file `. If the + """ return configuration value from an :ref:`ini file `. If the specified name hasn't been registered through a prior :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>` call (usually from a plugin), a ValueError is raised. """ From d96a9fca41ef142b90f1a2276ef2cbaeec8256af Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 2 Jun 2020 16:08:26 -0300 Subject: [PATCH 07/16] Add CHANGELOG --- changelog/1556.feature.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 changelog/1556.feature.rst diff --git a/changelog/1556.feature.rst b/changelog/1556.feature.rst new file mode 100644 index 00000000000..402e772e674 --- /dev/null +++ b/changelog/1556.feature.rst @@ -0,0 +1,17 @@ +pytest now supports ``pyproject.toml`` files for configuration. + +The configuration options is similar to the one available in other formats, but must be defined +in a ``[tool.pytest.ini_options]`` table to be picked up by pytest: + +.. code-block:: toml + + # pyproject.toml + [tool.pytest.ini_options] + minversion = "6.0" + addopts = "-ra -q" + testpaths = [ + "tests", + "integration", + ] + +More information can be found `in the docs `__. From a745f088891ef5e90271d7ff9ca716cb8ca67b76 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Jun 2020 16:41:16 -0300 Subject: [PATCH 08/16] Apply more suggestions --- src/_pytest/config/__init__.py | 15 ++++++++++--- src/_pytest/config/findpaths.py | 38 +++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 2a689f43af8..3d3b9509d1f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1140,9 +1140,18 @@ def _getini(self, name: str) -> Any: return [] # coerce the values based on types # note: some coercions are only required if we are reading from .ini files, because - # the file format doesn't contain type information; toml files however support - # data types and complex types such as lists directly, so many conversions are not - # necessary + # the file format doesn't contain type information, but when reading from toml we will + # get either str or list of str values (see _parse_ini_config_from_pyproject_toml). + # for example: + # + # ini: + # a_line_list = "tests acceptance" + # in this case, we need to split the string to obtain a list of strings + # + # toml: + # a_line_list = ["tests", "acceptance"] + # in this case, we already have a list ready to use + # if type == "pathlist": dp = py.path.local(self.inifile).dirpath() input_values = shlex.split(value) if isinstance(value, str) else value diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index d6bf6e9b774..d017978178b 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -5,6 +5,7 @@ from typing import List from typing import Optional from typing import Tuple +from typing import Union import py from iniconfig import IniConfig @@ -37,7 +38,7 @@ def _parse_ini_config(path: py.path.local) -> py.iniconfig.IniConfig: raise UsageError(str(exc)) -def _get_ini_config_from_pytest_ini(path: py.path.local) -> Optional[Dict[str, Any]]: +def _parse_ini_config_from_pytest_ini(path: py.path.local) -> Optional[Dict[str, str]]: """Parses and validates a 'pytest.ini' file. If present, 'pytest.ini' files are always considered the source of truth of pytest @@ -50,7 +51,7 @@ def _get_ini_config_from_pytest_ini(path: py.path.local) -> Optional[Dict[str, A return {} -def _get_ini_config_from_tox_ini(path: py.path.local) -> Optional[Dict[str, Any]]: +def _parse_ini_config_from_tox_ini(path: py.path.local) -> Optional[Dict[str, str]]: """Parses and validates a 'tox.ini' file for pytest configuration. 'tox.ini' files are only considered for pytest configuration if they contain a "[pytest]" @@ -63,7 +64,7 @@ def _get_ini_config_from_tox_ini(path: py.path.local) -> Optional[Dict[str, Any] return None -def _get_ini_config_from_setup_cfg(path: py.path.local) -> Optional[Dict[str, Any]]: +def _parse_ini_config_from_setup_cfg(path: py.path.local) -> Optional[Dict[str, str]]: """Parses and validates a 'setup.cfg' file for pytest configuration. 'setup.cfg' files are only considered for pytest configuration if they contain a "[tool:pytest]" @@ -81,13 +82,18 @@ def _get_ini_config_from_setup_cfg(path: py.path.local) -> Optional[Dict[str, An return None -def _get_ini_config_from_pyproject_toml( +def _parse_ini_config_from_pyproject_toml( path: py.path.local, -) -> Optional[Dict[str, Any]]: +) -> Optional[Dict[str, Union[str, List[str]]]]: """Parses and validates a ``pyproject.toml`` file for pytest configuration. The ``[tool.pytest]`` table is used by pytest. If the file contains that section, it is used as the config file. + + Note: toml supports richer data types than ini files (strings, arrays, floats, ints, etc), + however we need to convert all scalar values to str for compatibility with the rest + of the configuration system, which expects strings only. We needed to change the + handling of ini values in Config as to at least leave lists intact. """ import toml @@ -95,10 +101,10 @@ def _get_ini_config_from_pyproject_toml( result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) if result is not None: - # convert all scalar values to strings for compatibility with other ini formats - # conversion to actual useful values is made by Config._getini - def make_scalar(v): - return v if isinstance(v, (list, tuple)) else str(v) + # convert all scalar values to strings for compatibility with other ini formats; + # conversion to useful values is made by Config._getini + def make_scalar(v: Any) -> Union[str, List[str]]: + return v if isinstance(v, list) else str(v) return {k: make_scalar(v) for k, v in result.items()} else: @@ -110,11 +116,11 @@ def getcfg(args): Search the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict). """ - ini_names_and_handlers = [ - ("pytest.ini", _get_ini_config_from_pytest_ini), - ("pyproject.toml", _get_ini_config_from_pyproject_toml), - ("tox.ini", _get_ini_config_from_tox_ini), - ("setup.cfg", _get_ini_config_from_setup_cfg), + ini_names_and_parsers = [ + ("pytest.ini", _parse_ini_config_from_pytest_ini), + ("pyproject.toml", _parse_ini_config_from_pyproject_toml), + ("tox.ini", _parse_ini_config_from_tox_ini), + ("setup.cfg", _parse_ini_config_from_setup_cfg), ] args = [x for x in args if not str(x).startswith("-")] if not args: @@ -122,10 +128,10 @@ def getcfg(args): for arg in args: arg = py.path.local(arg) for base in arg.parts(reverse=True): - for inibasename, handler in ini_names_and_handlers: + for inibasename, parser in ini_names_and_parsers: p = base.join(inibasename) if p.isfile(): - ini_config = handler(p) + ini_config = parser(p) if ini_config is not None: return base, p, ini_config return None, None, None From 62aff27d3b550bdaf650a4f11f5a3b9a1689904b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Jun 2020 17:35:34 -0300 Subject: [PATCH 09/16] Support specifying a pyproject.toml file using -c --- src/_pytest/config/findpaths.py | 131 ++++++++++++-------------------- testing/test_config.py | 53 +++++++++++-- 2 files changed, 94 insertions(+), 90 deletions(-) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index d017978178b..8e3f25552ed 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -38,89 +38,67 @@ def _parse_ini_config(path: py.path.local) -> py.iniconfig.IniConfig: raise UsageError(str(exc)) -def _parse_ini_config_from_pytest_ini(path: py.path.local) -> Optional[Dict[str, str]]: - """Parses and validates a 'pytest.ini' file. +def load_config_dict_from_file( + filepath: py.path.local, +) -> Optional[Dict[str, Union[str, List[str]]]]: + """Loads pytest configuration from the given file path, if supported. - If present, 'pytest.ini' files are always considered the source of truth of pytest - configuration, even if empty or without a "[pytest]" section. + Return None if the file does not contain valid pytest configuration. """ - iniconfig = _parse_ini_config(path) - if "pytest" in iniconfig: - return dict(iniconfig["pytest"].items()) - else: - return {} - -def _parse_ini_config_from_tox_ini(path: py.path.local) -> Optional[Dict[str, str]]: - """Parses and validates a 'tox.ini' file for pytest configuration. + # configuration from ini files are obtained from the [pytest] section, if present. + if filepath.ext == ".ini": + iniconfig = _parse_ini_config(filepath) - 'tox.ini' files are only considered for pytest configuration if they contain a "[pytest]" - section. - """ - iniconfig = _parse_ini_config(path) - if "pytest" in iniconfig: - return dict(iniconfig["pytest"].items()) - else: + if "pytest" in iniconfig: + return dict(iniconfig["pytest"].items()) + else: + # "pytest.ini" files are always the source of configuration, even if empty + return {} if filepath.basename == "pytest.ini" else None + + # '.cfg' files are considered if they contain a "[tool:pytest]" section + elif filepath.ext == ".cfg": + iniconfig = _parse_ini_config(filepath) + + if "tool:pytest" in iniconfig.sections: + return dict(iniconfig["tool:pytest"].items()) + elif "pytest" in iniconfig.sections: + # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that + # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). + fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) return None + # '.toml' files are considered if they contain a [tool.pytest.ini_options] table + elif filepath.ext == ".toml": + import toml -def _parse_ini_config_from_setup_cfg(path: py.path.local) -> Optional[Dict[str, str]]: - """Parses and validates a 'setup.cfg' file for pytest configuration. + config = toml.load(filepath) - 'setup.cfg' files are only considered for pytest configuration if they contain a "[tool:pytest]" - section. + result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) + if result is not None: + # TOML supports richer data types than ini files (strings, arrays, floats, ints, etc), + # however we need to convert all scalar values to str for compatibility with the rest + # of the configuration system, which expects strings only. We needed to change the + def make_scalar(v: Any) -> Union[str, List[str]]: + return v if isinstance(v, list) else str(v) - If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that - plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). - """ - iniconfig = _parse_ini_config(path) + return {k: make_scalar(v) for k, v in result.items()} + else: + return None - if "tool:pytest" in iniconfig.sections: - return dict(iniconfig["tool:pytest"].items()) - elif "pytest" in iniconfig.sections: - fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) return None -def _parse_ini_config_from_pyproject_toml( - path: py.path.local, -) -> Optional[Dict[str, Union[str, List[str]]]]: - """Parses and validates a ``pyproject.toml`` file for pytest configuration. - - The ``[tool.pytest]`` table is used by pytest. If the file contains that section, - it is used as the config file. - - Note: toml supports richer data types than ini files (strings, arrays, floats, ints, etc), - however we need to convert all scalar values to str for compatibility with the rest - of the configuration system, which expects strings only. We needed to change the - handling of ini values in Config as to at least leave lists intact. - """ - import toml - - config = toml.load(path) - - result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) - if result is not None: - # convert all scalar values to strings for compatibility with other ini formats; - # conversion to useful values is made by Config._getini - def make_scalar(v: Any) -> Union[str, List[str]]: - return v if isinstance(v, list) else str(v) - - return {k: make_scalar(v) for k, v in result.items()} - else: - return None - - def getcfg(args): """ Search the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict). """ - ini_names_and_parsers = [ - ("pytest.ini", _parse_ini_config_from_pytest_ini), - ("pyproject.toml", _parse_ini_config_from_pyproject_toml), - ("tox.ini", _parse_ini_config_from_tox_ini), - ("setup.cfg", _parse_ini_config_from_setup_cfg), + config_names = [ + "pytest.ini", + "pyproject.toml", + "tox.ini", + "setup.cfg", ] args = [x for x in args if not str(x).startswith("-")] if not args: @@ -128,10 +106,10 @@ def getcfg(args): for arg in args: arg = py.path.local(arg) for base in arg.parts(reverse=True): - for inibasename, parser in ini_names_and_parsers: - p = base.join(inibasename) + for config_name in config_names: + p = base.join(config_name) if p.isfile(): - ini_config = parser(p) + ini_config = load_config_dict_from_file(p) if ini_config is not None: return base, p, ini_config return None, None, None @@ -191,23 +169,10 @@ def determine_setup( rootdir_cmd_arg: Optional[str] = None, config: Optional["Config"] = None, ) -> Tuple[py.path.local, Optional[str], Any]: + rootdir = None dirs = get_dirs_from_args(args) if inifile: - iniconfig = IniConfig(inifile) - is_cfg_file = str(inifile).endswith(".cfg") - sections = ["tool:pytest", "pytest"] if is_cfg_file else ["pytest"] - for section in sections: - try: - inicfg = iniconfig[ - section - ] # type: Optional[py.iniconfig._SectionWrapper] - if is_cfg_file and section == "pytest" and config is not None: - fail( - CFG_PYTEST_SECTION.format(filename=str(inifile)), pytrace=False - ) - break - except KeyError: - inicfg = None + inicfg = load_config_dict_from_file(py.path.local(inifile)) or {} if rootdir_cmd_arg is None: rootdir = get_common_ancestor(dirs) else: diff --git a/testing/test_config.py b/testing/test_config.py index 8f5cc79c2db..49a9264855d 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -260,6 +260,18 @@ def pytest_addoption(parser): config = testdir.parseconfig("-c", "custom_tool_pytest_section.cfg") assert config.getini("custom") == "1" + testdir.makefile( + ".toml", + custom=""" + [tool.pytest.ini_options] + custom = 1 + value = [ + ] # this is here on purpose, as it makes this an invalid '.ini' file + """, + ) + config = testdir.parseconfig("-c", "custom.toml") + assert config.getini("custom") == "1" + def test_absolute_win32_path(self, testdir): temp_ini_file = testdir.makefile( ".ini", @@ -1023,10 +1035,20 @@ def test_simple_noini(self, tmpdir): assert get_common_ancestor([no_path]) == tmpdir assert get_common_ancestor([no_path.join("a")]) == tmpdir - @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) - def test_with_ini(self, tmpdir: py.path.local, name: str) -> None: + @pytest.mark.parametrize( + "name, contents", + [ + pytest.param("pytest.ini", "[pytest]\nx=10", id="pytest.ini"), + pytest.param( + "pyproject.toml", "[tool.pytest.ini_options]\nx=10", id="pyproject.toml" + ), + pytest.param("tox.ini", "[pytest]\nx=10", id="tox.ini"), + pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"), + ], + ) + def test_with_ini(self, tmpdir: py.path.local, name: str, contents: str) -> None: inifile = tmpdir.join(name) - inifile.write("[pytest]\n" if name != "setup.cfg" else "[tool:pytest]\n") + inifile.write(contents) a = tmpdir.mkdir("a") b = a.mkdir("b") @@ -1034,9 +1056,10 @@ def test_with_ini(self, tmpdir: py.path.local, name: str) -> None: rootdir, parsed_inifile, _ = determine_setup(None, args) assert rootdir == tmpdir assert parsed_inifile == inifile - rootdir, parsed_inifile, _ = determine_setup(None, [str(b), str(a)]) + rootdir, parsed_inifile, ini_config = determine_setup(None, [str(b), str(a)]) assert rootdir == tmpdir assert parsed_inifile == inifile + assert ini_config == {"x": "10"} @pytest.mark.parametrize("name", "setup.cfg tox.ini".split()) def test_pytestini_overrides_empty_other(self, tmpdir: py.path.local, name) -> None: @@ -1063,10 +1086,26 @@ def test_nothing(self, tmpdir: py.path.local, monkeypatch) -> None: assert inifile is None assert inicfg == {} - def test_with_specific_inifile(self, tmpdir: py.path.local) -> None: - inifile = tmpdir.ensure("pytest.ini") - rootdir, _, _ = determine_setup(str(inifile), [str(tmpdir)]) + @pytest.mark.parametrize( + "name, contents", + [ + # pytest.param("pytest.ini", "[pytest]\nx=10", id="pytest.ini"), + pytest.param( + "pyproject.toml", "[tool.pytest.ini_options]\nx=10", id="pyproject.toml" + ), + # pytest.param("tox.ini", "[pytest]\nx=10", id="tox.ini"), + # pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"), + ], + ) + def test_with_specific_inifile( + self, tmpdir: py.path.local, name: str, contents: str + ) -> None: + p = tmpdir.ensure(name) + p.write(contents) + rootdir, inifile, ini_config = determine_setup(str(p), [str(tmpdir)]) assert rootdir == tmpdir + assert inifile == p + assert ini_config == {"x": "10"} def test_with_arg_outside_cwd_without_inifile(self, tmpdir, monkeypatch) -> None: monkeypatch.chdir(str(tmpdir)) From 78fefddcf2b763ede6dbb0e1b14590b71d51de79 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Jun 2020 18:41:59 -0300 Subject: [PATCH 10/16] Rename 'getcfg' to 'locate_cfg' and add type annotations --- src/_pytest/config/findpaths.py | 15 ++++++++++----- testing/test_config.py | 6 +++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 8e3f25552ed..e87508f3aca 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -89,9 +89,14 @@ def make_scalar(v: Any) -> Union[str, List[str]]: return None -def getcfg(args): +LocatedConfig = Tuple[ + Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]], +] + + +def locate_config(args: List[Union[str, py.path.local]]) -> LocatedConfig: """ - Search the list of arguments for a valid ini-file for pytest, + Search in the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict). """ config_names = [ @@ -112,7 +117,7 @@ def getcfg(args): ini_config = load_config_dict_from_file(p) if ini_config is not None: return base, p, ini_config - return None, None, None + return None, None, {} def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local: @@ -177,7 +182,7 @@ def determine_setup( rootdir = get_common_ancestor(dirs) else: ancestor = get_common_ancestor(dirs) - rootdir, inifile, inicfg = getcfg([ancestor]) + rootdir, inifile, inicfg = locate_config([ancestor]) if rootdir is None and rootdir_cmd_arg is None: for possible_rootdir in ancestor.parts(reverse=True): if possible_rootdir.join("setup.py").exists(): @@ -185,7 +190,7 @@ def determine_setup( break else: if dirs != [ancestor]: - rootdir, inifile, inicfg = getcfg(dirs) + rootdir, inifile, inicfg = locate_config(dirs) if rootdir is None: if config is not None: cwd = config.invocation_dir diff --git a/testing/test_config.py b/testing/test_config.py index 49a9264855d..639327f5b4a 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -15,7 +15,7 @@ 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.config.findpaths import locate_config from _pytest.pathlib import Path @@ -36,14 +36,14 @@ def test_getcfg_and_config(self, testdir, tmpdir, section, filename): ) ) ) - _, _, cfg = getcfg([sub]) + _, _, cfg = locate_config([sub]) assert cfg["name"] == "value" config = testdir.parseconfigure(sub) assert config.inicfg["name"] == "value" def test_getcfg_empty_path(self): """correctly handle zero length arguments (a la pytest '')""" - getcfg([""]) + locate_config([""]) def test_setupcfg_uses_toolpytest_with_pytest(self, testdir): p1 = testdir.makepyfile("def test(): pass") From 953df4500280f8b898ab3a3c2bed3da0fce9ba1e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Jun 2020 19:00:06 -0300 Subject: [PATCH 11/16] Fix iniconfig usage --- src/_pytest/config/findpaths.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index e87508f3aca..49aebc9e41f 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -7,9 +7,8 @@ from typing import Tuple from typing import Union +import iniconfig import py -from iniconfig import IniConfig -from iniconfig import ParseError from .exceptions import UsageError from _pytest.compat import TYPE_CHECKING @@ -26,15 +25,15 @@ def exists(path, ignore=OSError): return False -def _parse_ini_config(path: py.path.local) -> py.iniconfig.IniConfig: +def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig: """Parses the given generic '.ini' file using legacy IniConfig parser, returning the parsed object. Raises UsageError if the file cannot be parsed. """ try: - return py.iniconfig.IniConfig(path) - except py.iniconfig.ParseError as exc: + return iniconfig.IniConfig(path) + except iniconfig.ParseError as exc: raise UsageError(str(exc)) From 99ddf36e5af3f3b8b294ad8f520c4381b5961f4e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Jun 2020 19:03:34 -0300 Subject: [PATCH 12/16] Fix linting --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 6831c4b1535..a563cf4f573 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,11 +46,11 @@ install_requires = packaging pluggy>=0.12,<1.0 py>=1.5.0 + toml atomicwrites>=1.0;sys_platform=="win32" colorama;sys_platform=="win32" importlib-metadata>=0.12;python_version<"3.8" pathlib2>=2.2.0;python_version<"3.6" - toml python_requires = >=3.5 package_dir = =src From f211ea60198da93fb60ceb04478de9841dfb8383 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Jun 2020 19:45:40 -0300 Subject: [PATCH 13/16] Remove findpaths.exists() This function no longer seems to be necessary --- src/_pytest/config/__init__.py | 3 +-- src/_pytest/config/findpaths.py | 7 ------- testing/test_config.py | 1 - 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 3d3b9509d1f..fd034b76d9b 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -33,7 +33,6 @@ from .exceptions import PrintHelp from .exceptions import UsageError from .findpaths import determine_setup -from .findpaths import exists from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback from _pytest._io import TerminalWriter @@ -449,7 +448,7 @@ def _set_initial_conftests(self, namespace): if i != -1: path = path[:i] anchor = current.join(path, abs=1) - if exists(anchor): # we found some file object + if anchor.exists(): # we found some file object self._try_load_conftest(anchor) foundanchor = True if not foundanchor: diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 49aebc9e41f..e98e432df90 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -18,13 +18,6 @@ from . import Config -def exists(path, ignore=OSError): - try: - return path.check() - except ignore: - return False - - def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig: """Parses the given generic '.ini' file using legacy IniConfig parser, returning the parsed object. diff --git a/testing/test_config.py b/testing/test_config.py index 639327f5b4a..38b61b7891c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -903,7 +903,6 @@ def test_consider_args_after_options_for_rootdir(testdir, args): result.stdout.fnmatch_lines(["*rootdir: *myroot"]) -@pytest.mark.skipif("sys.platform == 'win32'") def test_toolongargs_issue224(testdir): result = testdir.runpytest("-m", "hello" * 500) assert result.ret == ExitCode.NO_TESTS_COLLECTED From a2917962d2003bf588424aff4b7437a17763c88b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Jun 2020 20:18:45 -0300 Subject: [PATCH 14/16] Improve coverage --- src/_pytest/config/findpaths.py | 8 +-- testing/test_findpaths.py | 110 ++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 testing/test_findpaths.py diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index e98e432df90..7ed34865647 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -46,7 +46,8 @@ def load_config_dict_from_file( return dict(iniconfig["pytest"].items()) else: # "pytest.ini" files are always the source of configuration, even if empty - return {} if filepath.basename == "pytest.ini" else None + if filepath.basename == "pytest.ini": + return {} # '.cfg' files are considered if they contain a "[tool:pytest]" section elif filepath.ext == ".cfg": @@ -58,7 +59,6 @@ def load_config_dict_from_file( # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) - return None # '.toml' files are considered if they contain a [tool.pytest.ini_options] table elif filepath.ext == ".toml": @@ -70,13 +70,11 @@ def load_config_dict_from_file( if result is not None: # TOML supports richer data types than ini files (strings, arrays, floats, ints, etc), # however we need to convert all scalar values to str for compatibility with the rest - # of the configuration system, which expects strings only. We needed to change the + # of the configuration system, which expects strings only. def make_scalar(v: Any) -> Union[str, List[str]]: return v if isinstance(v, list) else str(v) return {k: make_scalar(v) for k, v in result.items()} - else: - return None return None diff --git a/testing/test_findpaths.py b/testing/test_findpaths.py new file mode 100644 index 00000000000..3de2ea21828 --- /dev/null +++ b/testing/test_findpaths.py @@ -0,0 +1,110 @@ +from textwrap import dedent + +import py + +import pytest +from _pytest.config.findpaths import get_common_ancestor +from _pytest.config.findpaths import load_config_dict_from_file + + +class TestLoadConfigDictFromFile: + def test_empty_pytest_ini(self, tmpdir): + """pytest.ini files are always considered for configuration, even if empty""" + fn = tmpdir.join("pytest.ini") + fn.write("") + assert load_config_dict_from_file(fn) == {} + + def test_pytest_ini(self, tmpdir): + """[pytest] section in pytest.ini files is read correctly""" + fn = tmpdir.join("pytest.ini") + fn.write("[pytest]\nx=1") + assert load_config_dict_from_file(fn) == {"x": "1"} + + def test_custom_ini(self, tmpdir): + """[pytest] section in any .ini file is read correctly""" + fn = tmpdir.join("custom.ini") + fn.write("[pytest]\nx=1") + assert load_config_dict_from_file(fn) == {"x": "1"} + + def test_custom_ini_without_section(self, tmpdir): + """Custom .ini files without [pytest] section are not considered for configuration""" + fn = tmpdir.join("custom.ini") + fn.write("[custom]") + assert load_config_dict_from_file(fn) is None + + def test_custom_cfg_file(self, tmpdir): + """Custom .cfg files without [tool:pytest] section are not considered for configuration""" + fn = tmpdir.join("custom.cfg") + fn.write("[custom]") + assert load_config_dict_from_file(fn) is None + + def test_valid_cfg_file(self, tmpdir): + """Custom .cfg files with [tool:pytest] section are read correctly""" + fn = tmpdir.join("custom.cfg") + fn.write("[tool:pytest]\nx=1") + assert load_config_dict_from_file(fn) == {"x": "1"} + + def test_unsupported_pytest_section_in_cfg_file(self, tmpdir): + """.cfg files with [pytest] section are no longer supported and should fail to alert users""" + fn = tmpdir.join("custom.cfg") + fn.write("[pytest]") + with pytest.raises(pytest.fail.Exception): + load_config_dict_from_file(fn) + + def test_invalid_toml_file(self, tmpdir): + """.toml files without [tool.pytest.ini_options] are not considered for configuration.""" + fn = tmpdir.join("myconfig.toml") + fn.write( + dedent( + """ + [build_system] + x = 1 + """ + ) + ) + assert load_config_dict_from_file(fn) is None + + def test_valid_toml_file(self, tmpdir): + """.toml files with [tool.pytest.ini_options] are read correctly, including changing + data types to str/list for compatibility with other configuration options.""" + fn = tmpdir.join("myconfig.toml") + fn.write( + dedent( + """ + [tool.pytest.ini_options] + x = 1 + y = 20.0 + values = ["tests", "integration"] + name = "foo" + """ + ) + ) + assert load_config_dict_from_file(fn) == { + "x": "1", + "y": "20.0", + "values": ["tests", "integration"], + "name": "foo", + } + + +class TestCommonAncestor: + def test_has_ancestor(self, tmpdir): + fn1 = tmpdir.join("foo/bar/test_1.py").ensure(file=1) + fn2 = tmpdir.join("foo/zaz/test_2.py").ensure(file=1) + assert get_common_ancestor([fn1, fn2]) == tmpdir.join("foo") + assert get_common_ancestor([py.path.local(fn1.dirname), fn2]) == tmpdir.join( + "foo" + ) + assert get_common_ancestor( + [py.path.local(fn1.dirname), py.path.local(fn2.dirname)] + ) == tmpdir.join("foo") + assert get_common_ancestor([fn1, py.path.local(fn2.dirname)]) == tmpdir.join( + "foo" + ) + + def test_single_dir(self, tmpdir): + assert get_common_ancestor([tmpdir]) == tmpdir + + def test_single_file(self, tmpdir): + fn = tmpdir.join("foo.py").ensure(file=1) + assert get_common_ancestor([fn]) == tmpdir From 414b022ab37513dd48ce3b5a7dacc7c106721076 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Jun 2020 20:19:28 -0300 Subject: [PATCH 15/16] Ignore local coverage html report --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 83b6dbe7351..faea9eac03f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ doc/*/_changelog_towncrier_draft.rst build/ dist/ *.egg-info +htmlcov/ issue/ env/ .env/ From ffd5898698f4c9029349a9e290741190441f48db Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Jun 2020 20:54:33 -0300 Subject: [PATCH 16/16] Apply type changes and note to pyproject.toml (review) --- doc/en/customize.rst | 10 ++++++++++ src/_pytest/config/__init__.py | 8 +++++--- src/_pytest/config/findpaths.py | 14 ++++++-------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/doc/en/customize.rst b/doc/en/customize.rst index 71e3e6642d7..01315cb683c 100644 --- a/doc/en/customize.rst +++ b/doc/en/customize.rst @@ -59,6 +59,16 @@ pyproject.toml "integration", ] +.. note:: + + One might wonder why ``[tool.pytest.ini_options]`` instead of ``[tool.pytest]`` as is the + case with other tools. + + The reason is that the pytest team intends to fully utilize the rich TOML data format + for configuration in the future, reserving the ``[tool.pytest]`` table for that. + The ``ini_options`` table is being used, for now, as a bridge between the existing + ``.ini`` configuration system and the future configuration format. + tox.ini ~~~~~~~ diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index fd034b76d9b..98e6e002882 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1127,8 +1127,8 @@ def _getini(self, name: str) -> Any: description, type, default = self._parser._inidict[name] except KeyError: raise ValueError("unknown configuration value: {!r}".format(name)) - value = self._get_override_ini_value(name) - if value is None: + override_value = self._get_override_ini_value(name) + if override_value is None: try: value = self.inicfg[name] except KeyError: @@ -1137,6 +1137,8 @@ def _getini(self, name: str) -> Any: if type is None: return "" return [] + else: + value = override_value # coerce the values based on types # note: some coercions are only required if we are reading from .ini files, because # the file format doesn't contain type information, but when reading from toml we will @@ -1163,7 +1165,7 @@ def _getini(self, name: str) -> Any: else: return value elif type == "bool": - return bool(_strtobool(value.strip())) + return bool(_strtobool(str(value).strip())) else: assert type is None return value diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 7ed34865647..796fa9b0ae0 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -1,5 +1,4 @@ import os -from typing import Any from typing import Dict from typing import Iterable from typing import List @@ -71,7 +70,7 @@ def load_config_dict_from_file( # TOML supports richer data types than ini files (strings, arrays, floats, ints, etc), # however we need to convert all scalar values to str for compatibility with the rest # of the configuration system, which expects strings only. - def make_scalar(v: Any) -> Union[str, List[str]]: + def make_scalar(v: object) -> Union[str, List[str]]: return v if isinstance(v, list) else str(v) return {k: make_scalar(v) for k, v in result.items()} @@ -79,12 +78,11 @@ def make_scalar(v: Any) -> Union[str, List[str]]: return None -LocatedConfig = Tuple[ +def locate_config( + args: Iterable[Union[str, py.path.local]] +) -> Tuple[ Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]], -] - - -def locate_config(args: List[Union[str, py.path.local]]) -> LocatedConfig: +]: """ Search in the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict). @@ -163,7 +161,7 @@ def determine_setup( args: List[str], rootdir_cmd_arg: Optional[str] = None, config: Optional["Config"] = None, -) -> Tuple[py.path.local, Optional[str], Any]: +) -> Tuple[py.path.local, Optional[str], Dict[str, Union[str, List[str]]]]: rootdir = None dirs = get_dirs_from_args(args) if inifile: