Skip to content

Commit fcece11

Browse files
214 implement usethis tool pyproject (#325)
* Add failing tests for `usethis tool pyproject.toml` * Implement `usethis tool pyproject.toml` * Tweak tool descriptions * Fix TOML manager when deleting file * Add unit tests to tool class
1 parent 588ac7e commit fcece11

7 files changed

Lines changed: 179 additions & 14 deletions

File tree

src/usethis/_core/tool.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
uninstall_pre_commit_hooks,
1818
)
1919
from usethis._integrations.pre_commit.hooks import add_placeholder_hook, get_hook_names
20+
from usethis._integrations.pyproject.remove import remove_pyproject_toml
21+
from usethis._integrations.pyproject.valid import ensure_pyproject_validity
2022
from usethis._integrations.pytest.core import add_pytest_dir, remove_pytest_dir
2123
from usethis._integrations.ruff.rules import (
2224
deselect_ruff_rules,
@@ -37,6 +39,7 @@
3739
DeptryTool,
3840
PreCommitTool,
3941
PyprojectFmtTool,
42+
PyprojectTOMLTool,
4043
PytestTool,
4144
RequirementsTxtTool,
4245
RuffTool,
@@ -209,6 +212,19 @@ def use_pyproject_fmt(*, remove: bool = False) -> None:
209212
remove_deps_from_group(tool.dev_deps, "dev")
210213

211214

215+
def use_pyproject_toml(*, remove: bool = False) -> None:
216+
tool = PyprojectTOMLTool()
217+
218+
ensure_pyproject_toml()
219+
220+
if not remove:
221+
ensure_pyproject_toml()
222+
ensure_pyproject_validity()
223+
tool.print_how_to_use()
224+
else:
225+
remove_pyproject_toml()
226+
227+
212228
def use_pytest(*, remove: bool = False) -> None:
213229
tool = PytestTool()
214230

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from pathlib import Path
2+
3+
from usethis._console import tick_print
4+
from usethis._integrations.pyproject.io_ import pyproject_toml_io_manager
5+
6+
7+
def remove_pyproject_toml() -> None:
8+
path = Path.cwd() / "pyproject.toml"
9+
if path.exists() and path.is_file():
10+
tick_print("Removing 'pyproject.toml' file")
11+
pyproject_toml_io_manager._opener.write_file()
12+
pyproject_toml_io_manager._opener._set = False
13+
path.unlink()

src/usethis/_interface/tool.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use_deptry,
1111
use_pre_commit,
1212
use_pyproject_fmt,
13+
use_pyproject_toml,
1314
use_pytest,
1415
use_requirements_txt,
1516
use_ruff,
@@ -40,7 +41,7 @@ def codespell(
4041
_run_tool(use_codespell, remove=remove)
4142

4243

43-
@app.command(help="Use the coverage code coverage measurement tool.")
44+
@app.command(help="Use coverage: a code coverage measurement tool.")
4445
def coverage(
4546
remove: bool = remove_opt,
4647
offline: bool = offline_opt,
@@ -99,6 +100,22 @@ def pyproject_fmt(
99100
_run_tool(use_pyproject_fmt, remove=remove)
100101

101102

103+
@app.command(
104+
name="pyproject.toml", help="Use a pyproject.toml file to configure the project."
105+
)
106+
def pyproject_toml(
107+
remove: bool = remove_opt,
108+
offline: bool = offline_opt,
109+
quiet: bool = quiet_opt,
110+
frozen: bool = frozen_opt,
111+
) -> None:
112+
with (
113+
usethis_config.set(offline=offline, quiet=quiet, frozen=frozen),
114+
pyproject_toml_io_manager.open(),
115+
):
116+
_run_tool(use_pyproject_toml, remove=remove)
117+
118+
102119
@app.command(help="Use the pytest testing framework.")
103120
def pytest(
104121
remove: bool = remove_opt,

src/usethis/_tool.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from pathlib import Path
33
from typing import Protocol
44

5-
from usethis._console import box_print, tick_print
5+
from usethis._console import box_print, info_print, tick_print
66
from usethis._integrations.bitbucket.anchor import (
77
ScriptItemAnchor as BitbucketScriptItemAnchor,
88
)
@@ -439,6 +439,27 @@ def get_bitbucket_steps(self) -> list[BitbucketStep]:
439439
]
440440

441441

442+
class PyprojectTOMLTool(Tool):
443+
@property
444+
def name(self) -> str:
445+
return "pyproject.toml"
446+
447+
@property
448+
def dev_deps(self) -> list[Dependency]:
449+
return []
450+
451+
def print_how_to_use(self) -> None:
452+
box_print("Populate 'pyproject.toml' with the project configuration.")
453+
info_print(
454+
"Learn more at https://packaging.python.org/en/latest/guides/writing-pyproject-toml/"
455+
)
456+
457+
def get_managed_files(self) -> list[Path]:
458+
return [
459+
Path("pyproject.toml"),
460+
]
461+
462+
442463
class PytestTool(Tool):
443464
@property
444465
def name(self) -> str:
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from usethis._integrations.pyproject.remove import remove_pyproject_toml
6+
from usethis._test import change_cwd
7+
8+
9+
class TestRemovePyprojectTOML:
10+
def test_removed(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]):
11+
# Arrange
12+
pyproject_path = tmp_path / "pyproject.toml"
13+
pyproject_path.touch()
14+
15+
# Act
16+
with change_cwd(tmp_path):
17+
remove_pyproject_toml()
18+
19+
# Assert
20+
assert not pyproject_path.exists()
21+
out, err = capfd.readouterr()
22+
assert not err
23+
assert out == "✔ Removing 'pyproject.toml' file\n"

tests/usethis/_interface/test_tool.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@
99
from usethis._test import change_cwd
1010

1111

12+
class TestCodespell:
13+
def test_add(self, tmp_path: Path):
14+
# Act
15+
runner = CliRunner()
16+
with change_cwd(tmp_path):
17+
result = runner.invoke(app, ["codespell"])
18+
19+
# Assert
20+
assert result.exit_code == 0, result.output
21+
22+
1223
class TestDeptry:
1324
@pytest.mark.usefixtures("_vary_network_conn")
1425
def test_cli(self, uv_init_dir: Path):
@@ -31,6 +42,26 @@ def test_cli_not_frozen(self, uv_init_dir: Path):
3142
assert (uv_init_dir / ".venv").exists()
3243

3344

45+
class TestPyprojectTOML:
46+
def test_add(self, tmp_path: Path):
47+
# Act
48+
runner = CliRunner()
49+
with change_cwd(tmp_path):
50+
result = runner.invoke(app, ["pyproject.toml"])
51+
52+
# Assert
53+
assert result.exit_code == 0, result.output
54+
55+
def test_remove(self, tmp_path: Path):
56+
# Act
57+
runner = CliRunner()
58+
with change_cwd(tmp_path):
59+
result = runner.invoke(app, ["pyproject.toml", "--remove"])
60+
61+
# Assert
62+
assert result.exit_code == 0, result.output
63+
64+
3465
class TestPreCommit:
3566
@pytest.mark.usefixtures("_vary_network_conn")
3667
def test_cli_pass(self, uv_init_repo_dir: Path):
@@ -92,17 +123,6 @@ def test_add(self, tmp_path: Path):
92123
assert result.exit_code == 0, result.output
93124

94125

95-
class TestCodespell:
96-
def test_add(self, tmp_path: Path):
97-
# Act
98-
runner = CliRunner()
99-
with change_cwd(tmp_path):
100-
result = runner.invoke(app, ["codespell"])
101-
102-
# Assert
103-
assert result.exit_code == 0, result.output
104-
105-
106126
@pytest.mark.benchmark
107127
def test_several_tools_add_and_remove(tmp_path: Path):
108128
runner = CliRunner()

tests/usethis/test_usethis_tool.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from pathlib import Path
22

33
import pytest
4+
import requests
45

6+
from usethis._config import usethis_config
57
from usethis._console import box_print
68
from usethis._integrations.pre_commit.hooks import _PLACEHOLDER_ID, get_hook_names
79
from usethis._integrations.pre_commit.schema import HookDefinition, LocalRepo, UriRepo
@@ -10,7 +12,7 @@
1012
from usethis._integrations.pyproject.io_ import pyproject_toml_io_manager
1113
from usethis._integrations.uv.deps import Dependency, add_deps_to_group
1214
from usethis._test import change_cwd
13-
from usethis._tool import ALL_TOOLS, DeptryTool, Tool
15+
from usethis._tool import ALL_TOOLS, DeptryTool, PyprojectTOMLTool, Tool
1416

1517

1618
class DefaultTool(Tool):
@@ -772,3 +774,56 @@ def test_all_tools_config_keys_are_subkeys_of_id_keys(tool: Tool):
772774
assert any(config.id_keys[: len(id_key)] == id_key for id_key in id_keys), (
773775
f"Config keys {config.id_keys} not covered by ID keys in {tool.name}"
774776
)
777+
778+
779+
class TestPyprojectTOMLTool:
780+
class TestPrintHowToUse:
781+
@pytest.mark.usefixtures("_vary_network_conn")
782+
def test_link_isnt_dead(self):
783+
"""A regression test."""
784+
785+
# Arrange
786+
url = (
787+
"https://packaging.python.org/en/latest/guides/writing-pyproject-toml/"
788+
)
789+
790+
if not usethis_config.offline:
791+
# Act
792+
result = requests.head(url)
793+
794+
# Assert
795+
assert result.status_code == 200
796+
797+
def test_some_output(self, capfd: pytest.CaptureFixture[str]):
798+
# Arrange
799+
tool = PyprojectTOMLTool()
800+
801+
# Act
802+
tool.print_how_to_use()
803+
804+
# Assert
805+
out, err = capfd.readouterr()
806+
assert not err
807+
assert out
808+
809+
class TestName:
810+
def test_value(self):
811+
# Arrange
812+
tool = PyprojectTOMLTool()
813+
814+
# Act
815+
result = tool.name
816+
817+
# Assert
818+
assert result == "pyproject.toml"
819+
820+
class TestDevDeps:
821+
def test_none(self):
822+
# Arrange
823+
tool = PyprojectTOMLTool()
824+
825+
# Act
826+
result = tool.dev_deps
827+
828+
# Assert
829+
assert result == []

0 commit comments

Comments
 (0)