Skip to content

Commit 61fc24c

Browse files
Only add pytest-cov when both coverage and pytest are used (#235)
1 parent 139add7 commit 61fc24c

5 files changed

Lines changed: 139 additions & 31 deletions

File tree

README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ To use pytest, run:
7373

7474
```console
7575
$ uvx usethis tool pytest
76-
✔ Adding dependencies 'pytest', 'pytest-cov' to the 'test' dependency group in 'pyproject.toml'.
76+
✔ Adding dependency 'pytest' to the 'test' dependency group in 'pyproject.toml'.
7777
✔ Adding pytest config to 'pyproject.toml'.
7878
✔ Enabling Ruff rule 'PT' in 'pyproject.toml'.
7979
✔ Creating '/tests'.
@@ -107,16 +107,13 @@ Add a new tool to a Python project, including:
107107

108108
Currently supported tools:
109109

110-
- `usethis tool ruff`
111-
- `usethis tool pytest`
110+
- `usethis tool coverage`
112111
- `usethis tool deptry`
113112
- `usethis tool pre-commit`
114113
- `usethis tool pyproject-fmt`
114+
- `usethis tool pytest`
115115
- `usethis tool requirements.txt`
116-
117-
Example:
118-
119-
`usethis tool ruff`
116+
- `usethis tool ruff`
120117

121118
Supported arguments:
122119

src/usethis/_core/tool.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
select_ruff_rules,
2222
)
2323
from usethis._integrations.uv.call import call_uv_subprocess
24-
from usethis._integrations.uv.deps import add_deps_to_group, remove_deps_from_group
24+
from usethis._integrations.uv.deps import (
25+
Dependency,
26+
add_deps_to_group,
27+
remove_deps_from_group,
28+
)
2529
from usethis._integrations.uv.init import ensure_pyproject_toml
2630
from usethis._tool import (
2731
ALL_TOOLS,
@@ -41,7 +45,10 @@ def use_coverage(*, remove: bool = False) -> None:
4145
ensure_pyproject_toml()
4246

4347
if not remove:
44-
add_deps_to_group(tool.dev_deps, "test")
48+
deps = tool.dev_deps
49+
if PytestTool().is_used():
50+
deps += [Dependency(name="pytest-cov")]
51+
add_deps_to_group(deps, "test")
4552

4653
tool.add_pyproject_configs()
4754

@@ -51,7 +58,7 @@ def use_coverage(*, remove: bool = False) -> None:
5158
_coverage_instructions_basic()
5259
else:
5360
tool.remove_pyproject_configs()
54-
remove_deps_from_group(tool.dev_deps, "test")
61+
remove_deps_from_group([*tool.dev_deps, Dependency(name="pytest-cov")], "test")
5562

5663

5764
def _coverage_instructions_basic() -> None:
@@ -172,7 +179,11 @@ def use_pytest(*, remove: bool = False) -> None:
172179
ensure_pyproject_toml()
173180

174181
if not remove:
175-
add_deps_to_group(tool.dev_deps, "test")
182+
deps = tool.dev_deps
183+
if CoverageTool().is_used():
184+
deps += [Dependency(name="pytest-cov")]
185+
add_deps_to_group(deps, "test")
186+
176187
tool.add_pyproject_configs()
177188
if RuffTool().is_used():
178189
select_ruff_rules(tool.get_associated_ruff_rules())
@@ -199,7 +210,8 @@ def use_pytest(*, remove: bool = False) -> None:
199210
if RuffTool().is_used():
200211
deselect_ruff_rules(tool.get_associated_ruff_rules())
201212
tool.remove_pyproject_configs()
202-
remove_deps_from_group(tool.dev_deps, "test")
213+
remove_deps_from_group([*tool.dev_deps, Dependency(name="pytest-cov")], "test")
214+
203215
remove_pytest_dir() # Last, since this is a manual step
204216

205217
if CoverageTool().is_used():

src/usethis/_tool.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ def dev_deps(self) -> list[Dependency]:
4242
"""The tool's development dependencies."""
4343
return []
4444

45+
def get_extra_dev_deps(self) -> list[Dependency]:
46+
"""Additional development dependencies for the tool.
47+
48+
These won't be installed automatically - usually they are only needed for
49+
integrations with other tools and will only be conditionally installed.
50+
51+
However, they will be used to determine if the tool is being used, so they
52+
should be considered characteristic of the tool. It follows that they should be
53+
removed when the tool is being removed.
54+
"""
55+
return []
56+
4557
def get_pre_commit_repos(self) -> list[LocalRepo | UriRepo]:
4658
"""Get the pre-commit repository configurations for the tool."""
4759
return []
@@ -70,15 +82,20 @@ def is_used(self) -> bool:
7082
2. Whether any of the tool's managed files are in the project.
7183
3. Whether any of the tool's managed pyproject.toml sections are present.
7284
"""
73-
is_any_deps = any(is_dep_in_any_group(dep) for dep in self.dev_deps)
74-
is_any_files = any(
75-
file.exists() and file.is_file() for file in self.get_managed_files()
76-
)
77-
is_any_pyproject = any(
78-
do_id_keys_exist(id_keys) for id_keys in self.get_pyproject_id_keys()
79-
)
80-
81-
return is_any_deps or is_any_files or is_any_pyproject
85+
for file in self.get_managed_files():
86+
if file.exists() and file.is_file():
87+
return True
88+
for id_keys in self.get_pyproject_id_keys():
89+
if do_id_keys_exist(id_keys):
90+
return True
91+
for dep in self.dev_deps:
92+
if is_dep_in_any_group(dep):
93+
return True
94+
for extra_dep in self.get_extra_dev_deps():
95+
if is_dep_in_any_group(extra_dep):
96+
return True
97+
98+
return False
8299

83100
def add_pre_commit_repo_configs(self) -> None:
84101
"""Add the tool's pre-commit configuration."""
@@ -272,10 +289,10 @@ def name(self) -> str:
272289

273290
@property
274291
def dev_deps(self) -> list[Dependency]:
275-
return [
276-
Dependency(name="pytest"),
277-
Dependency(name="pytest-cov"),
278-
]
292+
return [Dependency(name="pytest")]
293+
294+
def get_extra_dev_deps(self) -> list[Dependency]:
295+
return [Dependency(name="pytest-cov")]
279296

280297
def get_pyproject_configs(self) -> list[PyProjectConfig]:
281298
return [

tests/usethis/_core/test_tool.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def test_no_pyproject_toml(
8888
)
8989

9090
@pytest.mark.usefixtures("_vary_network_conn")
91-
def test_pytest_used(
91+
def test_pytest_integration(
9292
self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]
9393
):
9494
with change_cwd(uv_init_dir):
@@ -106,7 +106,7 @@ def test_pytest_used(
106106
out, err = capfd.readouterr()
107107
assert not err
108108
assert out == (
109-
"✔ Adding dependency 'coverage' to the 'test' group in 'pyproject.toml'.\n"
109+
"✔ Adding dependencies 'coverage', 'pytest-cov' to the 'test' group in \n'pyproject.toml'.\n"
110110
"✔ Adding coverage config to 'pyproject.toml'.\n"
111111
"☐ Run 'pytest --cov' to run your tests with coverage.\n"
112112
)
@@ -141,6 +141,27 @@ def test_roundtrip(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]):
141141
"✔ Removing dependency 'coverage' from the 'test' group in 'pyproject.toml'.\n"
142142
)
143143

144+
def test_pytest_integration(
145+
self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]
146+
):
147+
with change_cwd(uv_init_dir):
148+
# Arrange
149+
with usethis_config.set(quiet=True):
150+
use_pytest()
151+
use_coverage()
152+
153+
# Act
154+
use_coverage(remove=True)
155+
156+
# Assert
157+
assert get_deps_from_group("test") == [Dependency(name="pytest")]
158+
out, err = capfd.readouterr()
159+
assert not err
160+
assert out == (
161+
"✔ Removing coverage config from 'pyproject.toml'.\n"
162+
"✔ Removing dependencies 'coverage', 'pytest-cov' from the 'test' group in \n'pyproject.toml'.\n"
163+
)
164+
144165

145166
class TestDeptry:
146167
class TestAdd:
@@ -810,13 +831,14 @@ def test_no_pyproject(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]):
810831
assert is_dep_satisfied_in(
811832
Dependency(name="pytest"), in_=list(deps_from_test)
812833
)
813-
assert is_dep_satisfied_in(
834+
# pytest-cov should only be added when we are using coverage
835+
assert not is_dep_satisfied_in(
814836
Dependency(name="pytest-cov"), in_=list(deps_from_test)
815837
)
816838
out, _ = capfd.readouterr()
817839
assert out == (
818840
"✔ Writing 'pyproject.toml'.\n"
819-
"✔ Adding dependencies 'pytest', 'pytest-cov' to the 'test' group in \n'pyproject.toml'.\n"
841+
"✔ Adding dependency 'pytest' to the 'test' group in 'pyproject.toml'.\n"
820842
"✔ Adding pytest config to 'pyproject.toml'.\n"
821843
"✔ Creating '/tests'.\n"
822844
"✔ Writing '/tests/conftest.py'.\n"
@@ -838,7 +860,7 @@ def test_bitbucket_integration(self, uv_init_dir: Path):
838860
assert "pytest" in (uv_init_dir / "bitbucket-pipelines.yml").read_text()
839861

840862
@pytest.mark.usefixtures("_vary_network_conn")
841-
def test_coverage_notice(
863+
def test_coverage_integration(
842864
self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]
843865
):
844866
with change_cwd(uv_init_dir):
@@ -968,7 +990,7 @@ def test_remove(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]):
968990
"✔ Removing 'Test on 3.12' from default pipeline in 'bitbucket-pipelines.yml'.\n"
969991
"✔ Adding cache 'uv' definition to 'bitbucket-pipelines.yml'.\n"
970992
"✔ Removing pytest config from 'pyproject.toml'.\n"
971-
"✔ Removing dependencies 'pytest', 'pytest-cov' from the 'test' group in 'pyproject.toml'.\n"
993+
"✔ Removing dependency 'pytest' from the 'test' group in 'pyproject.toml'.\n"
972994
"✔ Removing '/tests'.\n"
973995
).replace("\n", "").replace(" ", "")
974996
contents = (uv_init_dir / "bitbucket-pipelines.yml").read_text()
@@ -997,6 +1019,27 @@ def test_remove(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]):
9971019
"""
9981020
)
9991021

1022+
def test_coverage_integration(
1023+
self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]
1024+
):
1025+
with change_cwd(uv_init_dir):
1026+
# Arrange
1027+
with usethis_config.set(quiet=True):
1028+
use_coverage()
1029+
use_pytest()
1030+
1031+
# Act
1032+
use_pytest(remove=True)
1033+
1034+
# Assert
1035+
out, _ = capfd.readouterr()
1036+
assert out == (
1037+
"✔ Removing pytest config from 'pyproject.toml'.\n"
1038+
"✔ Removing dependencies 'pytest', 'pytest-cov' from the 'test' group in \n'pyproject.toml'.\n"
1039+
"✔ Removing '/tests'.\n"
1040+
"☐ Run 'coverage help' to see available coverage commands.\n"
1041+
)
1042+
10001043

10011044
class TestRuff:
10021045
class TestAdd:

tests/usethis/test_tool.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ def dev_deps(self) -> list[Dependency]:
4040
Dependency(name="flake8"),
4141
]
4242

43+
def get_extra_dev_deps(self) -> list[Dependency]:
44+
return [Dependency(name="pytest")]
45+
4346
def get_pre_commit_repos(self) -> list[LocalRepo | UriRepo]:
4447
return [
4548
UriRepo(
@@ -221,6 +224,42 @@ def test_empty(self, uv_init_dir: Path):
221224
# Assert
222225
assert not result
223226

227+
def test_extra_dev_deps(self, uv_init_dir: Path):
228+
# Arrange
229+
tool = MyTool()
230+
231+
with change_cwd(uv_init_dir):
232+
add_deps_to_group(
233+
[
234+
Dependency(name="pytest"),
235+
],
236+
"test",
237+
)
238+
239+
# Act
240+
result = tool.is_used()
241+
242+
# Assert
243+
assert result
244+
245+
def test_not_extra_dev_deps(self, uv_init_dir: Path):
246+
# Arrange
247+
tool = MyTool()
248+
249+
with change_cwd(uv_init_dir):
250+
add_deps_to_group(
251+
[
252+
Dependency(name="isort"),
253+
],
254+
"test",
255+
)
256+
257+
# Act
258+
result = tool.is_used()
259+
260+
# Assert
261+
assert not result
262+
224263
class TestAddPreCommitRepoConfigs:
225264
def test_no_repo_configs(self, uv_init_dir: Path):
226265
# Arrange

0 commit comments

Comments
 (0)