From d37b01f92593fb97921fc5492b6b8678084d2101 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 23 May 2020 14:48:43 -0300 Subject: [PATCH] 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