Skip to content

Commit f34b8be

Browse files
authored
Merge pull request #8 from 56kyle/develop
Develop
2 parents aee66ad + 0141951 commit f34b8be

File tree

11 files changed

+205
-24
lines changed

11 files changed

+205
-24
lines changed

noxfile.py

Lines changed: 31 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"
@@ -45,6 +43,8 @@
4543

4644
TEMPLATE_PYTHON_LOCATIONS: tuple[Path, ...] = (
4745
Path("noxfile.py"),
46+
Path("scripts/*"),
47+
Path("hooks/*")
4848
)
4949

5050
TEMPLATE_CONFIG_AND_DOCS: tuple[Path, ...] = (
@@ -105,6 +105,31 @@ def uv_in_demo(session: Session) -> None:
105105
)
106106

107107

108+
@nox.session(name="in-demo", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
109+
def in_demo(session: Session) -> None:
110+
session.install("cookiecutter", "platformdirs", "loguru", "typer")
111+
session.run(
112+
"python",
113+
"scripts/generate-demo-project.py",
114+
*GENERATE_DEMO_PROJECT_OPTIONS,
115+
)
116+
original_dir: Path = Path.cwd()
117+
session.cd(DEMO_ROOT_FOLDER)
118+
session.run(*session.posargs)
119+
session.cd(original_dir)
120+
121+
122+
@nox.session(name="clear-cache", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
123+
def clear_cache(session: Session) -> None:
124+
"""Clear the cache of generated project demos.
125+
126+
Not commonly used, but sometimes permissions might get messed up if exiting mid-build and such.
127+
"""
128+
session.log("Clearing cache of generated project demos...")
129+
shutil.rmtree(PROJECT_DEMOS_FOLDER, ignore_errors=True)
130+
session.log("Cache cleared.")
131+
132+
108133
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION)
109134
def lint(session: Session):
110135
"""Lint the template's own Python files and configurations."""
@@ -170,7 +195,6 @@ def test(session: Session) -> None:
170195
generated_project_dir = temp_dir / "test_project" # Use the slug defined in --extra-context
171196
if not generated_project_dir.exists():
172197
session.error(f"Generated project directory not found: {generated_project_dir}")
173-
return
174198

175199
session.log(f"Changing to generated project directory: {generated_project_dir}")
176200
session.cd(generated_project_dir)
@@ -196,10 +220,9 @@ def release_template(session: Session):
196220
session.log("Running release process for the TEMPLATE using Commitizen...")
197221
try:
198222
session.run("git", "version", success_codes=[0], external=True, silent=True)
199-
except nox.command.CommandFailed:
223+
except CommandFailed:
200224
session.log("Git command not found. Commitizen requires Git.")
201225
session.skip("Git not available.")
202-
return
203226

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

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for cookiecutter-robust-python."""

tests/conftest.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Fixtures used in all tests for cookiecutter-robust-python."""
2+
3+
import os
4+
import subprocess
5+
6+
from pathlib import Path
7+
from typing import Generator
8+
9+
import pytest
10+
from _pytest.tmpdir import TempPathFactory
11+
from cookiecutter.main import cookiecutter
12+
13+
from tests.constants import REPO_FOLDER
14+
15+
16+
pytest_plugins: list[str] = ["pytester"]
17+
18+
19+
@pytest.fixture(scope="session")
20+
def robust_python_demo_path(tmp_path_factory: TempPathFactory) -> Path:
21+
"""Creates a temporary example python project for testing against and returns its Path."""
22+
demos_path: Path = tmp_path_factory.mktemp("demos")
23+
cookiecutter(
24+
str(REPO_FOLDER),
25+
no_input=True,
26+
overwrite_if_exists=True,
27+
output_dir=demos_path,
28+
extra_context={
29+
"project_name": "robust-python-demo",
30+
"add_rust_extension": False
31+
}
32+
)
33+
path: Path = demos_path / "robust-python-demo"
34+
subprocess.run(["uv", "lock"], cwd=path)
35+
return path
36+
37+
38+
@pytest.fixture(scope="session")
39+
def robust_maturin_demo_path(tmp_path_factory: TempPathFactory) -> Path:
40+
"""Creates a temporary example maturin project for testing against and returns its Path."""
41+
demos_path: Path = tmp_path_factory.mktemp("demos")
42+
cookiecutter(
43+
str(REPO_FOLDER),
44+
no_input=True,
45+
overwrite_if_exists=True,
46+
output_dir=demos_path,
47+
extra_context={
48+
"project_name": "robust-maturin-demo",
49+
"add_rust_extension": True
50+
}
51+
)
52+
path: Path = demos_path / "robust-maturin-demo"
53+
subprocess.run(["uv", "sync"], cwd=path)
54+
return path
55+
56+
57+
@pytest.fixture(scope="function")
58+
def inside_robust_python_demo(robust_python_demo_path: Path) -> Generator[Path, None, None]:
59+
"""Changes the current working directory to the robust-python-demo project."""
60+
original_path: Path = Path.cwd()
61+
os.chdir(robust_python_demo_path)
62+
yield robust_python_demo_path
63+
os.chdir(original_path)
64+
65+
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""Integration tests for cookiecutter-robust-python."""
2+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Fixtures used in integration tests for cookiecutter-robust-python."""
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}}/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ classifiers = [
2222
dependencies = [
2323
"loguru>=0.7.3",
2424
"platformdirs>=4.3.8",
25+
"typer>=0.15.4"
2526
]
2627

2728
[dependency-groups]

{{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()

0 commit comments

Comments
 (0)