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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ To use pytest, run:

```console
$ uvx usethis tool pytest
✔ Adding dependencies 'pytest', 'pytest-cov', 'coverage' to the 'test' dependency group in 'pyproject.toml'.
✔ Adding dependencies 'pytest', 'pytest-cov' to the 'test' dependency group in 'pyproject.toml'.
✔ Adding pytest config to 'pyproject.toml'.
✔ Enabling Ruff rule 'PT' in 'pyproject.toml'.
✔ Creating '/tests'.
Expand Down
86 changes: 70 additions & 16 deletions src/usethis/_core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from usethis._integrations.uv.init import ensure_pyproject_toml
from usethis._tool import (
ALL_TOOLS,
CoverageTool,
DeptryTool,
PreCommitTool,
PyprojectFmtTool,
Expand All @@ -34,6 +35,33 @@
)


def use_coverage(*, remove: bool = False) -> None:
tool = CoverageTool()

ensure_pyproject_toml()

if not remove:
add_deps_to_group(tool.dev_deps, "test")

tool.add_pyproject_configs()

if PytestTool().is_used():
_coverage_instructions_pytest()
else:
_coverage_instructions_basic()
else:
tool.remove_pyproject_configs()
remove_deps_from_group(tool.dev_deps, "test")


def _coverage_instructions_basic() -> None:
box_print("Run 'coverage help' to see available coverage commands.")


def _coverage_instructions_pytest() -> None:
box_print("Run 'pytest --cov' to run your tests with coverage.")


def use_deptry(*, remove: bool = False) -> None:
tool = DeptryTool()

Expand All @@ -53,6 +81,7 @@ def use_deptry(*, remove: bool = False) -> None:

def use_pre_commit(*, remove: bool = False) -> None:
tool = PreCommitTool()
pyproject_fmt_tool = PyprojectFmtTool()

ensure_pyproject_toml()

Expand All @@ -62,13 +91,14 @@ def use_pre_commit(*, remove: bool = False) -> None:
if _tool.is_used():
_tool.add_pre_commit_repo_configs()

if PyprojectFmtTool().is_used():
if pyproject_fmt_tool.is_used():
# We will use pre-commit instead of the dev-dep.
remove_deps_from_group(PyprojectFmtTool().get_unique_dev_deps(), "dev")
use_pyproject_fmt()
remove_deps_from_group(pyproject_fmt_tool.get_unique_dev_deps(), "dev")
pyproject_fmt_tool.add_pyproject_configs()
_pyproject_fmt_instructions_pre_commit()

if RequirementsTxtTool().is_used():
use_requirements_txt()
_requirements_txt_instructions_pre_commit()

if not get_hook_names():
add_placeholder_hook()
Expand All @@ -91,12 +121,15 @@ def use_pre_commit(*, remove: bool = False) -> None:
remove_deps_from_group(tool.dev_deps, "dev")

# Need to add a new way of running some hooks manually if they are not dev
# dependencies yet
if PyprojectFmtTool().is_used():
use_pyproject_fmt()
# dependencies yet - explain to the user.
if pyproject_fmt_tool.is_used():
add_deps_to_group(pyproject_fmt_tool.dev_deps, "dev")
_pyproject_fmt_instructions_basic()

# Likewise, explain how to manually generate the requirements.txt file, since
# they're not going to do it via pre-commit anymore.
if RequirementsTxtTool().is_used():
use_requirements_txt()
_requirements_txt_instructions_basic()


def use_pyproject_fmt(*, remove: bool = False) -> None:
Expand All @@ -115,18 +148,24 @@ def use_pyproject_fmt(*, remove: bool = False) -> None:
tool.add_pyproject_configs()

if not is_pre_commit:
box_print("Run 'pyproject-fmt pyproject.toml' to run pyproject-fmt.")
_pyproject_fmt_instructions_basic()
else:
box_print(
"Run 'pre-commit run pyproject-fmt --all-files' to run pyproject-fmt."
)
_pyproject_fmt_instructions_pre_commit()
else:
tool.remove_pyproject_configs()
if PreCommitTool().is_used():
tool.remove_pre_commit_repo_configs()
remove_deps_from_group(tool.dev_deps, "dev")


def _pyproject_fmt_instructions_basic() -> None:
box_print("Run 'pyproject-fmt pyproject.toml' to run pyproject-fmt.")


def _pyproject_fmt_instructions_pre_commit() -> None:
box_print("Run 'pre-commit run pyproject-fmt --all-files' to run pyproject-fmt.")


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

Expand All @@ -137,6 +176,7 @@ def use_pytest(*, remove: bool = False) -> None:
tool.add_pyproject_configs()
if RuffTool().is_used():
select_ruff_rules(tool.get_associated_ruff_rules())

# deptry currently can't scan the tests folder for dev deps
# https://github.com/fpgmaas/deptry/issues/302
add_pytest_dir()
Expand All @@ -149,6 +189,9 @@ def use_pytest(*, remove: bool = False) -> None:
)
box_print("Add test functions with the format 'test_*()'.")
box_print("Run 'pytest' to run the tests.")

if CoverageTool().is_used():
_coverage_instructions_pytest()
else:
if is_bitbucket_used():
remove_bitbucket_pytest_steps()
Expand All @@ -159,6 +202,9 @@ def use_pytest(*, remove: bool = False) -> None:
remove_deps_from_group(tool.dev_deps, "test")
remove_pytest_dir() # Last, since this is a manual step

if CoverageTool().is_used():
_coverage_instructions_basic()


def use_requirements_txt(*, remove: bool = False) -> None:
tool = RequirementsTxtTool()
Expand Down Expand Up @@ -190,11 +236,9 @@ def use_requirements_txt(*, remove: bool = False) -> None:
)

if not is_pre_commit:
box_print(
"Run 'uv export --no-dev --output-file=requirements.txt' to write 'requirements.txt'."
)
_requirements_txt_instructions_basic()
else:
box_print("Run the 'pre-commit run uv-export' to write 'requirements.txt'.")
_requirements_txt_instructions_pre_commit()
else:
if PreCommitTool().is_used():
tool.remove_pre_commit_repo_configs()
Expand All @@ -204,6 +248,16 @@ def use_requirements_txt(*, remove: bool = False) -> None:
path.unlink()


def _requirements_txt_instructions_basic() -> None:
box_print(
"Run 'uv export --no-dev --output-file=requirements.txt' to write 'requirements.txt'."
)


def _requirements_txt_instructions_pre_commit() -> None:
box_print("Run the 'pre-commit run uv-export' to write 'requirements.txt'.")


def use_ruff(*, remove: bool = False) -> None:
tool = RuffTool()

Expand Down
9 changes: 8 additions & 1 deletion src/usethis/_integrations/pyproject/io.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import tomllib
from functools import cache
from pathlib import Path
from typing import Any

Expand All @@ -11,8 +12,13 @@


def read_pyproject_toml() -> tomlkit.TOMLDocument:
return read_pyproject_toml_from_path(Path.cwd() / "pyproject.toml")


@cache
def read_pyproject_toml_from_path(path: Path) -> tomlkit.TOMLDocument:
try:
return tomlkit.parse((Path.cwd() / "pyproject.toml").read_text())
return tomlkit.parse(path.read_text())
except FileNotFoundError:
msg = "'pyproject.toml' not found in the current directory."
raise PyProjectTOMLNotFoundError(msg)
Expand All @@ -33,4 +39,5 @@ def read_pyproject_dict() -> dict[str, Any]:


def write_pyproject_toml(toml_document: tomlkit.TOMLDocument) -> None:
read_pyproject_toml_from_path.cache_clear()
(Path.cwd() / "pyproject.toml").write_text(tomlkit.dumps(toml_document))
2 changes: 2 additions & 0 deletions src/usethis/_integrations/uv/call.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from usethis._integrations.pyproject.io import read_pyproject_toml_from_path
from usethis._integrations.uv.errors import UVSubprocessFailedError
from usethis._subprocess import SubprocessFailedError, call_subprocess

Expand All @@ -8,6 +9,7 @@ def call_uv_subprocess(args: list[str]) -> str:
Raises:
UVSubprocessFailedError: If the subprocess fails.
"""
read_pyproject_toml_from_path.cache_clear()
try:
return call_subprocess(["uv", *args])
except SubprocessFailedError as err:
Expand Down
9 changes: 9 additions & 0 deletions src/usethis/_interface/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from usethis._config import offline_opt, quiet_opt, usethis_config
from usethis._console import err_print
from usethis._core.tool import (
use_coverage,
use_deptry,
use_pre_commit,
use_pyproject_fmt,
Expand All @@ -22,6 +23,14 @@
)


@app.command(help="Use the coverage code coverage measurement tool.")
def coverage(
remove: bool = remove_opt, offline: bool = offline_opt, quiet: bool = quiet_opt
) -> None:
with usethis_config.set(offline=offline, quiet=quiet):
_run_tool(use_coverage, remove=remove)


@app.command(
help="Use the deptry linter: avoid missing or superfluous dependency declarations."
)
Expand Down
63 changes: 42 additions & 21 deletions src/usethis/_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,46 @@ def remove_pyproject_configs(self) -> None:
first_removal = False


class CoverageTool(Tool):
@property
def name(self) -> str:
return "coverage"

@property
def dev_deps(self) -> list[str]:
return ["coverage[toml]"]

def get_pyproject_configs(self) -> list[PyProjectConfig]:
return [
PyProjectConfig(
id_keys=["tool", "coverage", "run"],
value={
"source": ["src"],
"omit": ["*/pytest-of-*/*"],
},
),
PyProjectConfig(
id_keys=["tool", "coverage", "report"],
value={
"exclude_also": [
"if TYPE_CHECKING:",
"raise AssertionError",
"raise NotImplementedError",
"assert_never(.*)",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
},
),
]

def get_pyproject_id_keys(self):
return [["tool", "coverage"]]

def get_managed_files(self):
return [Path(".coveragerc")]


class DeptryTool(Tool):
@property
def name(self) -> str:
Expand Down Expand Up @@ -238,7 +278,7 @@ def name(self) -> str:

@property
def dev_deps(self) -> list[str]:
return ["pytest", "pytest-cov", "coverage[toml]"]
return ["pytest", "pytest-cov"]

def get_pyproject_configs(self) -> list[PyProjectConfig]:
return [
Expand All @@ -254,26 +294,6 @@ def get_pyproject_configs(self) -> list[PyProjectConfig]:
}
},
),
PyProjectConfig(
id_keys=["tool", "coverage", "run"],
value={
"source": ["src"],
"omit": ["*/pytest-of-*/*"],
},
),
PyProjectConfig(
id_keys=["tool", "coverage", "report"],
value={
"exclude_also": [
"if TYPE_CHECKING:",
"raise AssertionError",
"raise NotImplementedError",
"assert_never(.*)",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
},
),
]

def get_associated_ruff_rules(self) -> list[str]:
Expand Down Expand Up @@ -385,6 +405,7 @@ def get_managed_files(self):


ALL_TOOLS: list[Tool] = [
CoverageTool(),
DeptryTool(),
PreCommitTool(),
PyprojectFmtTool(),
Expand Down
Loading