Skip to content

Commit 607b581

Browse files
802 print how to use could give misleading message regarding pre commit (#812)
* Add failing test for codespell's `print_how_to_use` * Simplify test for speed * Tweak test to use expected behaviour * Introduce `get_install_method` method for inferring how a given tool is installed and pass test with placeholder implementation * Add failing test for case where pre-commit is the way codespell is installed for print_how_to_use * Add `is_declared_as_dep` to functionalize checking whether a tool's characteristic dependencies are declared as dependencies * Pass tests by implementing method to check whether a tool is installed as a pre-commit and implementing the `get_install_method` abstraction
1 parent 906d62c commit 607b581

4 files changed

Lines changed: 160 additions & 16 deletions

File tree

src/usethis/_tool/base.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from abc import abstractmethod
4-
from typing import TYPE_CHECKING, Protocol
4+
from typing import TYPE_CHECKING, Literal, Protocol
55

66
from typing_extensions import assert_never
77

@@ -175,6 +175,24 @@ def get_pre_commit_repos(self) -> list[LocalRepo | UriRepo]:
175175
"""Get the pre-commit repository definitions for the tool."""
176176
return [c.repo for c in self.get_pre_commit_config().repo_configs]
177177

178+
def is_pre_commit_config_present(self) -> bool:
179+
"""Whether the tool's pre-commit configuration is present."""
180+
repo_configs = self.get_pre_commit_repos()
181+
182+
for repo_config in repo_configs:
183+
if repo_config.hooks is None:
184+
continue
185+
186+
# Check if any of the hooks are present.
187+
for hook in repo_config.hooks:
188+
if any(
189+
hook_ids_are_equivalent(hook.id, hook_id)
190+
for hook_id in get_hook_ids()
191+
):
192+
return True
193+
194+
return False
195+
178196
def add_pre_commit_config(self) -> None:
179197
"""Add the tool's pre-commit configuration."""
180198
repos = self.get_pre_commit_repos()
@@ -463,6 +481,15 @@ def remove_managed_files(self) -> None:
463481
tick_print(f"Removing '{file}'.")
464482
file.unlink()
465483

484+
def get_install_method(self) -> Literal["pre-commit", "devdep"] | None:
485+
"""Infer the method used to install the tool, return None is uninstalled."""
486+
if self.is_declared_as_dep():
487+
return "devdep"
488+
489+
if self.is_pre_commit_config_present():
490+
return "pre-commit"
491+
return None
492+
466493
def get_bitbucket_steps(self) -> list[BitbucketStep]:
467494
"""Get the Bitbucket pipeline step associated with this tool."""
468495
return []

src/usethis/_tool/impl/codespell.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
from pathlib import Path
44

5-
from usethis._config_file import (
6-
CodespellRCManager,
7-
)
5+
from typing_extensions import assert_never
6+
7+
from usethis._config_file import CodespellRCManager
88
from usethis._console import box_print
99
from usethis._integrations.ci.bitbucket.anchor import (
1010
ScriptItemAnchor as BitbucketScriptItemAnchor,
@@ -13,17 +13,11 @@
1313
from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep
1414
from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager
1515
from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager
16-
from usethis._integrations.pre_commit.schema import (
17-
HookDefinition,
18-
UriRepo,
19-
)
20-
from usethis._integrations.uv.deps import (
21-
Dependency,
22-
)
16+
from usethis._integrations.pre_commit.schema import HookDefinition, UriRepo
17+
from usethis._integrations.uv.deps import Dependency
2318
from usethis._integrations.uv.used import is_uv_used
2419
from usethis._tool.base import Tool
2520
from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec
26-
from usethis._tool.impl.pre_commit import PreCommitTool
2721
from usethis._tool.pre_commit import PreCommitConfig
2822

2923

@@ -34,7 +28,8 @@ def name(self) -> str:
3428
return "Codespell"
3529

3630
def print_how_to_use(self) -> None:
37-
if PreCommitTool().is_used():
31+
install_method = self.get_install_method()
32+
if install_method == "pre-commit":
3833
if is_uv_used():
3934
box_print(
4035
"Run 'uv run pre-commit run codespell --all-files' to run the Codespell spellchecker."
@@ -43,10 +38,13 @@ def print_how_to_use(self) -> None:
4338
box_print(
4439
"Run 'pre-commit run codespell --all-files' to run the Codespell spellchecker."
4540
)
46-
elif is_uv_used():
47-
box_print("Run 'uv run codespell' to run the Codespell spellchecker.")
41+
elif install_method == "devdep" or install_method is None:
42+
if is_uv_used():
43+
box_print("Run 'uv run codespell' to run the Codespell spellchecker.")
44+
else:
45+
box_print("Run 'codespell' to run the Codespell spellchecker.")
4846
else:
49-
box_print("Run 'codespell' to run the Codespell spellchecker.")
47+
assert_never(install_method)
5048

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

tests/usethis/_tool/impl/test_codespell.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,103 @@
11
import os
2+
from pathlib import Path
23

34
import pytest
45

6+
from usethis._config_file import files_manager
57
from usethis._integrations.ci.github.errors import GitHubTagError
68
from usethis._integrations.ci.github.tags import get_github_latest_tag
79
from usethis._integrations.pre_commit.schema import UriRepo
10+
from usethis._test import change_cwd
811
from usethis._tool.impl.codespell import CodespellTool
912

1013

1114
class TestCodespellTool:
15+
class TestPrintHowToUse:
16+
def test_pre_commit_used(
17+
self, tmp_path: Path, capfd: pytest.CaptureFixture[str]
18+
):
19+
# https://github.com/usethis-python/usethis-python/issues/802
20+
21+
# Arrange
22+
(tmp_path / ".pre-commit-config.yaml").write_text(
23+
"""\
24+
repos:
25+
- repo: https://github.com/codespell-project/codespell
26+
rev: v2.4.1
27+
hooks:
28+
- id: codespell
29+
additional_dependencies:
30+
- tomli
31+
"""
32+
)
33+
34+
# Act
35+
with change_cwd(tmp_path), files_manager():
36+
CodespellTool().print_how_to_use()
37+
38+
# Assert
39+
out, err = capfd.readouterr()
40+
assert not err
41+
assert out == (
42+
"☐ Run 'pre-commit run codespell --all-files' to run the Codespell spellchecker.\n"
43+
)
44+
45+
def test_pre_commit_used_but_not_configured(
46+
self, tmp_path: Path, capfd: pytest.CaptureFixture[str]
47+
):
48+
# https://github.com/usethis-python/usethis-python/issues/802
49+
50+
# Arrange
51+
(tmp_path / "pyproject.toml").write_text(
52+
"""\
53+
[tool.codespell]
54+
"""
55+
)
56+
(tmp_path / ".pre-commit-config.yaml").write_text(
57+
"""\
58+
repos: []
59+
"""
60+
)
61+
62+
# Act
63+
with change_cwd(tmp_path), files_manager():
64+
CodespellTool().print_how_to_use()
65+
66+
# Assert
67+
out, err = capfd.readouterr()
68+
assert not err
69+
assert out == ("☐ Run 'codespell' to run the Codespell spellchecker.\n")
70+
71+
def test_both_devdep_and_pre_commit_used(
72+
self, tmp_path: Path, capfd: pytest.CaptureFixture[str]
73+
):
74+
# Arrange
75+
(tmp_path / "pyproject.toml").write_text(
76+
"""\
77+
[dependency-groups]
78+
dev = ["codespell"]
79+
"""
80+
)
81+
(tmp_path / ".pre-commit-config.yaml").write_text(
82+
"""\
83+
repos:
84+
- repo: https://github.com/codespell-project/codespell
85+
rev: v2.4.1
86+
hooks:
87+
- id: codespell
88+
additional_dependencies:
89+
- tomli
90+
"""
91+
)
92+
# Act
93+
with change_cwd(tmp_path), files_manager():
94+
CodespellTool().print_how_to_use()
95+
96+
# Assert
97+
out, err = capfd.readouterr()
98+
assert not err
99+
assert out == ("☐ Run 'codespell' to run the Codespell spellchecker.\n")
100+
12101
def test_latest_version(self):
13102
(config,) = CodespellTool().get_pre_commit_config().repo_configs
14103
repo = config.repo

tests/usethis/_tool/test_base.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,36 @@ def test_dev_deps(self, uv_init_dir: Path):
423423
# Assert
424424
assert result
425425

426+
class TestIsPreCommitConfigPresent:
427+
def test_no_file(self, tmp_path: Path):
428+
# Arrange
429+
tool = MyTool()
430+
431+
# Act
432+
with change_cwd(tmp_path):
433+
result = tool.is_pre_commit_config_present()
434+
435+
# Assert
436+
assert not result
437+
438+
def test_config_present(self, tmp_path: Path):
439+
# Arrange
440+
tool = MyTool()
441+
442+
# Create a pre-commit config file
443+
(tmp_path / ".pre-commit-config.yaml").write_text("""\
444+
repos:
445+
- repo: local
446+
hooks:
447+
- id: deptry
448+
""")
449+
# Act
450+
with change_cwd(tmp_path):
451+
result = tool.is_pre_commit_config_present()
452+
453+
# Assert
454+
assert result
455+
426456
class TestAddPreCommitConfig:
427457
def test_no_repo_configs(self, uv_init_dir: Path):
428458
# Arrange

0 commit comments

Comments
 (0)