Skip to content

Commit

Permalink
Refactor the way tool dependencies are handled (#327)
Browse files Browse the repository at this point in the history
* Refactor the way tool dependencies are handled

* Properly use test deps in is_used heuristic
  • Loading branch information
nathanjmcdougall authored Feb 21, 2025
1 parent b5d051d commit ef12f43
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 84 deletions.
67 changes: 27 additions & 40 deletions src/usethis/_core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@
select_ruff_rules,
)
from usethis._integrations.uv.call import call_uv_subprocess
from usethis._integrations.uv.deps import (
Dependency,
add_deps_to_group,
remove_deps_from_group,
)
from usethis._integrations.uv.init import ensure_pyproject_toml
from usethis._tool import (
ALL_TOOLS,
Expand All @@ -53,7 +48,7 @@ def use_codespell(*, remove: bool = False) -> None:

if not remove:
if not PreCommitTool().is_used():
add_deps_to_group(tool.dev_deps, "dev")
tool.add_dev_deps()
if is_bitbucket_used():
add_bitbucket_steps_in_default(tool.get_bitbucket_steps())
else:
Expand All @@ -65,7 +60,7 @@ def use_codespell(*, remove: bool = False) -> None:
remove_bitbucket_steps_from_default(tool.get_bitbucket_steps())
tool.remove_pyproject_configs()
tool.remove_pre_commit_repo_configs()
remove_deps_from_group(tool.dev_deps, "dev")
tool.remove_dev_deps()
tool.remove_managed_files()


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

if not remove:
deps = tool.dev_deps
if PytestTool().is_used():
deps += [Dependency(name="pytest-cov")]
add_deps_to_group(deps, "test")

tool.add_test_deps()
tool.add_pyproject_configs()
tool.print_how_to_use()
else:
tool.remove_pyproject_configs()
remove_deps_from_group([*tool.dev_deps, Dependency(name="pytest-cov")], "test")
tool.remove_test_deps()
tool.remove_managed_files()


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

if not remove:
add_deps_to_group(tool.dev_deps, "dev")
tool.add_dev_deps()
if PreCommitTool().is_used():
tool.add_pre_commit_repo_configs()
elif is_bitbucket_used():
Expand All @@ -105,33 +96,34 @@ def use_deptry(*, remove: bool = False) -> None:
tool.remove_pre_commit_repo_configs()
tool.remove_pyproject_configs()
remove_bitbucket_steps_from_default(tool.get_bitbucket_steps())
remove_deps_from_group(tool.dev_deps, "dev")
tool.remove_dev_deps()
tool.remove_managed_files()


def use_pre_commit(*, remove: bool = False) -> None:
tool = PreCommitTool()
pyproject_fmt_tool = PyprojectFmtTool()
codespell_tool = CodespellTool()
requirements_txt_tool = RequirementsTxtTool()

ensure_pyproject_toml()

if not remove:
add_deps_to_group(tool.dev_deps, "dev")
tool.add_dev_deps()
_add_all_tools_pre_commit_configs()

# We will use pre-commit instead of project-installed dependencies:
if pyproject_fmt_tool.is_used():
remove_deps_from_group(pyproject_fmt_tool.dev_deps, "dev")
pyproject_fmt_tool.remove_dev_deps()
pyproject_fmt_tool.add_pyproject_configs()
PyprojectFmtTool().print_how_to_use()
pyproject_fmt_tool.print_how_to_use()
if codespell_tool.is_used():
remove_deps_from_group(codespell_tool.dev_deps, "dev")
codespell_tool.remove_dev_deps()
codespell_tool.add_pyproject_configs()
CodespellTool().print_how_to_use()
codespell_tool.print_how_to_use()

if RequirementsTxtTool().is_used():
RequirementsTxtTool().print_how_to_use()
if requirements_txt_tool.is_used():
requirements_txt_tool.print_how_to_use()

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

remove_pre_commit_config()
remove_deps_from_group(tool.dev_deps, "dev")
tool.remove_dev_deps()

# Need to add a new way of running some hooks manually if they are not dev
# dependencies yet - explain to the user.
if pyproject_fmt_tool.is_used():
add_deps_to_group(pyproject_fmt_tool.dev_deps, "dev")
PyprojectFmtTool().print_how_to_use()
pyproject_fmt_tool.add_dev_deps()
pyproject_fmt_tool.print_how_to_use()
if codespell_tool.is_used():
add_deps_to_group(codespell_tool.dev_deps, "dev")
CodespellTool().print_how_to_use()
codespell_tool.add_dev_deps()
codespell_tool.print_how_to_use()

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


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

if not remove:
if not PreCommitTool().is_used():
add_deps_to_group(tool.dev_deps, "dev")
tool.add_dev_deps()
if is_bitbucket_used():
add_bitbucket_steps_in_default(tool.get_bitbucket_steps())
else:
Expand All @@ -212,7 +204,7 @@ def use_pyproject_fmt(*, remove: bool = False) -> None:
remove_bitbucket_steps_from_default(tool.get_bitbucket_steps())
tool.remove_pyproject_configs()
tool.remove_pre_commit_repo_configs()
remove_deps_from_group(tool.dev_deps, "dev")
tool.remove_dev_deps()
tool.remove_managed_files()


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

if not remove:
deps = tool.dev_deps
if CoverageTool().is_used():
deps += [Dependency(name="pytest-cov")]
add_deps_to_group(deps, "test")

tool.add_test_deps()
tool.add_pyproject_configs()
if RuffTool().is_used():
select_ruff_rules(tool.get_associated_ruff_rules())
Expand All @@ -262,8 +250,7 @@ def use_pytest(*, remove: bool = False) -> None:
if RuffTool().is_used():
deselect_ruff_rules(tool.get_associated_ruff_rules())
tool.remove_pyproject_configs()
remove_deps_from_group([*tool.dev_deps, Dependency(name="pytest-cov")], "test")

tool.remove_test_deps()
remove_pytest_dir() # Last, since this is a manual step

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

if not remove:
add_deps_to_group(tool.dev_deps, "dev")
tool.add_dev_deps()
tool.add_pyproject_configs()
select_ruff_rules(rules)
ignore_ruff_rules(ignored_rules)
Expand All @@ -353,5 +340,5 @@ def use_ruff(*, remove: bool = False) -> None:
tool.remove_pre_commit_repo_configs()
remove_bitbucket_steps_from_default(tool.get_bitbucket_steps())
tool.remove_pyproject_configs()
remove_deps_from_group(tool.dev_deps, "dev")
tool.remove_dev_deps()
tool.remove_managed_files()
87 changes: 53 additions & 34 deletions src/usethis/_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@
set_config_value,
)
from usethis._integrations.pyproject.remove import remove_pyproject_toml
from usethis._integrations.uv.deps import Dependency, is_dep_in_any_group
from usethis._integrations.uv.deps import (
Dependency,
add_deps_to_group,
is_dep_in_any_group,
remove_deps_from_group,
)


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

@property
def dev_deps(self) -> list[Dependency]:
"""The tool's development dependencies."""
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
"""The tool's development dependencies.
These should all be considered characteristic of this particular tool.
Args:
unconditional: Whether to return all possible dependencies regardless of
whether they are relevant to the current project.
"""
return []

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

Expand Down Expand Up @@ -111,15 +121,27 @@ def is_used(self) -> bool:
for id_keys in self.get_pyproject_id_keys():
if do_id_keys_exist(id_keys):
return True
for dep in self.dev_deps:
for dep in self.get_dev_deps(unconditional=True):
if is_dep_in_any_group(dep):
return True
for extra_dep in self.get_extra_dev_deps():
if is_dep_in_any_group(extra_dep):
for dep in self.get_test_deps(unconditional=True):
if is_dep_in_any_group(dep):
return True

return False

def add_dev_deps(self) -> None:
add_deps_to_group(self.get_dev_deps(), "dev")

def remove_dev_deps(self) -> None:
remove_deps_from_group(self.get_dev_deps(unconditional=True), "dev")

def add_test_deps(self) -> None:
add_deps_to_group(self.get_test_deps(), "test")

def remove_test_deps(self) -> None:
remove_deps_from_group(self.get_test_deps(unconditional=True), "test")

def add_pre_commit_repo_configs(self) -> None:
"""Add the tool's pre-commit configuration."""
repos = self.get_pre_commit_repos()
Expand Down Expand Up @@ -217,8 +239,7 @@ class CodespellTool(Tool):
def name(self) -> str:
return "Codespell"

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

def print_how_to_use(self) -> None:
Expand Down Expand Up @@ -278,9 +299,11 @@ class CoverageTool(Tool):
def name(self) -> str:
return "coverage"

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

def print_how_to_use(self) -> None:
if PytestTool().is_used():
Expand Down Expand Up @@ -324,8 +347,7 @@ class DeptryTool(Tool):
def name(self) -> str:
return "deptry"

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

def print_how_to_use(self) -> None:
Expand Down Expand Up @@ -374,8 +396,7 @@ class PreCommitTool(Tool):
def name(self) -> str:
return "pre-commit"

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

def print_how_to_use(self) -> None:
Expand Down Expand Up @@ -404,8 +425,7 @@ class PyprojectFmtTool(Tool):
def name(self) -> str:
return "pyproject-fmt"

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

def print_how_to_use(self) -> None:
Expand Down Expand Up @@ -456,8 +476,7 @@ class PyprojectTOMLTool(Tool):
def name(self) -> str:
return "pyproject.toml"

@property
def dev_deps(self) -> list[Dependency]:
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
return []

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

@property
def dev_deps(self) -> list[Dependency]:
return [Dependency(name="pytest")]
def get_test_deps(self, *, unconditional: bool = False) -> list[Dependency]:
deps = [Dependency(name="pytest")]
if unconditional or CoverageTool().is_used():
deps += [Dependency(name="pytest-cov")]
return deps

def print_how_to_use(self) -> None:
box_print(
Expand Down Expand Up @@ -526,8 +547,7 @@ class RequirementsTxtTool(Tool):
def name(self) -> str:
return "requirements.txt"

@property
def dev_deps(self) -> list[Dependency]:
def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
return []

def print_how_to_use(self) -> None:
Expand Down Expand Up @@ -565,8 +585,7 @@ class RuffTool(Tool):
def name(self) -> str:
return "Ruff"

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

def print_how_to_use(self) -> None:
Expand Down
Loading

0 comments on commit ef12f43

Please sign in to comment.