Skip to content
Merged
71 changes: 67 additions & 4 deletions src/usethis/_tool/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
from usethis._config import usethis_config
from usethis._console import tick_print, warn_print
from usethis._deps import add_deps_to_group, is_dep_in_any_group, remove_deps_from_group
from usethis._integrations.backend.dispatch import get_backend
from usethis._integrations.ci.bitbucket.anchor import (
ScriptItemAnchor as BitbucketScriptItemAnchor,
)
from usethis._integrations.ci.bitbucket.schema import Script as BitbucketScript
from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep
from usethis._integrations.ci.bitbucket.steps import (
add_bitbucket_step_in_default,
bitbucket_steps_are_equivalent,
Expand All @@ -25,13 +31,13 @@
from usethis._tool.config import ConfigSpec, NoConfigValue
from usethis._tool.pre_commit import PreCommitConfig
from usethis._tool.rule import RuleConfig
from usethis.errors import FileConfigError
from usethis._types.backend import BackendEnum
from usethis.errors import FileConfigError, NoDefaultToolCommand

if TYPE_CHECKING:
from pathlib import Path

from usethis._integrations.backend.uv.deps import Dependency
from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep
from usethis._integrations.pre_commit.schema import LocalRepo, UriRepo
from usethis._io import KeyValueFileManager
from usethis._tool.config import ConfigItem, ResolutionT
Expand All @@ -56,6 +62,26 @@ def print_how_to_use(self) -> None:
"""
pass

def default_command(self) -> str:
"""The default command to run the tool, backend-dependent.

This method returns the command string for running the tool, which varies
based on the current backend (e.g., "uv", "none"). This is used to avoid
duplication in get_bitbucket_steps methods and help messages.

Returns:
The command string for running the tool.

Raises:
NoDefaultToolCommand: If the tool has no associated command.

Examples:
For codespell with uv backend: "uv run codespell"
For codespell with none backend: "codespell"
"""
msg = f"{self.name} has no default command."
raise NoDefaultToolCommand(msg)

def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
"""The tool's development dependencies.

Expand Down Expand Up @@ -545,8 +571,45 @@ def get_install_method(self) -> Literal["pre-commit", "devdep"] | None:
return None

def get_bitbucket_steps(self) -> list[BitbucketStep]:
"""Get the Bitbucket pipeline step associated with this tool."""
return []
"""Get the Bitbucket pipeline step associated with this tool.

By default, this creates a single step using the tool's default_command().
Tools can override this method for more complex step requirements (e.g., pytest
with multiple Python versions, or Ruff with separate linter/formatter steps).
"""
try:
cmd = self.default_command()
except NoDefaultToolCommand:
return []

backend = get_backend()
if backend is BackendEnum.uv:
return [
BitbucketStep(
name=f"Run {self.name}",
caches=["uv"],
script=BitbucketScript(
[
BitbucketScriptItemAnchor(name="install-uv"),
cmd,
]
),
)
]
elif backend is BackendEnum.none:
return [
BitbucketStep(
name=f"Run {self.name}",
script=BitbucketScript(
[
BitbucketScriptItemAnchor(name="ensure-venv"),
cmd,
]
),
)
]
else:
assert_never(backend)

def get_managed_bitbucket_step_names(self) -> list[str]:
"""These are the names of the Bitbucket steps that are managed by this tool.
Expand Down
52 changes: 11 additions & 41 deletions src/usethis/_tool/impl/codespell.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@
from usethis._console import how_print
from usethis._integrations.backend.dispatch import get_backend
from usethis._integrations.backend.uv.used import is_uv_used
from usethis._integrations.ci.bitbucket.anchor import (
ScriptItemAnchor as BitbucketScriptItemAnchor,
)
from usethis._integrations.ci.bitbucket.schema import Script as BitbucketScript
from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep
from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager
from usethis._integrations.pre_commit.schema import HookDefinition, UriRepo
Expand All @@ -36,6 +31,15 @@ class CodespellTool(Tool):
def name(self) -> str:
return "Codespell"

def default_command(self) -> str:
backend = get_backend()
if backend is BackendEnum.uv and is_uv_used():
return "uv run codespell"
elif backend is BackendEnum.none or backend is BackendEnum.uv:
return "codespell"
else:
assert_never(backend)

def print_how_to_use(self) -> None:
backend = get_backend()
install_method = self.get_install_method()
Expand All @@ -50,11 +54,8 @@ def print_how_to_use(self) -> None:
"Run 'pre-commit run codespell --all-files' to run the Codespell spellchecker."
)
elif install_method == "devdep" or install_method is None:
if backend is BackendEnum.uv and is_uv_used():
how_print("Run 'uv run codespell' to run the Codespell spellchecker.")
else:
assert backend in (BackendEnum.none, BackendEnum.uv)
how_print("Run 'codespell' to run the Codespell spellchecker.")
cmd = self.default_command()
how_print(f"Run '{cmd}' to run the Codespell spellchecker.")
else:
assert_never(install_method)

Expand Down Expand Up @@ -136,34 +137,3 @@ def get_pre_commit_config(self) -> PreCommitConfig:
),
requires_venv=False,
)

def get_bitbucket_steps(self) -> list[BitbucketStep]:
backend = get_backend()

if backend is BackendEnum.uv:
return [
BitbucketStep(
name=f"Run {self.name}",
caches=["uv"],
script=BitbucketScript(
[
BitbucketScriptItemAnchor(name="install-uv"),
"uv run codespell",
]
),
)
]
elif backend is BackendEnum.none:
return [
BitbucketStep(
name=f"Run {self.name}",
script=BitbucketScript(
[
BitbucketScriptItemAnchor(name="ensure-venv"),
"codespell",
]
),
)
]
else:
assert_never(backend)
55 changes: 12 additions & 43 deletions src/usethis/_tool/impl/deptry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@
from usethis._console import how_print, info_print, tick_print
from usethis._integrations.backend.dispatch import get_backend
from usethis._integrations.backend.uv.used import is_uv_used
from usethis._integrations.ci.bitbucket.anchor import (
ScriptItemAnchor as BitbucketScriptItemAnchor,
)
from usethis._integrations.ci.bitbucket.schema import Script as BitbucketScript
from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep
from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._integrations.pre_commit.schema import HookDefinition, Language, LocalRepo
from usethis._integrations.project.layout import get_source_dir_str
Expand All @@ -38,6 +33,16 @@ class DeptryTool(Tool):
def name(self) -> str:
return "deptry"

def default_command(self) -> str:
backend = get_backend()
_dir = get_source_dir_str()
if backend is BackendEnum.uv and is_uv_used():
return f"uv run deptry {_dir}"
elif backend is BackendEnum.none or backend is BackendEnum.uv:
return f"deptry {_dir}"
else:
assert_never(backend)

def print_how_to_use(self) -> None:
_dir = get_source_dir_str()
install_method = self.get_install_method()
Expand All @@ -53,11 +58,8 @@ def print_how_to_use(self) -> None:
f"Run 'pre-commit run deptry --all-files' to run {self.name}."
)
elif install_method == "devdep" or install_method is None:
if backend is BackendEnum.uv and is_uv_used():
how_print(f"Run 'uv run deptry {_dir}' to run deptry.")
else:
assert backend in (BackendEnum.none, BackendEnum.uv)
how_print(f"Run 'deptry {_dir}' to run deptry.")
cmd = self.default_command()
how_print(f"Run '{cmd}' to run deptry.")
else:
assert_never(install_method)

Expand Down Expand Up @@ -129,39 +131,6 @@ def get_pre_commit_config(self) -> PreCommitConfig:
else:
assert_never(backend)

def get_bitbucket_steps(self) -> list[BitbucketStep]:
backend = get_backend()

_dir = get_source_dir_str()

if backend is BackendEnum.uv:
return [
BitbucketStep(
name=f"Run {self.name}",
caches=["uv"],
script=BitbucketScript(
[
BitbucketScriptItemAnchor(name="install-uv"),
f"uv run deptry {_dir}",
]
),
)
]
elif backend is BackendEnum.none:
return [
BitbucketStep(
name=f"Run {self.name}",
script=BitbucketScript(
[
BitbucketScriptItemAnchor(name="ensure-venv"),
f"deptry {_dir}",
]
),
)
]
else:
assert_never(backend)

def is_managed_rule(self, rule: Rule) -> bool:
return rule.startswith("DEP") and rule[3:].isdigit()

Expand Down
52 changes: 11 additions & 41 deletions src/usethis/_tool/impl/import_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@
from usethis._console import how_print, info_print, warn_print
from usethis._integrations.backend.dispatch import get_backend
from usethis._integrations.backend.uv.used import is_uv_used
from usethis._integrations.ci.bitbucket.anchor import (
ScriptItemAnchor as BitbucketScriptItemAnchor,
)
from usethis._integrations.ci.bitbucket.schema import Script as BitbucketScript
from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep
from usethis._integrations.file.ini.io_ import INIFileManager
from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager
Expand Down Expand Up @@ -59,6 +54,15 @@ def is_used(self) -> bool:
with usethis_config.set(quiet=True):
return super().is_used()

def default_command(self) -> str:
backend = get_backend()
if backend is BackendEnum.uv and is_uv_used():
return "uv run lint-imports"
elif backend is BackendEnum.none or backend is BackendEnum.uv:
return "lint-imports"
else:
assert_never(backend)

def print_how_to_use(self) -> None:
if not _is_inp_rule_selected():
# If Ruff is used, we enable the INP rules instead.
Expand All @@ -79,11 +83,8 @@ def print_how_to_use(self) -> None:
f"Run 'pre-commit run import-linter --all-files' to run {self.name}."
)
elif install_method == "devdep" or install_method is None:
if backend is BackendEnum.uv and is_uv_used():
how_print(f"Run 'uv run lint-imports' to run {self.name}.")
else:
assert backend in (BackendEnum.none, BackendEnum.uv)
how_print(f"Run 'lint-imports' to run {self.name}.")
cmd = self.default_command()
how_print(f"Run '{cmd}' to run {self.name}.")
else:
assert_never(install_method)

Expand Down Expand Up @@ -365,37 +366,6 @@ def get_pre_commit_config(self) -> PreCommitConfig:
def get_managed_files(self) -> list[Path]:
return [Path(".importlinter")]

def get_bitbucket_steps(self) -> list[BitbucketStep]:
backend = get_backend()

if backend is BackendEnum.uv:
return [
BitbucketStep(
name=f"Run {self.name}",
caches=["uv"],
script=BitbucketScript(
[
BitbucketScriptItemAnchor(name="install-uv"),
"uv run lint-imports",
]
),
)
]
elif backend is BackendEnum.none:
return [
BitbucketStep(
name=f"Run {self.name}",
script=BitbucketScript(
[
BitbucketScriptItemAnchor(name="ensure-venv"),
"lint-imports",
]
),
)
]
else:
assert_never(backend)

def get_rule_config(self) -> RuleConfig:
return RuleConfig(unmanaged_selected=["INP"], tests_unmanaged_ignored=["INP"])

Expand Down
Loading