Skip to content

Commit 6c7e944

Browse files
Add support for the different pytest files (#418)
* Add support for the different pytest files * Fix broken tests * Update src/usethis/_tool.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 57e3bd8 commit 6c7e944

File tree

6 files changed

+191
-9
lines changed

6 files changed

+191
-9
lines changed

src/usethis/_config_file.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ def files_manager() -> Iterator[None]:
2121
CodespellRCManager(),
2222
CoverageRCManager(),
2323
DotRuffTOMLManager(),
24+
DotPytestINIManager(),
25+
PytestINIManager(),
2426
RuffTOMLManager(),
2527
ToxINIManager(),
2628
):
@@ -51,6 +53,22 @@ def relative_path(self) -> Path:
5153
return Path(".ruff.toml")
5254

5355

56+
class DotPytestINIManager(INIFileManager):
57+
"""Class to manage the .pytest.ini file."""
58+
59+
@property
60+
def relative_path(self) -> Path:
61+
return Path(".pytest.ini")
62+
63+
64+
class PytestINIManager(INIFileManager):
65+
"""Class to manage the pytest.ini file."""
66+
67+
@property
68+
def relative_path(self) -> Path:
69+
return Path("pytest.ini")
70+
71+
5472
class RuffTOMLManager(TOMLFileManager):
5573
"""Class to manage the ruff.toml file."""
5674

src/usethis/_tool.py

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from usethis._config_file import (
1111
CodespellRCManager,
1212
CoverageRCManager,
13+
DotPytestINIManager,
1314
DotRuffTOMLManager,
15+
PytestINIManager,
1416
RuffTOMLManager,
1517
ToxINIManager,
1618
)
@@ -40,7 +42,7 @@
4042
)
4143
from usethis._io import KeyValueFileManager
4244

43-
ResolutionT: TypeAlias = Literal["first"]
45+
ResolutionT: TypeAlias = Literal["first", "bespoke"]
4446

4547

4648
class ConfigSpec(BaseModel):
@@ -289,6 +291,7 @@ def get_active_config_file_managers(self) -> set[KeyValueFileManager]:
289291
config_spec = self.get_config_spec()
290292
resolution = config_spec.resolution
291293
if resolution == "first":
294+
# N.B. keep this roughly in sync with the bespoke logic for pytest
292295
for (
293296
relative_path,
294297
file_manager,
@@ -311,6 +314,12 @@ def get_active_config_file_managers(self) -> set[KeyValueFileManager]:
311314
)
312315
raise NotImplementedError(msg)
313316
return {preferred_file_manager}
317+
elif resolution == "bespoke":
318+
msg = (
319+
"The bespoke resolution method is not yet implemented for the tool "
320+
f"{self.name}."
321+
)
322+
raise NotImplementedError(msg)
314323
else:
315324
assert_never(resolution)
316325

@@ -901,32 +910,114 @@ def get_config_spec(self) -> ConfigSpec:
901910
"log_cli_level": "INFO", # include all >=INFO level log messages (sp-repo-review)
902911
"minversion": "7", # minimum pytest version (sp-repo-review)
903912
}
913+
value_ini = value.copy()
914+
# https://docs.pytest.org/en/stable/reference/reference.html#confval-xfail_strict
915+
value_ini["xfail_strict"] = "True" # stringify boolean
904916

905917
return ConfigSpec.from_flat(
906-
file_managers=[PyprojectTOMLManager()],
907-
resolution="first",
918+
file_managers=[
919+
PytestINIManager(),
920+
DotPytestINIManager(),
921+
PyprojectTOMLManager(),
922+
ToxINIManager(),
923+
SetupCFGManager(),
924+
],
925+
resolution="bespoke",
908926
config_items=[
909927
ConfigItem(
910928
description="Overall Config",
911-
root={Path("pyproject.toml"): ConfigEntry(keys=["tool", "pytest"])},
929+
root={
930+
Path("pytest.ini"): ConfigEntry(keys=[]),
931+
Path(".pytest.ini"): ConfigEntry(keys=[]),
932+
Path("pyproject.toml"): ConfigEntry(keys=["tool", "pytest"]),
933+
Path("tox.ini"): ConfigEntry(keys=["pytest"]),
934+
Path("setup.cfg"): ConfigEntry(keys=["tool:pytest"]),
935+
},
912936
),
913937
ConfigItem(
914938
description="INI-Style Options",
915939
root={
940+
Path("pytest.ini"): ConfigEntry(
941+
keys=["pytest"], value=value_ini
942+
),
943+
Path(".pytest.ini"): ConfigEntry(
944+
keys=["pytest"], value=value_ini
945+
),
916946
Path("pyproject.toml"): ConfigEntry(
917947
keys=["tool", "pytest", "ini_options"], value=value
918-
)
948+
),
949+
Path("tox.ini"): ConfigEntry(keys=["pytest"], value=value_ini),
950+
Path("setup.cfg"): ConfigEntry(
951+
keys=["tool:pytest"], value=value_ini
952+
),
919953
},
920954
),
921955
],
922956
)
923957

924958
def get_managed_files(self) -> list[Path]:
925-
return [Path("pytest.ini"), Path("tests/conftest.py")]
959+
return [Path(".pytest.ini"), Path("pytest.ini"), Path("tests/conftest.py")]
926960

927961
def get_associated_ruff_rules(self) -> list[str]:
928962
return ["PT"]
929963

964+
def get_active_config_file_managers(self) -> set[KeyValueFileManager]:
965+
# This is a variant of the "first" method
966+
config_spec = self.get_config_spec()
967+
assert config_spec.resolution == "bespoke"
968+
# As per https://docs.pytest.org/en/stable/reference/customize.html#finding-the-rootdir
969+
# Files will only be matched for configuration if:
970+
# - pytest.ini: will always match and take precedence, even if empty.
971+
# - pyproject.toml: contains a [tool.pytest.ini_options] table.
972+
# - tox.ini: contains a [pytest] section.
973+
# - setup.cfg: contains a [tool:pytest] section.
974+
# Finally, a pyproject.toml file will be considered the configfile if no other
975+
# match was found, in this case even if it does not contain a
976+
# [tool.pytest.ini_options] table
977+
# Also, the docs mention that the hidden .pytest.ini variant is allowed, in my
978+
# experimentation is takes precedence over pyproject.toml but not pytest.ini.
979+
980+
for (
981+
relative_path,
982+
file_manager,
983+
) in config_spec.file_manager_by_relative_path.items():
984+
path = Path.cwd() / relative_path
985+
if path.exists() and path.is_file():
986+
if isinstance(file_manager, PyprojectTOMLManager):
987+
if ["tool", "pytest", "ini_options"] in file_manager:
988+
return {file_manager}
989+
else:
990+
continue
991+
return {file_manager}
992+
993+
# Second chance for pyproject.toml
994+
for (
995+
relative_path,
996+
file_manager,
997+
) in config_spec.file_manager_by_relative_path.items():
998+
path = Path.cwd() / relative_path
999+
if (
1000+
path.exists()
1001+
and path.is_file()
1002+
and isinstance(file_manager, PyprojectTOMLManager)
1003+
):
1004+
return {file_manager}
1005+
1006+
file_managers = config_spec.file_manager_by_relative_path.values()
1007+
if not file_managers:
1008+
return set()
1009+
1010+
# Use the preferred default file since there's no existing file.
1011+
preferred_file_manager = self.preferred_file_manager()
1012+
if preferred_file_manager not in file_managers:
1013+
msg = (
1014+
f"The preferred file manager '{preferred_file_manager}' is not "
1015+
f"among the file managers '{file_managers}' for the tool "
1016+
f"'{self.name}'"
1017+
)
1018+
raise NotImplementedError(msg)
1019+
return {preferred_file_manager}
1020+
9301021

9311022
class RequirementsTxtTool(Tool):
9321023
# https://pip.pypa.io/en/stable/reference/requirements-file-format/

tests/usethis/_core/test_core_tool.py

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ class TestAdd:
199199
def test_from_nothing(
200200
self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]
201201
):
202-
with change_cwd(uv_init_dir), PyprojectTOMLManager():
202+
with change_cwd(uv_init_dir), files_manager():
203203
# Act
204204
use_coverage()
205205

@@ -224,7 +224,7 @@ def test_no_pyproject_toml(
224224
# Set python version
225225
(tmp_path / ".python-version").write_text(get_python_version())
226226

227-
with change_cwd(tmp_path), PyprojectTOMLManager():
227+
with change_cwd(tmp_path), files_manager():
228228
# Act
229229
use_coverage()
230230

@@ -331,7 +331,7 @@ def test_unused(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]):
331331
assert not err
332332

333333
def test_roundtrip(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]):
334-
with change_cwd(uv_init_dir), PyprojectTOMLManager():
334+
with change_cwd(uv_init_dir), files_manager():
335335
# Arrange
336336
with usethis_config.set(quiet=True):
337337
use_coverage()
@@ -1399,6 +1399,79 @@ def test_ruff_integration(self, uv_init_dir: Path):
13991399
# Assert
14001400
assert "PT" in RuffTool().get_rules()
14011401

1402+
@pytest.mark.usefixtures("_vary_network_conn")
1403+
def test_pytest_ini_priority(self, uv_init_dir: Path):
1404+
# Arrange
1405+
(uv_init_dir / "pytest.ini").touch()
1406+
(uv_init_dir / "pyproject.toml").touch()
1407+
1408+
# Act
1409+
with change_cwd(uv_init_dir), files_manager():
1410+
use_pytest()
1411+
1412+
# Assert
1413+
assert (
1414+
(uv_init_dir / "pytest.ini").read_text()
1415+
== """\
1416+
[pytest]
1417+
testpaths =
1418+
tests
1419+
addopts =
1420+
--import-mode=importlib
1421+
-ra
1422+
--showlocals
1423+
--strict-markers
1424+
--strict-config
1425+
filterwarnings =
1426+
error
1427+
xfail_strict = True
1428+
log_cli_level = INFO
1429+
minversion = 7
1430+
"""
1431+
)
1432+
1433+
with PyprojectTOMLManager() as manager:
1434+
assert ["tool", "pytest"] not in manager
1435+
1436+
@pytest.mark.usefixtures("_vary_network_conn")
1437+
def test_pyproject_with_ini_priority(
1438+
self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str]
1439+
):
1440+
# testing it takes priority over setup.cfg
1441+
# Arrange
1442+
(uv_init_repo_dir / "setup.cfg").touch()
1443+
(uv_init_repo_dir / "pyproject.toml").write_text("""\
1444+
[tool.pytest.ini_options]
1445+
testpaths = ["tests"]
1446+
""")
1447+
1448+
# Act
1449+
with change_cwd(uv_init_repo_dir), files_manager():
1450+
use_pytest()
1451+
1452+
# Assert
1453+
assert (uv_init_repo_dir / "setup.cfg").read_text() == "", (
1454+
"Expected pyproject.toml to take priority when it has a [tool.pytest.ini_options] section"
1455+
)
1456+
1457+
@pytest.mark.usefixtures("_vary_network_conn")
1458+
def test_pyproject_without_ini_priority(
1459+
self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str]
1460+
):
1461+
# Arrange
1462+
(uv_init_repo_dir / "setup.cfg").touch()
1463+
(uv_init_repo_dir / "pyproject.toml").write_text("""\
1464+
[tool.pytest]
1465+
foo = "bar"
1466+
""")
1467+
1468+
# Act
1469+
with change_cwd(uv_init_repo_dir), files_manager():
1470+
use_pytest()
1471+
1472+
# Assert
1473+
assert (uv_init_repo_dir / "setup.cfg").read_text()
1474+
14021475
class TestRemove:
14031476
class TestRuffIntegration:
14041477
def test_deselected(self, uv_init_dir: Path):

0 commit comments

Comments
 (0)