Skip to content

Commit

Permalink
214 implement usethis tool pyproject (#325)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nathanjmcdougall authored Feb 21, 2025
1 parent 588ac7e commit fcece11
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 14 deletions.
16 changes: 16 additions & 0 deletions src/usethis/_core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
uninstall_pre_commit_hooks,
)
from usethis._integrations.pre_commit.hooks import add_placeholder_hook, get_hook_names
from usethis._integrations.pyproject.remove import remove_pyproject_toml
from usethis._integrations.pyproject.valid import ensure_pyproject_validity
from usethis._integrations.pytest.core import add_pytest_dir, remove_pytest_dir
from usethis._integrations.ruff.rules import (
deselect_ruff_rules,
Expand All @@ -37,6 +39,7 @@
DeptryTool,
PreCommitTool,
PyprojectFmtTool,
PyprojectTOMLTool,
PytestTool,
RequirementsTxtTool,
RuffTool,
Expand Down Expand Up @@ -209,6 +212,19 @@ def use_pyproject_fmt(*, remove: bool = False) -> None:
remove_deps_from_group(tool.dev_deps, "dev")


def use_pyproject_toml(*, remove: bool = False) -> None:
tool = PyprojectTOMLTool()

ensure_pyproject_toml()

if not remove:
ensure_pyproject_toml()
ensure_pyproject_validity()
tool.print_how_to_use()
else:
remove_pyproject_toml()


def use_pytest(*, remove: bool = False) -> None:
tool = PytestTool()

Expand Down
13 changes: 13 additions & 0 deletions src/usethis/_integrations/pyproject/remove.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from pathlib import Path

from usethis._console import tick_print
from usethis._integrations.pyproject.io_ import pyproject_toml_io_manager


def remove_pyproject_toml() -> None:
path = Path.cwd() / "pyproject.toml"
if path.exists() and path.is_file():
tick_print("Removing 'pyproject.toml' file")
pyproject_toml_io_manager._opener.write_file()
pyproject_toml_io_manager._opener._set = False
path.unlink()
19 changes: 18 additions & 1 deletion src/usethis/_interface/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use_deptry,
use_pre_commit,
use_pyproject_fmt,
use_pyproject_toml,
use_pytest,
use_requirements_txt,
use_ruff,
Expand Down Expand Up @@ -40,7 +41,7 @@ def codespell(
_run_tool(use_codespell, remove=remove)


@app.command(help="Use the coverage code coverage measurement tool.")
@app.command(help="Use coverage: a code coverage measurement tool.")
def coverage(
remove: bool = remove_opt,
offline: bool = offline_opt,
Expand Down Expand Up @@ -99,6 +100,22 @@ def pyproject_fmt(
_run_tool(use_pyproject_fmt, remove=remove)


@app.command(
name="pyproject.toml", help="Use a pyproject.toml file to configure the project."
)
def pyproject_toml(
remove: bool = remove_opt,
offline: bool = offline_opt,
quiet: bool = quiet_opt,
frozen: bool = frozen_opt,
) -> None:
with (
usethis_config.set(offline=offline, quiet=quiet, frozen=frozen),
pyproject_toml_io_manager.open(),
):
_run_tool(use_pyproject_toml, remove=remove)


@app.command(help="Use the pytest testing framework.")
def pytest(
remove: bool = remove_opt,
Expand Down
23 changes: 22 additions & 1 deletion src/usethis/_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pathlib import Path
from typing import Protocol

from usethis._console import box_print, tick_print
from usethis._console import box_print, info_print, tick_print
from usethis._integrations.bitbucket.anchor import (
ScriptItemAnchor as BitbucketScriptItemAnchor,
)
Expand Down Expand Up @@ -439,6 +439,27 @@ def get_bitbucket_steps(self) -> list[BitbucketStep]:
]


class PyprojectTOMLTool(Tool):
@property
def name(self) -> str:
return "pyproject.toml"

@property
def dev_deps(self) -> list[Dependency]:
return []

def print_how_to_use(self) -> None:
box_print("Populate 'pyproject.toml' with the project configuration.")
info_print(
"Learn more at https://packaging.python.org/en/latest/guides/writing-pyproject-toml/"
)

def get_managed_files(self) -> list[Path]:
return [
Path("pyproject.toml"),
]


class PytestTool(Tool):
@property
def name(self) -> str:
Expand Down
23 changes: 23 additions & 0 deletions tests/usethis/_integrations/pyproject/test_remove.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from pathlib import Path

import pytest

from usethis._integrations.pyproject.remove import remove_pyproject_toml
from usethis._test import change_cwd


class TestRemovePyprojectTOML:
def test_removed(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]):
# Arrange
pyproject_path = tmp_path / "pyproject.toml"
pyproject_path.touch()

# Act
with change_cwd(tmp_path):
remove_pyproject_toml()

# Assert
assert not pyproject_path.exists()
out, err = capfd.readouterr()
assert not err
assert out == "✔ Removing 'pyproject.toml' file\n"
42 changes: 31 additions & 11 deletions tests/usethis/_interface/test_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
from usethis._test import change_cwd


class TestCodespell:
def test_add(self, tmp_path: Path):
# Act
runner = CliRunner()
with change_cwd(tmp_path):
result = runner.invoke(app, ["codespell"])

# Assert
assert result.exit_code == 0, result.output


class TestDeptry:
@pytest.mark.usefixtures("_vary_network_conn")
def test_cli(self, uv_init_dir: Path):
Expand All @@ -31,6 +42,26 @@ def test_cli_not_frozen(self, uv_init_dir: Path):
assert (uv_init_dir / ".venv").exists()


class TestPyprojectTOML:
def test_add(self, tmp_path: Path):
# Act
runner = CliRunner()
with change_cwd(tmp_path):
result = runner.invoke(app, ["pyproject.toml"])

# Assert
assert result.exit_code == 0, result.output

def test_remove(self, tmp_path: Path):
# Act
runner = CliRunner()
with change_cwd(tmp_path):
result = runner.invoke(app, ["pyproject.toml", "--remove"])

# Assert
assert result.exit_code == 0, result.output


class TestPreCommit:
@pytest.mark.usefixtures("_vary_network_conn")
def test_cli_pass(self, uv_init_repo_dir: Path):
Expand Down Expand Up @@ -92,17 +123,6 @@ def test_add(self, tmp_path: Path):
assert result.exit_code == 0, result.output


class TestCodespell:
def test_add(self, tmp_path: Path):
# Act
runner = CliRunner()
with change_cwd(tmp_path):
result = runner.invoke(app, ["codespell"])

# Assert
assert result.exit_code == 0, result.output


@pytest.mark.benchmark
def test_several_tools_add_and_remove(tmp_path: Path):
runner = CliRunner()
Expand Down
57 changes: 56 additions & 1 deletion tests/usethis/test_usethis_tool.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from pathlib import Path

import pytest
import requests

from usethis._config import usethis_config
from usethis._console import box_print
from usethis._integrations.pre_commit.hooks import _PLACEHOLDER_ID, get_hook_names
from usethis._integrations.pre_commit.schema import HookDefinition, LocalRepo, UriRepo
Expand All @@ -10,7 +12,7 @@
from usethis._integrations.pyproject.io_ import pyproject_toml_io_manager
from usethis._integrations.uv.deps import Dependency, add_deps_to_group
from usethis._test import change_cwd
from usethis._tool import ALL_TOOLS, DeptryTool, Tool
from usethis._tool import ALL_TOOLS, DeptryTool, PyprojectTOMLTool, Tool


class DefaultTool(Tool):
Expand Down Expand Up @@ -772,3 +774,56 @@ def test_all_tools_config_keys_are_subkeys_of_id_keys(tool: Tool):
assert any(config.id_keys[: len(id_key)] == id_key for id_key in id_keys), (
f"Config keys {config.id_keys} not covered by ID keys in {tool.name}"
)


class TestPyprojectTOMLTool:
class TestPrintHowToUse:
@pytest.mark.usefixtures("_vary_network_conn")
def test_link_isnt_dead(self):
"""A regression test."""

# Arrange
url = (
"https://packaging.python.org/en/latest/guides/writing-pyproject-toml/"
)

if not usethis_config.offline:
# Act
result = requests.head(url)

# Assert
assert result.status_code == 200

def test_some_output(self, capfd: pytest.CaptureFixture[str]):
# Arrange
tool = PyprojectTOMLTool()

# Act
tool.print_how_to_use()

# Assert
out, err = capfd.readouterr()
assert not err
assert out

class TestName:
def test_value(self):
# Arrange
tool = PyprojectTOMLTool()

# Act
result = tool.name

# Assert
assert result == "pyproject.toml"

class TestDevDeps:
def test_none(self):
# Arrange
tool = PyprojectTOMLTool()

# Act
result = tool.dev_deps

# Assert
assert result == []

0 comments on commit fcece11

Please sign in to comment.