Skip to content

Commit

Permalink
Add support for configuration in pyproject.toml
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoddemus committed Jun 2, 2020
1 parent e77ce02 commit ba7f46d
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 13 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
'colorama;sys_platform=="win32"',
"pluggy>=0.12,<1.0",
'importlib-metadata>=0.12;python_version<"3.8"',
"toml",
]


Expand Down
18 changes: 12 additions & 6 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
28 changes: 28 additions & 0 deletions src/_pytest/config/findpaths.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,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,
Expand All @@ -88,6 +115,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:
Expand Down
7 changes: 7 additions & 0 deletions src/_pytest/pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,13 @@ def getinicfg(self, source):
p = self.makeini(source)
return py.iniconfig.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
Expand Down
71 changes: 64 additions & 7 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -351,63 +361,110 @@ 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):
parser.addini("paths", "my new ini value", type="pathlist")
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):
parser.addini("args", "new args", type="args")
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):
parser.addini("xy", "", type="linelist")
parser.addini("a2", "", "linelist")
"""
)

def test_addini_linelist_ini_files(self, testdir):
self.make_conftest_for_linelist(testdir)
testdir.makeini(
"""
[pytest]
xy= 123 345
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
Expand Down

0 comments on commit ba7f46d

Please sign in to comment.