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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,8 @@ ALWAYS check whether an existing function already covers your use case before im
- `get_importable_packages()` (`usethis._integrations.project.packages`) — Get the names of packages in the source directory that can be imported.
- `fancy_model_dump()` (`usethis._integrations.pydantic.dump`) — Like `pydantic.model_dump` but with bespoke formatting options.
- `add_pytest_dir()` (`usethis._integrations.pytest.core`) — Create the tests directory and conftest.py if they do not already exist.
- `remove_pytest_dir()` (`usethis._integrations.pytest.core`) — Remove the tests directory if it contains only the managed conftest.py.
- `add_example_test()` (`usethis._integrations.pytest.core`) — Create an example test file in the tests directory if it does not already exist.
- `remove_pytest_dir()` (`usethis._integrations.pytest.core`) — Remove the tests directory if it contains only managed files.
- `get_readme_path()` (`usethis._integrations.readme.path`) — Return the path to the README file, searching for common README filenames.
- `get_markdown_readme_path()` (`usethis._integrations.readme.path`) — Return the path to the Markdown README file, raising an error if it is not Markdown.
- `get_sonar_project_properties()` (`usethis._integrations.sonarqube.config`) — Get contents for (or from) the sonar-project.properties file.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ $ uvx usethis tool pytest
✔ Adding pytest config to 'pyproject.toml'.
✔ Creating '/tests'.
✔ Writing '/tests/conftest.py'.
✔ Writing '/tests/test_example.py'.
✔ Selecting Ruff rule 'PT' in 'pyproject.toml'.
☐ Add test files to the '/tests' directory with the format 'test_*.py'.
☐ Add test functions with the format 'test_*()'.
Expand Down
3 changes: 2 additions & 1 deletion docs/functions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@
- `get_importable_packages()` (`usethis._integrations.project.packages`) — Get the names of packages in the source directory that can be imported.
- `fancy_model_dump()` (`usethis._integrations.pydantic.dump`) — Like `pydantic.model_dump` but with bespoke formatting options.
- `add_pytest_dir()` (`usethis._integrations.pytest.core`) — Create the tests directory and conftest.py if they do not already exist.
- `remove_pytest_dir()` (`usethis._integrations.pytest.core`) — Remove the tests directory if it contains only the managed conftest.py.
- `add_example_test()` (`usethis._integrations.pytest.core`) — Create an example test file in the tests directory if it does not already exist.
- `remove_pytest_dir()` (`usethis._integrations.pytest.core`) — Remove the tests directory if it contains only managed files.
- `get_readme_path()` (`usethis._integrations.readme.path`) — Return the path to the README file, searching for common README filenames.
- `get_markdown_readme_path()` (`usethis._integrations.readme.path`) — Return the path to the Markdown README file, raising an error if it is not Markdown.
- `get_sonar_project_properties()` (`usethis._integrations.sonarqube.config`) — Get contents for (or from) the sonar-project.properties file.
Expand Down
1 change: 1 addition & 0 deletions docs/start/example-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ $ uvx usethis tool pytest
✔ Adding pytest config to 'pyproject.toml'.
✔ Creating '/tests'.
✔ Writing '/tests/conftest.py'.
✔ Writing '/tests/test_example.py'.
✔ Selecting Ruff rule 'PT' in 'pyproject.toml'.
☐ Add test files to the '/tests' directory with the format 'test_*.py'.
☐ Add test functions with the format 'test_*()'.
Expand Down
20 changes: 17 additions & 3 deletions src/usethis/_core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
)
from usethis._integrations.pre_commit.errors import PreCommitInstallationError
from usethis._integrations.pre_commit.hooks import add_placeholder_hook, get_hook_ids
from usethis._integrations.pytest.core import add_pytest_dir, remove_pytest_dir
from usethis._integrations.pytest.core import (
add_example_test,
add_pytest_dir,
remove_pytest_dir,
)
from usethis._tool.all_ import ALL_TOOLS
from usethis._tool.impl.base.codespell import CodespellTool
from usethis._tool.impl.base.coverage_py import CoveragePyTool
Expand Down Expand Up @@ -270,8 +274,16 @@ def use_pyproject_toml(*, remove: bool = False, how: bool = False) -> None:
tool.remove_managed_files()


def use_pytest(*, remove: bool = False, how: bool = False) -> None:
"""Add and configure the pytest testing framework."""
def use_pytest(
*, remove: bool = False, how: bool = False, example: bool = True
) -> None:
"""Add and configure the pytest testing framework.

Args:
remove: If True, remove pytest instead of adding it.
how: If True, print how to use pytest instead of adding/removing it.
example: If True, create an example test file in the tests directory.
"""
tool = PytestTool()

if how:
Expand All @@ -287,6 +299,8 @@ def use_pytest(*, remove: bool = False, how: bool = False) -> None:
# deptry currently can't scan the tests folder for dev deps
# https://github.com/fpgmaas/deptry/issues/302
add_pytest_dir()
if example:
add_example_test()

rule_config = tool.rule_config

Expand Down
25 changes: 22 additions & 3 deletions src/usethis/_integrations/pytest/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
from usethis._config import usethis_config
from usethis._console import instruct_print, tick_print

_EXAMPLE_TEST_CONTENT = '''\
def test_add():
"""An example test - replace with your own tests!"""
assert 1 + 1 == 2
'''


def add_pytest_dir() -> None:
"""Create the tests directory and conftest.py if they do not already exist."""
Expand All @@ -26,16 +32,29 @@ def add_pytest_dir() -> None:
)


def add_example_test() -> None:
"""Create an example test file in the tests directory if it does not already exist."""
tests_dir = usethis_config.cpd() / "tests"

if (tests_dir / "test_example.py").exists():
# Early exit; example test already exists
return

tick_print("Writing '/tests/test_example.py'.")
(tests_dir / "test_example.py").write_text(_EXAMPLE_TEST_CONTENT, encoding="utf-8")


def remove_pytest_dir() -> None:
"""Remove the tests directory if it contains only the managed conftest.py."""
"""Remove the tests directory if it contains only managed files."""
tests_dir = usethis_config.cpd() / "tests"

if not tests_dir.exists():
# Early exit; tests directory does not exist
return

if set(tests_dir.iterdir()) <= {tests_dir / "conftest.py"}:
# The only file in the directory is conftest.py
managed_files = {tests_dir / "conftest.py", tests_dir / "test_example.py"}
if set(tests_dir.iterdir()) <= managed_files:
# The only files in the directory are managed files
tick_print("Removing '/tests'.")
shutil.rmtree(tests_dir)
else:
Expand Down
4 changes: 2 additions & 2 deletions src/usethis/_toolset/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from usethis._core.tool import use_coverage_py, use_pytest


def use_test_frameworks(remove: bool = False, how: bool = False):
def use_test_frameworks(remove: bool = False, how: bool = False, example: bool = True):
"""Add and configure testing framework tools for the project."""
use_pytest(remove=remove, how=how)
use_pytest(remove=remove, how=how, example=example)
use_coverage_py(remove=remove, how=how)
4 changes: 3 additions & 1 deletion src/usethis/_ui/interface/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from usethis._types.backend import BackendEnum
from usethis._ui.options import (
backend_opt,
example_opt,
frozen_opt,
how_opt,
offline_opt,
Expand All @@ -21,6 +22,7 @@ def test(
quiet: bool = quiet_opt,
frozen: bool = frozen_opt,
backend: BackendEnum = backend_opt,
example: bool = example_opt,
) -> None:
"""Add a recommended testing framework to the project."""
from usethis._config_file import files_manager
Expand All @@ -35,7 +37,7 @@ def test(
files_manager(),
):
try:
use_test_frameworks(remove=remove, how=how)
use_test_frameworks(remove=remove, how=how, example=example)
except UsethisError as err:
err_print(err)
raise typer.Exit(code=1) from None
4 changes: 3 additions & 1 deletion src/usethis/_ui/interface/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from usethis._types.backend import BackendEnum
from usethis._ui.options import (
backend_opt,
example_opt,
formatter_opt,
frozen_opt,
how_opt,
Expand Down Expand Up @@ -292,6 +293,7 @@ def pytest(
frozen: bool = frozen_opt,
backend: BackendEnum = backend_opt,
no_hook: bool = no_hook_opt,
example: bool = example_opt,
) -> None:
"""Use the pytest testing framework."""
from usethis._config_file import files_manager
Expand All @@ -307,7 +309,7 @@ def pytest(
),
files_manager(),
):
_run_tool(use_pytest, remove=remove, how=how)
_run_tool(use_pytest, remove=remove, how=how, example=example)


@app.command(
Expand Down
7 changes: 7 additions & 0 deletions src/usethis/_ui/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,10 @@
"--formatter/--no-formatter",
help="Add or remove specifically the Ruff formatter.",
)

# pytest command options
example_opt = typer.Option(
True,
"--example/--no-example",
help="Create an example test file in the tests directory.",
)
24 changes: 24 additions & 0 deletions tests/usethis/_core/test_core_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2492,6 +2492,7 @@ def test_no_pyproject_toml(
"✔ Adding pytest config to 'pyproject.toml'.\n"
"✔ Creating '/tests'.\n"
"✔ Writing '/tests/conftest.py'.\n"
"✔ Writing '/tests/test_example.py'.\n"
"☐ Add test files to the '/tests' directory with the format 'test_*.py'.\n"
"☐ Add test functions with the format 'test_*()'.\n"
"☐ Run 'uv run pytest' to run the tests.\n"
Expand All @@ -2510,6 +2511,28 @@ def test_no_pyproject_toml(
minversion = "7\""""
)

@pytest.mark.usefixtures("_vary_network_conn")
def test_example_file_exists(self, uv_init_dir: Path):
with change_cwd(uv_init_dir), files_manager():
# Act
use_pytest()

# Assert
assert (uv_init_dir / "tests" / "test_example.py").exists()
content = (uv_init_dir / "tests" / "test_example.py").read_text()
assert "def test_add():" in content
assert "assert 1 + 1 == 2" in content

@pytest.mark.usefixtures("_vary_network_conn")
def test_no_example(self, uv_init_dir: Path):
with change_cwd(uv_init_dir), files_manager():
# Act
use_pytest(example=False)

# Assert
assert not (uv_init_dir / "tests" / "test_example.py").exists()
assert (uv_init_dir / "tests" / "conftest.py").exists()

@pytest.mark.usefixtures("_vary_network_conn")
def test_coverage_integration(
self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]
Expand All @@ -2530,6 +2553,7 @@ def test_coverage_integration(
"✔ Adding pytest config to 'pyproject.toml'.\n"
"✔ Creating '/tests'.\n"
"✔ Writing '/tests/conftest.py'.\n"
"✔ Writing '/tests/test_example.py'.\n"
"☐ Add test files to the '/tests' directory with the format 'test_*.py'.\n"
"☐ Add test functions with the format 'test_*()'.\n"
"☐ Run 'uv run pytest' to run the tests.\n"
Expand Down
101 changes: 100 additions & 1 deletion tests/usethis/_integrations/pytest/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import pytest

from usethis._integrations.pytest.core import add_pytest_dir, remove_pytest_dir
from usethis._integrations.pytest.core import (
add_example_test,
add_pytest_dir,
remove_pytest_dir,
)
from usethis._test import change_cwd


Expand Down Expand Up @@ -37,6 +41,77 @@ def test_conftest_exists(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]
out, _ = capfd.readouterr()
assert out == "✔ Writing '/tests/conftest.py'.\n"

def test_conftest_already_exists(
self, tmp_path: Path, capfd: pytest.CaptureFixture[str]
):
# Arrange
(tmp_path / "tests").mkdir()
(tmp_path / "tests" / "conftest.py").write_text("existing")

# Act
with change_cwd(tmp_path):
add_pytest_dir()

# Assert
out, _ = capfd.readouterr()
assert out == ""
assert (tmp_path / "tests" / "conftest.py").read_text() == "existing"


class TestAddExampleTest:
def test_exists(self, tmp_path: Path):
# Arrange
(tmp_path / "tests").mkdir()

# Act
with change_cwd(tmp_path):
add_example_test()

# Assert
assert (tmp_path / "tests" / "test_example.py").exists()

def test_content(self, tmp_path: Path):
# Arrange
(tmp_path / "tests").mkdir()

# Act
with change_cwd(tmp_path):
add_example_test()

# Assert
content = (tmp_path / "tests" / "test_example.py").read_text()
assert "def test_add():" in content
assert "assert 1 + 1 == 2" in content
assert "An example test - replace with your own tests!" in content

def test_message(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]):
# Arrange
(tmp_path / "tests").mkdir()

# Act
with change_cwd(tmp_path):
add_example_test()

# Assert
out, _ = capfd.readouterr()
assert out == "✔ Writing '/tests/test_example.py'.\n"

def test_already_exists(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]):
# Arrange
(tmp_path / "tests").mkdir()
(tmp_path / "tests" / "test_example.py").write_text("existing content")

# Act
with change_cwd(tmp_path):
add_example_test()

# Assert
out, _ = capfd.readouterr()
assert out == ""
assert (
tmp_path / "tests" / "test_example.py"
).read_text() == "existing content"


class TestRemovePytestDir:
def test_blank_slate(self, tmp_path: Path):
Expand Down Expand Up @@ -81,3 +156,27 @@ def test_roundtrip(self, tmp_path: Path):

# Assert
assert not (tmp_path / "tests").exists()

def test_roundtrip_with_example(self, tmp_path: Path):
with change_cwd(tmp_path):
# Arrange
add_pytest_dir()
add_example_test()

# Act
remove_pytest_dir()

# Assert
assert not (tmp_path / "tests").exists()

def test_only_example_test(self, tmp_path: Path):
# Arrange
(tmp_path / "tests").mkdir()
(tmp_path / "tests" / "test_example.py").touch()

# Act
with change_cwd(tmp_path):
remove_pytest_dir()

# Assert
assert not (tmp_path / "tests").exists()
2 changes: 2 additions & 0 deletions tests/usethis/_ui/interface/test_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def test_none_backend_no_pyproject_toml(self, tmp_path: Path):
"✔ Adding pytest config to 'pytest.ini'.\n"
"✔ Creating '/tests'.\n"
"✔ Writing '/tests/conftest.py'.\n"
"✔ Writing '/tests/test_example.py'.\n"
"☐ Add test files to the '/tests' directory with the format 'test_*.py'.\n"
"☐ Add test functions with the format 'test_*()'.\n"
"☐ Run 'pytest' to run the tests.\n"
Expand All @@ -60,6 +61,7 @@ def test_none_backend_pyproject_toml(self, tmp_path: Path):
"✔ Adding pytest config to 'pyproject.toml'.\n"
"✔ Creating '/tests'.\n"
"✔ Writing '/tests/conftest.py'.\n"
"✔ Writing '/tests/test_example.py'.\n"
"☐ Add test files to the '/tests' directory with the format 'test_*.py'.\n"
"☐ Add test functions with the format 'test_*()'.\n"
"☐ Run 'pytest' to run the tests.\n"
Expand Down
Loading
Loading