Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/usethis/_core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from typing import TYPE_CHECKING, Protocol

from typing_extensions import assert_never

from usethis._config import usethis_config
from usethis._console import box_print, tick_print
from usethis._integrations.ci.bitbucket.used import is_bitbucket_used
Expand Down Expand Up @@ -35,6 +37,7 @@
from usethis._tool.rule import RuleConfig

if TYPE_CHECKING:
from usethis._tool.all_ import SupportedToolType
from usethis._tool.base import Tool

# Note - all these functions invoke ensure_pyproject_toml() at the start, since
Expand Down Expand Up @@ -467,3 +470,50 @@ def _get_basic_rule_config() -> RuleConfig:
)

return rule_config


def use_tool(
tool: SupportedToolType,
*,
remove: bool = False,
how: bool = False,
) -> None:
"""General dispatch function to add or remove a tool to/from the project.

This is mostly intended for situations when the exact tool being added is not known
dynamically. If you know the specific tool you wish to add, it is strongly
recommended to call the specific function directly, e.g. `use_codespell()`, etc.
"""
# One might wonder why we don't just implement a `use` method on the Tool class
# itself. Basically it's for architectural reasons: we want to keep a layer of
# abstraction between the tool and the logic to actually configure it.
# In the future, that might change if we can create a sufficiently generalized logic
# for all tools such that bespoke choices on a per-tool basis are not required, and
# all the logic is just deterministic based on the tool's properties/methods, etc.
if isinstance(tool, CodespellTool):
use_codespell(remove=remove, how=how)
elif isinstance(tool, CoveragePyTool):
use_coverage_py(remove=remove, how=how)
elif isinstance(tool, DeptryTool):
use_deptry(remove=remove, how=how)
elif isinstance(tool, ImportLinterTool):
use_import_linter(remove=remove, how=how)
elif isinstance(tool, PreCommitTool):
use_pre_commit(remove=remove, how=how)
elif isinstance(tool, PyprojectFmtTool):
use_pyproject_fmt(remove=remove, how=how)
elif isinstance(tool, PyprojectTOMLTool):
use_pyproject_toml(remove=remove, how=how)
elif isinstance(tool, PytestTool):
use_pytest(remove=remove, how=how)
elif isinstance(tool, RequirementsTxtTool):
use_requirements_txt(remove=remove, how=how)
elif isinstance(tool, RuffTool):
use_ruff(remove=remove, how=how)
else:
# Having the assert_never here is effectively a way of testing cases are
# exhaustively handled, which ensures it is kept up to date with ALL_TOOLS,
# together with the type annotation on ALL_TOOLS itself. That's why this
# function is implemented as a series of `if` statements rather than a
# dictionary or similar alternative.
assert_never(tool)
18 changes: 14 additions & 4 deletions src/usethis/_tool/all_.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TypeAlias

from usethis._tool.impl.codespell import CodespellTool
from usethis._tool.impl.coverage_py import CoveragePyTool
Expand All @@ -13,10 +13,20 @@
from usethis._tool.impl.requirements_txt import RequirementsTxtTool
from usethis._tool.impl.ruff import RuffTool

if TYPE_CHECKING:
from usethis._tool.base import Tool
SupportedToolType: TypeAlias = (
CodespellTool
| CoveragePyTool
| DeptryTool
| ImportLinterTool
| PreCommitTool
| PyprojectFmtTool
| PyprojectTOMLTool
| PytestTool
| RequirementsTxtTool
| RuffTool
)

ALL_TOOLS: list[Tool] = [
ALL_TOOLS: list[SupportedToolType] = [
CodespellTool(),
CoveragePyTool(),
DeptryTool(),
Expand Down
26 changes: 26 additions & 0 deletions tests/usethis/_core/test_core_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use_pytest,
use_requirements_txt,
use_ruff,
use_tool,
)
from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._integrations.pre_commit.hooks import (
Expand Down Expand Up @@ -1867,6 +1868,23 @@ def test_add_unsubsumed_tools(self, uv_init_repo_dir: Path):
assert "deptry" in contents
assert "ruff" in contents

class TestMultipleIntegrations:
def test_hooks_run_all_tools_empty_repo(self, uv_env_dir: Path):
# Arrange
with change_cwd(uv_env_dir), files_manager():
# Add the tools
use_pre_commit()
for tool in ALL_TOOLS:
if tool.get_pre_commit_repos():
use_tool(tool)

# Act, Assert
# Run the pre-commit hooks via subprocess - check it doesn't raise
call_uv_subprocess(
["run", "pre-commit", "run", "--all-files"],
change_toml=False,
)


class TestPyprojectFmt:
class TestAdd:
Expand Down Expand Up @@ -3450,3 +3468,11 @@ def test_removed_from_all_files(self, uv_init_dir: Path):
"[tool.ruff.lint]" not in (uv_init_dir / "pyproject.toml").read_text()
)
assert not (uv_init_dir / "ruff.toml").exists()


class TestUseTool:
def test_add_all_tool(self, uv_init_dir: Path):
# Act
with change_cwd(uv_init_dir), files_manager():
for tool in ALL_TOOLS:
use_tool(tool)