Skip to content

Commit aa00508

Browse files
authored
Merge pull request #11 from 56kyle/develop
Tons of varying changes
2 parents 9223d51 + db221d5 commit aa00508

File tree

13 files changed

+457
-244
lines changed

13 files changed

+457
-244
lines changed

noxfile.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Noxfile for the cookiecutter-robust-python template."""
2-
2+
import os
33
import shutil
44
import tempfile
55
from pathlib import Path
@@ -15,6 +15,7 @@
1515
DEFAULT_TEMPLATE_PYTHON_VERSION = "3.9"
1616

1717
REPO_ROOT: Path = Path(__file__).parent.resolve()
18+
SCRIPTS_FOLDER: Path = REPO_ROOT / "scripts"
1819
TEMPLATE_FOLDER: Path = REPO_ROOT / "{{cookiecutter.project_name}}"
1920

2021

@@ -26,89 +27,78 @@
2627
)
2728
).resolve()
2829

29-
PROJECT_DEMOS_FOLDER: Path = COOKIECUTTER_ROBUST_PYTHON_CACHE_FOLDER / "project_demos"
30-
DEFAULT_DEMO_NAME: str = "demo-project"
30+
DEFAULT_PROJECT_DEMOS_FOLDER = COOKIECUTTER_ROBUST_PYTHON_CACHE_FOLDER / "project_demos"
31+
PROJECT_DEMOS_FOLDER: Path = Path(os.getenv(
32+
"COOKIECUTTER_ROBUST_PYTHON_PROJECT_DEMOS_FOLDER", default=DEFAULT_PROJECT_DEMOS_FOLDER
33+
)).resolve()
34+
DEFAULT_DEMO_NAME: str = "robust-python-demo"
3135
DEMO_ROOT_FOLDER: Path = PROJECT_DEMOS_FOLDER / DEFAULT_DEMO_NAME
3236

37+
GENERATE_DEMO_PROJECT_SCRIPT: Path = SCRIPTS_FOLDER / "generate-demo-project.py"
3338
GENERATE_DEMO_PROJECT_OPTIONS: tuple[str, ...] = (
3439
*("--repo-folder", REPO_ROOT),
3540
*("--demos-cache-folder", PROJECT_DEMOS_FOLDER),
3641
*("--demo-name", DEFAULT_DEMO_NAME),
3742
)
3843

44+
SYNC_UV_WITH_DEMO_SCRIPT: Path = SCRIPTS_FOLDER / "sync-uv-with-demo.py"
3945
SYNC_UV_WITH_DEMO_OPTIONS: tuple[str, ...] = (
4046
*("--template-folder", TEMPLATE_FOLDER),
4147
*("--demos-cache-folder", PROJECT_DEMOS_FOLDER),
4248
*("--demo-name", DEFAULT_DEMO_NAME),
4349
)
4450

45-
TEMPLATE_PYTHON_LOCATIONS: tuple[Path, ...] = (Path("noxfile.py"), Path("scripts"), Path("hooks"))
46-
47-
TEMPLATE_CONFIG_AND_DOCS: tuple[Path, ...] = (
48-
Path("pyproject.toml"),
49-
Path(".ruff.toml"),
50-
Path(".editorconfig"),
51-
Path(".gitignore"),
52-
Path(".pre-commit-config.yaml"),
53-
Path(".cz.toml"),
54-
Path("cookiecutter.json"),
55-
Path("README.md"),
56-
Path("LICENSE"),
57-
Path("CODE_OF_CONDUCT.md"),
58-
Path("CHANGELOG.md"),
59-
Path("docs/"),
60-
)
61-
6251

6352
@nox.session(name="generate-demo-project", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
6453
def generate_demo_project(session: Session) -> None:
54+
"""Generates a project demo using the cookiecutter-robust-python template."""
6555
session.install("cookiecutter", "platformdirs", "loguru", "typer")
6656
session.run(
6757
"python",
68-
"scripts/generate-demo-project.py",
58+
GENERATE_DEMO_PROJECT_SCRIPT,
6959
*GENERATE_DEMO_PROJECT_OPTIONS,
70-
external=True,
60+
*session.posargs
7161
)
7262

7363

7464
@nox.session(name="sync-uv-with-demo", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
7565
def sync_uv_with_demo(session: Session) -> None:
66+
"""Syncs the uv environment with the current demo project."""
7667
session.install("cookiecutter", "platformdirs", "loguru", "typer")
7768
session.run(
7869
"python",
79-
"scripts/sync-uv-with-demo.py",
70+
SYNC_UV_WITH_DEMO_SCRIPT,
8071
*SYNC_UV_WITH_DEMO_OPTIONS,
81-
external=True,
8272
)
8373

8474

8575
@nox.session(name="uv-in-demo", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
8676
def uv_in_demo(session: Session) -> None:
77+
"""Runs a uv command in a new project demo project then syncs with it."""
8778
session.install("cookiecutter", "platformdirs", "loguru", "typer")
8879
session.run(
8980
"python",
90-
"scripts/generate-demo-project.py",
81+
GENERATE_DEMO_PROJECT_SCRIPT,
9182
*GENERATE_DEMO_PROJECT_OPTIONS,
92-
external=True,
9383
)
9484
original_dir: Path = Path.cwd()
9585
session.cd(DEMO_ROOT_FOLDER)
9686
session.run("uv", *session.posargs)
9787
session.cd(original_dir)
9888
session.run(
99-
"python",
100-
"scripts/sync-uv-with-demo.py",
89+
SYNC_UV_WITH_DEMO_SCRIPT,
10190
*SYNC_UV_WITH_DEMO_OPTIONS,
10291
external=True,
10392
)
10493

10594

10695
@nox.session(name="in-demo", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
10796
def in_demo(session: Session) -> None:
97+
"""Generates a project demo and run a uv command in it."""
10898
session.install("cookiecutter", "platformdirs", "loguru", "typer")
10999
session.run(
110100
"python",
111-
"scripts/generate-demo-project.py",
101+
GENERATE_DEMO_PROJECT_SCRIPT,
112102
*GENERATE_DEMO_PROJECT_OPTIONS,
113103
)
114104
original_dir: Path = Path.cwd()
@@ -141,6 +131,17 @@ def lint(session: Session):
141131
session.run("ruff", "check", "--verbose", "--fix")
142132

143133

134+
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="lint-generated-project", tags=[])
135+
def lint_generated_project(session: Session):
136+
"""Lint the generated project's Python files and configurations."""
137+
session.log("Installing linting dependencies for the generated project...")
138+
session.install("-e", ".", "--group", "dev", "--group", "lint")
139+
session._runner.posargs = ["nox", "-s", "pre-commit"]
140+
in_demo(session)
141+
session._runner.posargs = [""]
142+
session.run("retrocookie")
143+
144+
144145
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION)
145146
def docs(session: Session):
146147
"""Build the template documentation website."""

scripts/generate-demo-project.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Python script for generating a demo project."""
2-
2+
import os
33
import shutil
4+
import stat
45
import sys
56
from functools import partial
67
from pathlib import Path
78
from typing import Annotated
9+
from typing import Any
10+
from typing import Callable
811

912
import typer
1013
from cookiecutter.main import cookiecutter
@@ -16,10 +19,11 @@
1619
)
1720

1821

19-
def generate_demo_project(repo_folder: Path, demos_cache_folder: Path, demo_name: str) -> Path:
22+
def generate_demo_project(repo_folder: Path, demos_cache_folder: Path, demo_name: str, no_cache: bool) -> Path:
2023
"""Generates a demo project and returns its root path."""
2124
demos_cache_folder.mkdir(exist_ok=True)
22-
_remove_any_existing_demo(demos_cache_folder)
25+
if no_cache:
26+
_remove_existing_demo(demo_path=demos_cache_folder / demo_name)
2327
cookiecutter(
2428
template=str(repo_folder),
2529
no_input=True,
@@ -30,10 +34,27 @@ def generate_demo_project(repo_folder: Path, demos_cache_folder: Path, demo_name
3034
return demos_cache_folder / demo_name
3135

3236

33-
def _remove_any_existing_demo(parent_path: Path) -> None:
34-
"""Removes any existing demos."""
35-
for path in parent_path.iterdir():
36-
shutil.rmtree(path)
37+
def _remove_existing_demo(demo_path: Path) -> None:
38+
"""Removes the existing demo if present."""
39+
if demo_path.exists() and demo_path.is_dir():
40+
previous_demo_pyproject: Path = Path(demo_path, "pyproject.toml")
41+
if not previous_demo_pyproject.exists():
42+
typer.secho(f"No pyproject.toml found at {previous_demo_pyproject=}.", fg="red")
43+
typer.confirm(
44+
"This folder may not be a demo, are you sure you would like to continue?",
45+
default=False,
46+
abort=True,
47+
show_default=True
48+
)
49+
50+
typer.secho(f"Removing existing demo project at {demo_path=}.", fg="yellow")
51+
shutil.rmtree(demo_path, onerror=remove_readonly)
52+
53+
54+
def remove_readonly(func: Callable[[str], Any], path: str, _: Any) -> None:
55+
"""Clears the readonly bit and attempts to call the provided function."""
56+
os.chmod(path, stat.S_IWRITE)
57+
func(path)
3758

3859

3960
cli: typer.Typer = typer.Typer()
@@ -44,10 +65,16 @@ def main(
4465
repo_folder: Annotated[Path, FolderOption("--repo-folder", "-r")],
4566
demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c")],
4667
demo_name: Annotated[str, typer.Option("--demo-name", "-d")],
68+
no_cache: Annotated[bool, typer.Option("--no-cache", "-n")] = False,
4769
) -> None:
4870
"""Updates the poetry.lock file."""
4971
try:
50-
generate_demo_project(repo_folder=repo_folder, demos_cache_folder=demos_cache_folder, demo_name=demo_name)
72+
generate_demo_project(
73+
repo_folder=repo_folder,
74+
demos_cache_folder=demos_cache_folder,
75+
demo_name=demo_name,
76+
no_cache=no_cache
77+
)
5178
except Exception as error:
5279
typer.secho(f"error: {error}", fg="red")
5380
sys.exit(1)

tests/conftest.py

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
"""Fixtures used in all tests for cookiecutter-robust-python."""
22

3-
import os
43
import subprocess
54
from pathlib import Path
6-
from typing import Generator
75

86
import pytest
97
from _pytest.tmpdir import TempPathFactory
@@ -16,49 +14,37 @@
1614

1715

1816
@pytest.fixture(scope="session")
19-
def robust_python_demo_path(tmp_path_factory: TempPathFactory) -> Path:
17+
def demos_folder(tmp_path_factory: TempPathFactory) -> Path:
18+
"""Temp Folder used for storing demos while testing."""
19+
return tmp_path_factory.mktemp("demos")
20+
21+
22+
@pytest.fixture(scope="session")
23+
def robust_python_demo_path(demos_folder: Path) -> Path:
2024
"""Creates a temporary example python project for testing against and returns its Path."""
21-
demos_path: Path = tmp_path_factory.mktemp("demos")
2225
cookiecutter(
2326
str(REPO_FOLDER),
2427
no_input=True,
2528
overwrite_if_exists=True,
26-
output_dir=demos_path,
29+
output_dir=demos_folder,
2730
extra_context={"project_name": "robust-python-demo", "add_rust_extension": False},
2831
)
29-
path: Path = demos_path / "robust-python-demo"
30-
subprocess.run(["uv", "lock"], cwd=path)
32+
path: Path = demos_folder / "robust-python-demo"
33+
subprocess.run(["nox", "-s", "setup-repo"], cwd=path, capture_output=True)
3134
return path
3235

3336

3437
@pytest.fixture(scope="session")
35-
def robust_maturin_demo_path(tmp_path_factory: TempPathFactory) -> Path:
38+
def robust_maturin_demo_path(demos_folder: Path) -> Path:
3639
"""Creates a temporary example maturin project for testing against and returns its Path."""
37-
demos_path: Path = tmp_path_factory.mktemp("demos")
3840
cookiecutter(
3941
str(REPO_FOLDER),
4042
no_input=True,
4143
overwrite_if_exists=True,
42-
output_dir=demos_path,
43-
extra_context={"project_name": "robust-maturin-demo", "add_rust_extension": True},
44+
output_dir=demos_folder,
45+
extra_context={"project_name": "robust-maturin-demo", "add_rust_extension": True}
4446
)
45-
path: Path = demos_path / "robust-maturin-demo"
46-
subprocess.run(["uv", "sync"], cwd=path)
47+
path: Path = demos_folder / "robust-maturin-demo"
48+
subprocess.run(["nox", "-s", "setup-repo"], cwd=path, capture_output=True)
4749
return path
4850

49-
50-
@pytest.fixture(scope="function")
51-
def inside_robust_python_demo(robust_python_demo_path: Path) -> Generator[Path, None, None]:
52-
"""Changes the current working directory to the robust-python-demo project."""
53-
original_path: Path = Path.cwd()
54-
os.chdir(robust_python_demo_path)
55-
yield robust_python_demo_path
56-
os.chdir(original_path)
57-
58-
59-
@pytest.fixture(scope="function")
60-
def inside_robust_maturin_demo(robust_maturin_demo_path: Path) -> Generator[Path, None, None]:
61-
original_path: Path = Path.cwd()
62-
os.chdir(robust_maturin_demo_path)
63-
yield robust_maturin_demo_path
64-
os.chdir(original_path)

tests/constants.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
COOKIECUTTER_FOLDER: Path = REPO_FOLDER / "{{cookiecutter.project_name}}"
1010
HOOKS_FOLDER: Path = REPO_FOLDER / "hooks"
1111
SCRIPTS_FOLDER: Path = REPO_FOLDER / "scripts"
12+
GITHUB_ACTIONS_FOLDER: Path = COOKIECUTTER_FOLDER / ".github"
1213

1314
COOKIECUTTER_JSON_PATH: Path = REPO_FOLDER / "cookiecutter.json"
1415
COOKIECUTTER_JSON: dict[str, Any] = json.loads(COOKIECUTTER_JSON_PATH.read_text())
@@ -18,18 +19,18 @@
1819
PYTHON_VERSIONS: list[str] = [f"3.{VERSION_SLUG}" for VERSION_SLUG in range(MIN_PYTHON_SLUG, MAX_PYTHON_SLUG + 1)]
1920
DEFAULT_PYTHON_VERSION: str = PYTHON_VERSIONS[1]
2021

21-
22+
FIRST_TIME_SETUP_NOX_SESSIONS: list[str] = ["setup-git", "setup-venv", "setup-repo"]
2223
TYPE_CHECK_NOX_SESSIONS: list[str] = [f"typecheck-{python_version}" for python_version in PYTHON_VERSIONS]
2324
TESTS_NOX_SESSIONS: list[str] = [f"tests-{python_version}" for python_version in PYTHON_VERSIONS]
2425
CHECK_NOX_SESSIONS: list[str] = [f"check-{python_version}" for python_version in PYTHON_VERSIONS]
2526
FULL_CHECK_NOX_SESSIONS: list[str] = [f"full-check-{python_version}" for python_version in PYTHON_VERSIONS]
2627

2728

29+
2830
GLOBAL_NOX_SESSIONS: list[str] = [
2931
"pre-commit",
30-
"format-python",
31-
"lint-python",
3232
*TYPE_CHECK_NOX_SESSIONS,
33+
*TESTS_NOX_SESSIONS,
3334
"docs-build",
3435
"build-python",
3536
"build-container",
@@ -41,4 +42,4 @@
4142
"coverage",
4243
]
4344

44-
RUST_NOX_SESSIONS: list[str] = ["format-rust", "lint-rust", "tests-rust", "publish-rust"]
45+
RUST_NOX_SESSIONS: list[str] = ["tests-rust", "publish-rust"]

tests/integration_tests/test_robust_python_demo.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,48 @@ def test_demo_project_generation(robust_python_demo_path: Path) -> None:
1313

1414

1515
@pytest.mark.parametrize("session", GLOBAL_NOX_SESSIONS)
16-
def test_demo_project_noxfile(robust_python_demo_path: Path, session: str) -> None:
17-
command: list[str] = ["uvx", "nox", "-s", session]
16+
def test_demo_project_nox_session(robust_python_demo_path: Path, session: str) -> None:
17+
command: list[str] = ["nox", "-s", session]
1818
result: subprocess.CompletedProcess = subprocess.run(
1919
command,
2020
cwd=robust_python_demo_path,
2121
capture_output=True,
2222
text=True,
23-
timeout=10.0,
23+
timeout=20.0
2424
)
2525
print(result.stdout)
2626
print(result.stderr)
2727
result.check_returncode()
28+
29+
30+
def test_demo_project_nox_pre_commit(robust_python_demo_path: Path) -> None:
31+
command: list[str] = ["nox", "-s", "pre-commit"]
32+
result: subprocess.CompletedProcess = subprocess.run(
33+
command,
34+
cwd=robust_python_demo_path,
35+
capture_output=True,
36+
text=True,
37+
timeout=20.0
38+
)
39+
assert result.returncode == 0
40+
41+
42+
def test_demo_project_nox_pre_commit_with_install(robust_python_demo_path: Path) -> None:
43+
command: list[str] = ["nox", "-s", "pre-commit", "--", "install"]
44+
pre_commit_hook_path: Path = robust_python_demo_path / ".git" / "hooks" / "pre-commit"
45+
assert not pre_commit_hook_path.exists()
46+
47+
result: subprocess.CompletedProcess = subprocess.run(
48+
command,
49+
cwd=robust_python_demo_path,
50+
capture_output=True,
51+
text=True,
52+
timeout=20.0
53+
)
54+
assert pre_commit_hook_path.exists()
55+
assert pre_commit_hook_path.is_file()
56+
57+
assert result.returncode == 0
58+
59+
60+

0 commit comments

Comments
 (0)