Skip to content

Commit ef12f43

Browse files
Refactor the way tool dependencies are handled (#327)
* Refactor the way tool dependencies are handled * Properly use test deps in is_used heuristic
1 parent b5d051d commit ef12f43

3 files changed

Lines changed: 107 additions & 84 deletions

File tree

src/usethis/_core/tool.py

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@
2525
select_ruff_rules,
2626
)
2727
from usethis._integrations.uv.call import call_uv_subprocess
28-
from usethis._integrations.uv.deps import (
29-
Dependency,
30-
add_deps_to_group,
31-
remove_deps_from_group,
32-
)
3328
from usethis._integrations.uv.init import ensure_pyproject_toml
3429
from usethis._tool import (
3530
ALL_TOOLS,
@@ -53,7 +48,7 @@ def use_codespell(*, remove: bool = False) -> None:
5348

5449
if not remove:
5550
if not PreCommitTool().is_used():
56-
add_deps_to_group(tool.dev_deps, "dev")
51+
tool.add_dev_deps()
5752
if is_bitbucket_used():
5853
add_bitbucket_steps_in_default(tool.get_bitbucket_steps())
5954
else:
@@ -65,7 +60,7 @@ def use_codespell(*, remove: bool = False) -> None:
6560
remove_bitbucket_steps_from_default(tool.get_bitbucket_steps())
6661
tool.remove_pyproject_configs()
6762
tool.remove_pre_commit_repo_configs()
68-
remove_deps_from_group(tool.dev_deps, "dev")
63+
tool.remove_dev_deps()
6964
tool.remove_managed_files()
7065

7166

@@ -75,16 +70,12 @@ def use_coverage(*, remove: bool = False) -> None:
7570
ensure_pyproject_toml()
7671

7772
if not remove:
78-
deps = tool.dev_deps
79-
if PytestTool().is_used():
80-
deps += [Dependency(name="pytest-cov")]
81-
add_deps_to_group(deps, "test")
82-
73+
tool.add_test_deps()
8374
tool.add_pyproject_configs()
8475
tool.print_how_to_use()
8576
else:
8677
tool.remove_pyproject_configs()
87-
remove_deps_from_group([*tool.dev_deps, Dependency(name="pytest-cov")], "test")
78+
tool.remove_test_deps()
8879
tool.remove_managed_files()
8980

9081

@@ -94,7 +85,7 @@ def use_deptry(*, remove: bool = False) -> None:
9485
ensure_pyproject_toml()
9586

9687
if not remove:
97-
add_deps_to_group(tool.dev_deps, "dev")
88+
tool.add_dev_deps()
9889
if PreCommitTool().is_used():
9990
tool.add_pre_commit_repo_configs()
10091
elif is_bitbucket_used():
@@ -105,33 +96,34 @@ def use_deptry(*, remove: bool = False) -> None:
10596
tool.remove_pre_commit_repo_configs()
10697
tool.remove_pyproject_configs()
10798
remove_bitbucket_steps_from_default(tool.get_bitbucket_steps())
108-
remove_deps_from_group(tool.dev_deps, "dev")
99+
tool.remove_dev_deps()
109100
tool.remove_managed_files()
110101

111102

112103
def use_pre_commit(*, remove: bool = False) -> None:
113104
tool = PreCommitTool()
114105
pyproject_fmt_tool = PyprojectFmtTool()
115106
codespell_tool = CodespellTool()
107+
requirements_txt_tool = RequirementsTxtTool()
116108

117109
ensure_pyproject_toml()
118110

119111
if not remove:
120-
add_deps_to_group(tool.dev_deps, "dev")
112+
tool.add_dev_deps()
121113
_add_all_tools_pre_commit_configs()
122114

123115
# We will use pre-commit instead of project-installed dependencies:
124116
if pyproject_fmt_tool.is_used():
125-
remove_deps_from_group(pyproject_fmt_tool.dev_deps, "dev")
117+
pyproject_fmt_tool.remove_dev_deps()
126118
pyproject_fmt_tool.add_pyproject_configs()
127-
PyprojectFmtTool().print_how_to_use()
119+
pyproject_fmt_tool.print_how_to_use()
128120
if codespell_tool.is_used():
129-
remove_deps_from_group(codespell_tool.dev_deps, "dev")
121+
codespell_tool.remove_dev_deps()
130122
codespell_tool.add_pyproject_configs()
131-
CodespellTool().print_how_to_use()
123+
codespell_tool.print_how_to_use()
132124

133-
if RequirementsTxtTool().is_used():
134-
RequirementsTxtTool().print_how_to_use()
125+
if requirements_txt_tool.is_used():
126+
requirements_txt_tool.print_how_to_use()
135127

136128
if not get_hook_names():
137129
add_placeholder_hook()
@@ -151,21 +143,21 @@ def use_pre_commit(*, remove: bool = False) -> None:
151143
uninstall_pre_commit_hooks()
152144

153145
remove_pre_commit_config()
154-
remove_deps_from_group(tool.dev_deps, "dev")
146+
tool.remove_dev_deps()
155147

156148
# Need to add a new way of running some hooks manually if they are not dev
157149
# dependencies yet - explain to the user.
158150
if pyproject_fmt_tool.is_used():
159-
add_deps_to_group(pyproject_fmt_tool.dev_deps, "dev")
160-
PyprojectFmtTool().print_how_to_use()
151+
pyproject_fmt_tool.add_dev_deps()
152+
pyproject_fmt_tool.print_how_to_use()
161153
if codespell_tool.is_used():
162-
add_deps_to_group(codespell_tool.dev_deps, "dev")
163-
CodespellTool().print_how_to_use()
154+
codespell_tool.add_dev_deps()
155+
codespell_tool.print_how_to_use()
164156

165157
# Likewise, explain how to manually generate the requirements.txt file, since
166158
# they're not going to do it via pre-commit anymore.
167-
if RequirementsTxtTool().is_used():
168-
RequirementsTxtTool().print_how_to_use()
159+
if requirements_txt_tool.is_used():
160+
requirements_txt_tool.print_how_to_use()
169161
tool.remove_managed_files()
170162

171163

@@ -200,7 +192,7 @@ def use_pyproject_fmt(*, remove: bool = False) -> None:
200192

201193
if not remove:
202194
if not PreCommitTool().is_used():
203-
add_deps_to_group(tool.dev_deps, "dev")
195+
tool.add_dev_deps()
204196
if is_bitbucket_used():
205197
add_bitbucket_steps_in_default(tool.get_bitbucket_steps())
206198
else:
@@ -212,7 +204,7 @@ def use_pyproject_fmt(*, remove: bool = False) -> None:
212204
remove_bitbucket_steps_from_default(tool.get_bitbucket_steps())
213205
tool.remove_pyproject_configs()
214206
tool.remove_pre_commit_repo_configs()
215-
remove_deps_from_group(tool.dev_deps, "dev")
207+
tool.remove_dev_deps()
216208
tool.remove_managed_files()
217209

218210

@@ -235,11 +227,7 @@ def use_pytest(*, remove: bool = False) -> None:
235227
ensure_pyproject_toml()
236228

237229
if not remove:
238-
deps = tool.dev_deps
239-
if CoverageTool().is_used():
240-
deps += [Dependency(name="pytest-cov")]
241-
add_deps_to_group(deps, "test")
242-
230+
tool.add_test_deps()
243231
tool.add_pyproject_configs()
244232
if RuffTool().is_used():
245233
select_ruff_rules(tool.get_associated_ruff_rules())
@@ -262,8 +250,7 @@ def use_pytest(*, remove: bool = False) -> None:
262250
if RuffTool().is_used():
263251
deselect_ruff_rules(tool.get_associated_ruff_rules())
264252
tool.remove_pyproject_configs()
265-
remove_deps_from_group([*tool.dev_deps, Dependency(name="pytest-cov")], "test")
266-
253+
tool.remove_test_deps()
267254
remove_pytest_dir() # Last, since this is a manual step
268255

269256
if CoverageTool().is_used():
@@ -339,7 +326,7 @@ def use_ruff(*, remove: bool = False) -> None:
339326
]
340327

341328
if not remove:
342-
add_deps_to_group(tool.dev_deps, "dev")
329+
tool.add_dev_deps()
343330
tool.add_pyproject_configs()
344331
select_ruff_rules(rules)
345332
ignore_ruff_rules(ignored_rules)
@@ -353,5 +340,5 @@ def use_ruff(*, remove: bool = False) -> None:
353340
tool.remove_pre_commit_repo_configs()
354341
remove_bitbucket_steps_from_default(tool.get_bitbucket_steps())
355342
tool.remove_pyproject_configs()
356-
remove_deps_from_group(tool.dev_deps, "dev")
343+
tool.remove_dev_deps()
357344
tool.remove_managed_files()

src/usethis/_tool.py

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@
3131
set_config_value,
3232
)
3333
from usethis._integrations.pyproject.remove import remove_pyproject_toml
34-
from usethis._integrations.uv.deps import Dependency, is_dep_in_any_group
34+
from usethis._integrations.uv.deps import (
35+
Dependency,
36+
add_deps_to_group,
37+
is_dep_in_any_group,
38+
remove_deps_from_group,
39+
)
3540

3641

3742
class Tool(Protocol):
@@ -48,20 +53,25 @@ def get_bitbucket_steps(self) -> list[BitbucketStep]:
4853
"""Get the Bitbucket pipeline step associated with this tool."""
4954
return []
5055

51-
@property
52-
def dev_deps(self) -> list[Dependency]:
53-
"""The tool's development dependencies."""
56+
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
57+
"""The tool's development dependencies.
58+
59+
These should all be considered characteristic of this particular tool.
60+
61+
Args:
62+
unconditional: Whether to return all possible dependencies regardless of
63+
whether they are relevant to the current project.
64+
"""
5465
return []
5566

56-
def get_extra_dev_deps(self) -> list[Dependency]:
57-
"""Additional development dependencies for the tool.
67+
def get_test_deps(self, *, unconditional: bool = False) -> list[Dependency]:
68+
"""The tool's test dependencies.
5869
59-
These won't be installed automatically - usually they are only needed for
60-
integrations with other tools and will only be conditionally installed.
70+
These should all be considered characteristic of this particular tool.
6171
62-
However, they will be used to determine if the tool is being used, so they
63-
should be considered characteristic of the tool. It follows that they should be
64-
removed when the tool is being removed.
72+
Args:
73+
unconditional: Whether to return all possible dependencies regardless of
74+
whether they are relevant to the current project.
6575
"""
6676
return []
6777

@@ -111,15 +121,27 @@ def is_used(self) -> bool:
111121
for id_keys in self.get_pyproject_id_keys():
112122
if do_id_keys_exist(id_keys):
113123
return True
114-
for dep in self.dev_deps:
124+
for dep in self.get_dev_deps(unconditional=True):
115125
if is_dep_in_any_group(dep):
116126
return True
117-
for extra_dep in self.get_extra_dev_deps():
118-
if is_dep_in_any_group(extra_dep):
127+
for dep in self.get_test_deps(unconditional=True):
128+
if is_dep_in_any_group(dep):
119129
return True
120130

121131
return False
122132

133+
def add_dev_deps(self) -> None:
134+
add_deps_to_group(self.get_dev_deps(), "dev")
135+
136+
def remove_dev_deps(self) -> None:
137+
remove_deps_from_group(self.get_dev_deps(unconditional=True), "dev")
138+
139+
def add_test_deps(self) -> None:
140+
add_deps_to_group(self.get_test_deps(), "test")
141+
142+
def remove_test_deps(self) -> None:
143+
remove_deps_from_group(self.get_test_deps(unconditional=True), "test")
144+
123145
def add_pre_commit_repo_configs(self) -> None:
124146
"""Add the tool's pre-commit configuration."""
125147
repos = self.get_pre_commit_repos()
@@ -217,8 +239,7 @@ class CodespellTool(Tool):
217239
def name(self) -> str:
218240
return "Codespell"
219241

220-
@property
221-
def dev_deps(self) -> list[Dependency]:
242+
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
222243
return [Dependency(name="codespell")]
223244

224245
def print_how_to_use(self) -> None:
@@ -278,9 +299,11 @@ class CoverageTool(Tool):
278299
def name(self) -> str:
279300
return "coverage"
280301

281-
@property
282-
def dev_deps(self) -> list[Dependency]:
283-
return [Dependency(name="coverage", extras=frozenset({"toml"}))]
302+
def get_test_deps(self, *, unconditional: bool = False) -> list[Dependency]:
303+
deps = [Dependency(name="coverage", extras=frozenset({"toml"}))]
304+
if unconditional or PytestTool().is_used():
305+
deps += [Dependency(name="pytest-cov")]
306+
return deps
284307

285308
def print_how_to_use(self) -> None:
286309
if PytestTool().is_used():
@@ -324,8 +347,7 @@ class DeptryTool(Tool):
324347
def name(self) -> str:
325348
return "deptry"
326349

327-
@property
328-
def dev_deps(self) -> list[Dependency]:
350+
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
329351
return [Dependency(name="deptry")]
330352

331353
def print_how_to_use(self) -> None:
@@ -374,8 +396,7 @@ class PreCommitTool(Tool):
374396
def name(self) -> str:
375397
return "pre-commit"
376398

377-
@property
378-
def dev_deps(self) -> list[Dependency]:
399+
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
379400
return [Dependency(name="pre-commit")]
380401

381402
def print_how_to_use(self) -> None:
@@ -404,8 +425,7 @@ class PyprojectFmtTool(Tool):
404425
def name(self) -> str:
405426
return "pyproject-fmt"
406427

407-
@property
408-
def dev_deps(self) -> list[Dependency]:
428+
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
409429
return [Dependency(name="pyproject-fmt")]
410430

411431
def print_how_to_use(self) -> None:
@@ -456,8 +476,7 @@ class PyprojectTOMLTool(Tool):
456476
def name(self) -> str:
457477
return "pyproject.toml"
458478

459-
@property
460-
def dev_deps(self) -> list[Dependency]:
479+
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
461480
return []
462481

463482
def print_how_to_use(self) -> None:
@@ -481,9 +500,11 @@ class PytestTool(Tool):
481500
def name(self) -> str:
482501
return "pytest"
483502

484-
@property
485-
def dev_deps(self) -> list[Dependency]:
486-
return [Dependency(name="pytest")]
503+
def get_test_deps(self, *, unconditional: bool = False) -> list[Dependency]:
504+
deps = [Dependency(name="pytest")]
505+
if unconditional or CoverageTool().is_used():
506+
deps += [Dependency(name="pytest-cov")]
507+
return deps
487508

488509
def print_how_to_use(self) -> None:
489510
box_print(
@@ -526,8 +547,7 @@ class RequirementsTxtTool(Tool):
526547
def name(self) -> str:
527548
return "requirements.txt"
528549

529-
@property
530-
def dev_deps(self) -> list[Dependency]:
550+
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
531551
return []
532552

533553
def print_how_to_use(self) -> None:
@@ -565,8 +585,7 @@ class RuffTool(Tool):
565585
def name(self) -> str:
566586
return "Ruff"
567587

568-
@property
569-
def dev_deps(self) -> list[Dependency]:
588+
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
570589
return [Dependency(name="ruff")]
571590

572591
def print_how_to_use(self) -> None:

0 commit comments

Comments
 (0)