Skip to content

Commit 0141951

Browse files
committed
feat: add some pytests for the template itself to see if this is a helpful pattern or not
1 parent 57b0ed1 commit 0141951

File tree

7 files changed

+120
-39
lines changed

7 files changed

+120
-39
lines changed

noxfile.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
# noxfile.py
2-
# Nox configuration for the cookiecutter-robust-python TEMPLATE development and maintenance.
3-
# See https://nox.thea.codes/en/stable/config.html
4-
1+
"""Noxfile for the cookiecutter-robust-python template."""
52
from pathlib import Path
63
import shutil
74
import tempfile
8-
import sys
95

106
import nox
117
import platformdirs
8+
from nox.command import CommandFailed
129
from nox.sessions import Session
1310

11+
1412
nox.options.default_venv_backend = "uv"
1513

1614
DEFAULT_TEMPLATE_PYTHON_VERSION = "3.9"
@@ -197,7 +195,6 @@ def test(session: Session) -> None:
197195
generated_project_dir = temp_dir / "test_project" # Use the slug defined in --extra-context
198196
if not generated_project_dir.exists():
199197
session.error(f"Generated project directory not found: {generated_project_dir}")
200-
return
201198

202199
session.log(f"Changing to generated project directory: {generated_project_dir}")
203200
session.cd(generated_project_dir)
@@ -223,10 +220,9 @@ def release_template(session: Session):
223220
session.log("Running release process for the TEMPLATE using Commitizen...")
224221
try:
225222
session.run("git", "version", success_codes=[0], external=True, silent=True)
226-
except nox.command.CommandFailed:
223+
except CommandFailed:
227224
session.log("Git command not found. Commitizen requires Git.")
228225
session.skip("Git not available.")
229-
return
230226

231227
session.log("Checking Commitizen availability via uvx.")
232228
session.run("uvx", "cz", "--version", successcodes=[0], external=True)

tests/conftest.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
"""Fixtures used in all tests for cookiecutter-robust-python."""
2-
import json
2+
33
import os
4+
import subprocess
5+
46
from pathlib import Path
5-
from typing import Any, Generator
7+
from typing import Generator
68

7-
import platformdirs
89
import pytest
9-
from _pytest.fixtures import FixtureRequest
1010
from _pytest.tmpdir import TempPathFactory
1111
from cookiecutter.main import cookiecutter
1212

13+
from tests.constants import REPO_FOLDER
1314

14-
pytest_plugins: list[str] = ["pytester"]
15-
16-
17-
REPO_FOLDER: Path = Path(__file__).parent.parent
18-
COOKIECUTTER_FOLDER: Path = REPO_FOLDER / "{{cookiecutter.project_name}}"
19-
HOOKS_FOLDER: Path = REPO_FOLDER / "hooks"
20-
SCRIPTS_FOLDER: Path = REPO_FOLDER / "scripts"
2115

22-
COOKIECUTTER_JSON_PATH: Path = COOKIECUTTER_FOLDER / "cookiecutter.json"
23-
COOKIECUTTER_JSON: dict[str, Any] = json.loads(COOKIECUTTER_JSON_PATH.read_text())
16+
pytest_plugins: list[str] = ["pytester"]
2417

2518

2619
@pytest.fixture(scope="session")
@@ -37,7 +30,9 @@ def robust_python_demo_path(tmp_path_factory: TempPathFactory) -> Path:
3730
"add_rust_extension": False
3831
}
3932
)
40-
return demos_path / "robust-python-demo"
33+
path: Path = demos_path / "robust-python-demo"
34+
subprocess.run(["uv", "lock"], cwd=path)
35+
return path
4136

4237

4338
@pytest.fixture(scope="session")
@@ -54,7 +49,9 @@ def robust_maturin_demo_path(tmp_path_factory: TempPathFactory) -> Path:
5449
"add_rust_extension": True
5550
}
5651
)
57-
return demos_path / "robust-maturin-demo"
52+
path: Path = demos_path / "robust-maturin-demo"
53+
subprocess.run(["uv", "sync"], cwd=path)
54+
return path
5855

5956

6057
@pytest.fixture(scope="function")
@@ -66,3 +63,9 @@ def inside_robust_python_demo(robust_python_demo_path: Path) -> Generator[Path,
6663
os.chdir(original_path)
6764

6865

66+
@pytest.fixture(scope="function")
67+
def inside_robust_maturin_demo(robust_maturin_demo_path: Path) -> Generator[Path, None, None]:
68+
original_path: Path = Path.cwd()
69+
os.chdir(robust_maturin_demo_path)
70+
yield robust_maturin_demo_path
71+
os.chdir(original_path)

tests/constants.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Module containing constants used throughout all tests."""
2+
import json
3+
from pathlib import Path
4+
from typing import Any
5+
6+
REPO_FOLDER: Path = Path(__file__).parent.parent
7+
COOKIECUTTER_FOLDER: Path = REPO_FOLDER / "{{cookiecutter.project_name}}"
8+
HOOKS_FOLDER: Path = REPO_FOLDER / "hooks"
9+
SCRIPTS_FOLDER: Path = REPO_FOLDER / "scripts"
10+
11+
COOKIECUTTER_JSON_PATH: Path = REPO_FOLDER / "cookiecutter.json"
12+
COOKIECUTTER_JSON: dict[str, Any] = json.loads(COOKIECUTTER_JSON_PATH.read_text())
13+
14+
MIN_PYTHON_SLUG: int = int(COOKIECUTTER_JSON["min_python_version"].lstrip("3."))
15+
MAX_PYTHON_SLUG: int = int(COOKIECUTTER_JSON["max_python_version"].lstrip("3."))
16+
PYTHON_VERSIONS: list[str] = [f"3.{VERSION_SLUG}" for VERSION_SLUG in range(MIN_PYTHON_SLUG, MAX_PYTHON_SLUG + 1)]
17+
DEFAULT_PYTHON_VERSION: str = PYTHON_VERSIONS[1]
18+
19+
20+
TYPE_CHECK_NOX_SESSIONS: list[str] = [f"typecheck-{python_version}" for python_version in PYTHON_VERSIONS]
21+
TESTS_NOX_SESSIONS: list[str] = [f"tests-{python_version}" for python_version in PYTHON_VERSIONS]
22+
CHECK_NOX_SESSIONS: list[str] = [f"check-{python_version}" for python_version in PYTHON_VERSIONS]
23+
FULL_CHECK_NOX_SESSIONS: list[str] = [f"full-check-{python_version}" for python_version in PYTHON_VERSIONS]
24+
25+
26+
GLOBAL_NOX_SESSIONS: list[str] = [
27+
"pre-commit",
28+
"format-python",
29+
"lint-python",
30+
*TYPE_CHECK_NOX_SESSIONS,
31+
"docs-build",
32+
"build-python",
33+
"build-container",
34+
"publish-python",
35+
"release",
36+
"tox",
37+
*CHECK_NOX_SESSIONS,
38+
*FULL_CHECK_NOX_SESSIONS,
39+
"coverage"
40+
]
41+
42+
RUST_NOX_SESSIONS: list[str] = [
43+
"format-rust",
44+
"lint-rust",
45+
"tests-rust",
46+
"publish-rust"
47+
]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Tests project generation and template functionality using a Python build backend."""
2+
import subprocess
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from tests.constants import GLOBAL_NOX_SESSIONS
8+
9+
10+
def test_demo_project_generation(robust_python_demo_path: Path) -> None:
11+
assert robust_python_demo_path.exists()
12+
13+
14+
@pytest.mark.parametrize("session", GLOBAL_NOX_SESSIONS)
15+
def test_demo_project_noxfile(robust_python_demo_path: Path, session: str) -> None:
16+
command: list[str] = ["uvx", "nox", "-s", session]
17+
result: subprocess.CompletedProcess = subprocess.run(
18+
command,
19+
cwd=robust_python_demo_path,
20+
capture_output=True,
21+
text=True,
22+
timeout=10.0,
23+
)
24+
print(result.stdout)
25+
print(result.stderr)
26+
result.check_returncode()
27+
28+
29+

{{cookiecutter.project_name}}/noxfile.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"""Noxfile for the {{cookiecutter.project_name}} project."""
2-
32
from pathlib import Path
43
from typing import List
54

65
import nox
76
from nox.command import CommandFailed
87
from nox.sessions import Session
98

9+
1010
nox.options.default_venv_backend = "uv"
1111

1212
# Logic that helps avoid metaprogramming in cookiecutter-robust-python
@@ -86,12 +86,11 @@ def tests_python(session: Session) -> None:
8686
junitxml_file = test_results_dir / f"test-results-py{session.python}.xml"
8787

8888
session.run(
89-
"uv", "run", "pytest",
89+
"pytest",
9090
"--cov={}".format(PACKAGE_NAME),
9191
"--cov-report=xml",
9292
f"--junitxml={junitxml_file}",
93-
"tests/",
94-
external=True
93+
"tests/"
9594
)
9695

9796

@@ -151,15 +150,15 @@ def build_container(session: Session) -> None:
151150
try:
152151
session.run("docker", "info", success_codes=[0], external=True, silent=True)
153152
container_cli = "docker"
154-
except nox.command.CommandFailed:
153+
except CommandFailed:
155154
try:
156155
session.run("podman", "info", success_codes=[0], external=True, silent=True)
157156
container_cli = "podman"
158-
except nox.command.CommandFailed:
157+
except CommandFailed:
159158
session.log("Neither Docker nor Podman command found. Please install a container runtime.")
160159
session.skip("Container runtime not available.")
161160

162-
current_dir = Path(".")
161+
current_dir: Path = Path.cwd()
163162
session.log(f"Ensuring core dependencies are synced in {current_dir.resolve()} for build context...")
164163
session.run("uv", "sync", "--locked", external=True)
165164

@@ -177,6 +176,8 @@ def publish_python(session: Session) -> None:
177176
Requires packages to be built first (`nox -s build-python` or `nox -s build`).
178177
Requires TWINE_USERNAME/TWINE_PASSWORD or TWINE_API_KEY environment variables set (usually in CI).
179178
"""
179+
session.run("uv", "sync", "--locked", "--group", "dev", external=True)
180+
180181
session.log("Checking built packages with Twine.")
181182
session.run("uvx", "twine", "check", "dist/*", external=True)
182183

@@ -201,9 +202,11 @@ def release(session: Session) -> None:
201202
Optionally accepts increment (major, minor, patch) after '--'.
202203
"""
203204
session.log("Running release process using Commitizen...")
205+
session.run("uv", "sync", "--locked", "--group", "dev", external=True)
206+
204207
try:
205208
session.run("git", "version", success_codes=[0], external=True, silent=True)
206-
except nox.command.CommandFailed:
209+
except CommandFailed:
207210
session.log("Git command not found. Commitizen requires Git.")
208211
session.skip("Git not available.")
209212

@@ -243,6 +246,8 @@ def tox(session: Session) -> None:
243246
Accepts tox args after '--' (e.g., `nox -s tox -- -e py39`).
244247
"""
245248
session.log("Running Tox test matrix via uvx...")
249+
session.run("uv", "sync", "--locked", "--group", "dev", external=True)
250+
246251
tox_ini_path = Path("tox.ini")
247252
if not tox_ini_path.exists():
248253
session.log("tox.ini file not found at %s. Tox requires this file.", str(tox_ini_path))
@@ -309,13 +314,13 @@ def coverage(session: Session) -> None:
309314
session.log("Installing dependencies for coverage report session...")
310315
session.run("uv", "sync", "--locked", "--group", "dev", "--group", "test", external=True)
311316

312-
coverage_combined_file = Path(".") / ".coverage"
317+
coverage_combined_file: Path = Path.cwd() / ".coverage"
313318

314319
session.log("Combining coverage data.")
315320
try:
316321
session.run("uv", "run", "coverage", "combine", external=True)
317322
session.log(f"Combined coverage data into {coverage_combined_file.resolve()}")
318-
except nox.command.CommandFailed as e:
323+
except CommandFailed as e:
319324
if e.returncode == 1:
320325
session.log("No coverage data found to combine. Run tests first with coverage enabled.")
321326
else:
@@ -329,4 +334,4 @@ def coverage(session: Session) -> None:
329334
session.log("Running terminal coverage report.")
330335
session.run("uv", "run", "coverage", "report", external=True)
331336

332-
session.log(f"Coverage reports generated in ./{str(coverage_html_dir)} and terminal.")
337+
session.log(f"Coverage reports generated in ./{coverage_html_dir} and terminal.")

{{cookiecutter.project_name}}/src/{{cookiecutter.package_name}}/__main__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import typer
44

55

6-
@typer.command()
7-
@typer.version_option()
6+
app: typer.Typer = typer.Typer()
7+
8+
@app.command(name="{{cookiecutter.project_name}}")
89
def main() -> None:
910
"""{{cookiecutter.friendly_name}}."""
1011

1112

1213
if __name__ == "__main__":
13-
main(prog_name="{{cookiecutter.project_name}}") # pragma: no cover
14+
app()

{{cookiecutter.project_name}}/tests/unit_tests/test_main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Test cases for the __main__ module."""
22

33
import pytest
4-
from click.testing import CliRunner
4+
from typer.testing import CliRunner
55

66
from {{cookiecutter.package_name}} import __main__
77

@@ -14,5 +14,5 @@ def runner() -> CliRunner:
1414

1515
def test_main_succeeds(runner: CliRunner) -> None:
1616
"""It exits with a status code of zero."""
17-
result = runner.invoke(__main__.main)
17+
result = runner.invoke(__main__.app)
1818
assert result.exit_code == 0

0 commit comments

Comments
 (0)