Skip to content

Commit ba7f46d

Browse files
committed
Add support for configuration in pyproject.toml
Fix pytest-dev#1556
1 parent e77ce02 commit ba7f46d

File tree

5 files changed

+112
-13
lines changed

5 files changed

+112
-13
lines changed

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
'colorama;sys_platform=="win32"',
1313
"pluggy>=0.12,<1.0",
1414
'importlib-metadata>=0.12;python_version<"3.8"',
15+
"toml",
1516
]
1617

1718

src/_pytest/config/__init__.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -1138,16 +1138,22 @@ def _getini(self, name: str) -> Any:
11381138
if type is None:
11391139
return ""
11401140
return []
1141+
# coerce the values based on types
1142+
# note: some coercions are only required if we are reading from .ini files, because
1143+
# the file format doesn't contain type information; toml files however support
1144+
# data types and complex types such as lists directly, so many conversions are not
1145+
# necessary
11411146
if type == "pathlist":
11421147
dp = py.path.local(self.inifile).dirpath()
1143-
values = []
1144-
for relpath in shlex.split(value):
1145-
values.append(dp.join(relpath, abs=True))
1146-
return values
1148+
input_values = shlex.split(value) if isinstance(value, str) else value
1149+
return [dp.join(x, abs=True) for x in input_values]
11471150
elif type == "args":
1148-
return shlex.split(value)
1151+
return shlex.split(value) if isinstance(value, str) else value
11491152
elif type == "linelist":
1150-
return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
1153+
if isinstance(value, str):
1154+
return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
1155+
else:
1156+
return value
11511157
elif type == "bool":
11521158
return bool(_strtobool(value.strip()))
11531159
else:

src/_pytest/config/findpaths.py

+28
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,33 @@ def _get_ini_config_from_setup_cfg(path: py.path.local) -> Optional[Dict[str, An
7979
return None
8080

8181

82+
def _get_ini_config_from_pyproject_toml(
83+
path: py.path.local,
84+
) -> Optional[Dict[str, Any]]:
85+
"""Parses and validates a 'setup.cfg' file for pytest configuration.
86+
87+
'setup.cfg' files are only considered for pytest configuration if they contain a "[tool:pytest]"
88+
section.
89+
90+
If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
91+
plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
92+
"""
93+
import toml
94+
95+
config = toml.load(path)
96+
97+
result = config.get("tool", {}).get("pytest", {}).get("ini", None)
98+
if result is not None:
99+
# convert all scalar values to strings for compatibility with other ini formats
100+
# conversion to actual useful values is made by Config._getini
101+
def make_scalar(v):
102+
return v if isinstance(v, (list, tuple)) else str(v)
103+
104+
return {k: make_scalar(v) for k, v in result.items()}
105+
else:
106+
return None
107+
108+
82109
def getcfg(args):
83110
"""
84111
Search the list of arguments for a valid ini-file for pytest,
@@ -88,6 +115,7 @@ def getcfg(args):
88115
("pytest.ini", _get_ini_config_from_pytest_ini),
89116
("tox.ini", _get_ini_config_from_tox_ini),
90117
("setup.cfg", _get_ini_config_from_setup_cfg),
118+
("pyproject.toml", _get_ini_config_from_pyproject_toml),
91119
]
92120
args = [x for x in args if not str(x).startswith("-")]
93121
if not args:

src/_pytest/pytester.py

+7
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,13 @@ def getinicfg(self, source):
685685
p = self.makeini(source)
686686
return py.iniconfig.IniConfig(p)["pytest"]
687687

688+
def makepyprojecttoml(self, source):
689+
"""Write a pyproject.toml file with 'source' as contents.
690+
691+
.. versionadded:: 6.0
692+
"""
693+
return self.makefile(".toml", pyproject=source)
694+
688695
def makepyfile(self, *args, **kwargs):
689696
r"""Shortcut for .makefile() with a .py extension.
690697
Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting

testing/test_config.py

+64-7
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ def test_ini_names(self, testdir, name, section):
109109
config = testdir.parseconfig()
110110
assert config.getini("minversion") == "1.0"
111111

112+
def test_pyproject_toml(self, testdir):
113+
testdir.makepyprojecttoml(
114+
"""
115+
[tool.pytest.ini]
116+
minversion = "1.0"
117+
"""
118+
)
119+
config = testdir.parseconfig()
120+
assert config.getini("minversion") == "1.0"
121+
112122
def test_toxini_before_lower_pytestini(self, testdir):
113123
sub = testdir.tmpdir.mkdir("sub")
114124
sub.join("tox.ini").write(
@@ -351,63 +361,110 @@ def pytest_addoption(parser):
351361
assert val == "hello"
352362
pytest.raises(ValueError, config.getini, "other")
353363

354-
def test_addini_pathlist(self, testdir):
364+
def make_conftest_for_pathlist(self, testdir):
355365
testdir.makeconftest(
356366
"""
357367
def pytest_addoption(parser):
358368
parser.addini("paths", "my new ini value", type="pathlist")
359369
parser.addini("abc", "abc value")
360370
"""
361371
)
372+
373+
def test_addini_pathlist_ini_files(self, testdir):
374+
self.make_conftest_for_pathlist(testdir)
362375
p = testdir.makeini(
363376
"""
364377
[pytest]
365378
paths=hello world/sub.py
366379
"""
367380
)
381+
self.check_config_pathlist(testdir, p)
382+
383+
def test_addini_pathlist_pyproject_toml(self, testdir):
384+
self.make_conftest_for_pathlist(testdir)
385+
p = testdir.makepyprojecttoml(
386+
"""
387+
[tool.pytest.ini]
388+
paths=["hello", "world/sub.py"]
389+
"""
390+
)
391+
self.check_config_pathlist(testdir, p)
392+
393+
def check_config_pathlist(self, testdir, config_path):
368394
config = testdir.parseconfig()
369395
values = config.getini("paths")
370396
assert len(values) == 2
371-
assert values[0] == p.dirpath("hello")
372-
assert values[1] == p.dirpath("world/sub.py")
397+
assert values[0] == config_path.dirpath("hello")
398+
assert values[1] == config_path.dirpath("world/sub.py")
373399
pytest.raises(ValueError, config.getini, "other")
374400

375-
def test_addini_args(self, testdir):
401+
def make_conftest_for_args(self, testdir):
376402
testdir.makeconftest(
377403
"""
378404
def pytest_addoption(parser):
379405
parser.addini("args", "new args", type="args")
380406
parser.addini("a2", "", "args", default="1 2 3".split())
381407
"""
382408
)
409+
410+
def test_addini_args_ini_files(self, testdir):
411+
self.make_conftest_for_args(testdir)
383412
testdir.makeini(
384413
"""
385414
[pytest]
386415
args=123 "123 hello" "this"
387-
"""
416+
"""
388417
)
418+
self.check_config_args(testdir)
419+
420+
def test_addini_args_pyproject_toml(self, testdir):
421+
self.make_conftest_for_args(testdir)
422+
testdir.makepyprojecttoml(
423+
"""
424+
[tool.pytest.ini]
425+
args = ["123", "123 hello", "this"]
426+
"""
427+
)
428+
self.check_config_args(testdir)
429+
430+
def check_config_args(self, testdir):
389431
config = testdir.parseconfig()
390432
values = config.getini("args")
391-
assert len(values) == 3
392433
assert values == ["123", "123 hello", "this"]
393434
values = config.getini("a2")
394435
assert values == list("123")
395436

396-
def test_addini_linelist(self, testdir):
437+
def make_conftest_for_linelist(self, testdir):
397438
testdir.makeconftest(
398439
"""
399440
def pytest_addoption(parser):
400441
parser.addini("xy", "", type="linelist")
401442
parser.addini("a2", "", "linelist")
402443
"""
403444
)
445+
446+
def test_addini_linelist_ini_files(self, testdir):
447+
self.make_conftest_for_linelist(testdir)
404448
testdir.makeini(
405449
"""
406450
[pytest]
407451
xy= 123 345
408452
second line
409453
"""
410454
)
455+
self.check_config_linelist(testdir)
456+
457+
def test_addini_linelist_pprojecttoml(self, testdir):
458+
self.make_conftest_for_linelist(testdir)
459+
testdir.makepyprojecttoml(
460+
"""
461+
[tool.pytest.ini]
462+
xy = ["123 345", "second line"]
463+
"""
464+
)
465+
self.check_config_linelist(testdir)
466+
467+
def check_config_linelist(self, testdir):
411468
config = testdir.parseconfig()
412469
values = config.getini("xy")
413470
assert len(values) == 2

0 commit comments

Comments
 (0)