Skip to content

Commit 8a43cea

Browse files
authored
Merge pull request #5 from 56kyle/develop
Develop
2 parents 2e67692 + 128cfd5 commit 8a43cea

File tree

16 files changed

+3541
-91
lines changed

16 files changed

+3541
-91
lines changed

.gitignore

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
.mypy_cache/
2+
/.coverage
3+
/.coverage.*
4+
/.nox/
5+
/.python-version
6+
/.pytype/
7+
/dist/
8+
/docs/_build/
9+
/src/*.egg-info/
10+
11+
# Byte-code files
12+
*.pyc
13+
__pycache__
14+
*.pyd
15+
*.so
16+
*.o
17+
*.pyo # Older optimization files
18+
19+
# Virtual environments
20+
.venv
21+
venv
22+
env
23+
__pypackages__ # If using PEP 582 default (uv does standard venv by default)
24+
.nox # Nox environments
25+
# Other env dirs like virtualenvwrapper workdirs if applicable
26+
27+
# Build outputs
28+
dist/ # Package distributions (sdist, wheel)
29+
build/ # Build artifacts from setuptools/build backends
30+
.hypothesis/ # Hypothesis testing cache
31+
32+
# Test/Coverage related
33+
.pytest_cache/ # Pytest cache directory
34+
htmlcov/ # Coverage HTML reports (default output dir)
35+
.coverage* # Coverage data files (default name pattern)
36+
test-results/ # Directory for JUnit/Coverage XML reports (as configured in noxfile)
37+
38+
# Editor/IDE specific files
39+
.vscode/ # VS Code settings (if not shared via devcontainer)
40+
.idea/ # JetBrains IDE files
41+
*.swp # Vim swap files
42+
43+
# OS specific
44+
.DS_Store # macOS files
45+
Thumbs.db # Windows thumbnail cache
46+
47+
# MyPy cache (if used standalone or by IDE)
48+
.mypy_cache/
49+
50+
# Pyright cache
51+
.pyright/
52+
53+
# Ruff cache
54+
.ruff_cache/
55+
56+
# Bandit cache/baselines (if used standalone)
57+
.bandit_baseline
58+
59+
# Development logs/outputs
60+
debug.log
61+
nohup.out
62+
63+
# Cookiecutter/Cruft metadata (for template updates)
64+
.cruft.json

cookiecutter.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"friendly_name": "{{ cookiecutter.project_name.replace('-', ' ').title() }}",
55
"min_python_version": "3.9",
66
"max_python_version": "3.13",
7+
"add_rust_extension": false,
78
"author": "Kyle Oliver",
89
"email": "56kyleoliver+cookiecutter-robust-python@gmail.com",
910
"github_user": "56kyle",

noxfile.py

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,40 @@
88
import sys
99

1010
import nox
11+
import platformdirs
1112
from nox.sessions import Session
1213

1314
nox.options.default_venv_backend = "uv"
1415

15-
DEFAULT_TEMPLATE_PYTHON_VERSION = "3.12"
16+
DEFAULT_TEMPLATE_PYTHON_VERSION = "3.9"
17+
18+
REPO_ROOT: Path = Path(__file__).parent.resolve()
19+
TEMPLATE_FOLDER: Path = REPO_ROOT / "{{cookiecutter.project_name}}"
20+
21+
22+
COOKIECUTTER_ROBUST_PYTHON_CACHE_FOLDER: Path = Path(
23+
platformdirs.user_cache_path(
24+
appname="cookiecutter-robust-python",
25+
appauthor="56kyle",
26+
ensure_exists=True,
27+
)
28+
).resolve()
29+
30+
PROJECT_DEMOS_FOLDER: Path = COOKIECUTTER_ROBUST_PYTHON_CACHE_FOLDER / "project_demos"
31+
DEFAULT_DEMO_NAME: str = "demo-project"
32+
DEMO_ROOT_FOLDER: Path = PROJECT_DEMOS_FOLDER / DEFAULT_DEMO_NAME
33+
34+
GENERATE_DEMO_PROJECT_OPTIONS: tuple[str, ...] = (
35+
*("--repo-folder", REPO_ROOT),
36+
*("--demos-cache-folder", PROJECT_DEMOS_FOLDER),
37+
*("--demo-name", DEFAULT_DEMO_NAME),
38+
)
39+
40+
SYNC_UV_WITH_DEMO_OPTIONS: tuple[str, ...] = (
41+
*("--template-folder", TEMPLATE_FOLDER),
42+
*("--demos-cache-folder", PROJECT_DEMOS_FOLDER),
43+
*("--demo-name", DEFAULT_DEMO_NAME),
44+
)
1645

1746
TEMPLATE_PYTHON_LOCATIONS: tuple[Path, ...] = (
1847
Path("noxfile.py"),
@@ -34,8 +63,46 @@
3463
)
3564

3665

37-
# === TEMPLATE MAINTENANCE TASKS ===
38-
# Sessions for checking, formatting, building, and releasing the template itself.
66+
@nox.session(name="generate-demo-project", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
67+
def generate_demo_project(session: Session) -> None:
68+
session.install("cookiecutter", "platformdirs", "loguru", "typer")
69+
session.run(
70+
"python",
71+
"scripts/generate-demo-project.py",
72+
*GENERATE_DEMO_PROJECT_OPTIONS,
73+
external=True,
74+
)
75+
76+
77+
@nox.session(name="sync-uv-with-demo", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
78+
def sync_uv_with_demo(session: Session) -> None:
79+
session.install("cookiecutter", "platformdirs", "loguru", "typer")
80+
session.run(
81+
"python",
82+
"scripts/sync-uv-with-demo.py",
83+
*SYNC_UV_WITH_DEMO_OPTIONS,
84+
external=True,
85+
)
86+
87+
@nox.session(name="uv-in-demo", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
88+
def uv_in_demo(session: Session) -> None:
89+
session.install("cookiecutter", "platformdirs", "loguru", "typer")
90+
session.run(
91+
"python",
92+
"scripts/generate-demo-project.py",
93+
*GENERATE_DEMO_PROJECT_OPTIONS,
94+
external=True,
95+
)
96+
original_dir: Path = Path.cwd()
97+
session.cd(DEMO_ROOT_FOLDER)
98+
session.run("uv", *session.posargs)
99+
session.cd(original_dir)
100+
session.run(
101+
"python",
102+
"scripts/sync-uv-with-demo.py",
103+
*SYNC_UV_WITH_DEMO_OPTIONS,
104+
external=True,
105+
)
39106

40107

41108
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION)
@@ -74,15 +141,6 @@ def docs(session: Session):
74141
session.log(f"Template documentation built in {docs_build_dir.resolve()}.")
75142

76143

77-
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION)
78-
def generate_project(session: Session) -> None:
79-
"""Generate a demo project using the template."""
80-
session.log("Installing demo project generation dependencies...")
81-
session.install("cookiecutter", "typer")
82-
session.run("generate-demo-project", "--repo-folder=.", "--demos-cache-folder=.", "--demo-name=demo_project")
83-
84-
85-
86144
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION)
87145
def test(session: Session) -> None:
88146
"""Run tests for the template's own functionality.
@@ -104,19 +162,7 @@ def test(session: Session) -> None:
104162
# Run cookiecutter to generate a project
105163
# Need to find cookiecutter executable - it's in the template dev env installed by uv sync.
106164
cookiecutter_command: list[str] = ["uv", "run", "cookiecutter", "--no-input", "--output-dir", str(temp_dir), "."]
107-
# Add cookiecutter variables to customize the generated project for testing, using --extra-context
108-
cookiecutter_command.extend([
109-
"--extra-context",
110-
"project_name='Test Project'",
111-
"project_slug='test_project'",
112-
"package_name='test_package'",
113-
"author_name='Test Author'",
114-
"author_email='test@example.com'",
115-
"license='MIT'",
116-
"python_version='3.13'", # Use a fixed version for test stability
117-
"add_rust_extension='n'", # Test without Rust initially, add another test session for Rust
118-
# Add other variables needed by cookiecutter.json here to ensure no prompts
119-
])
165+
120166

121167
session.run(*cookiecutter_command, external=True)
122168

pyproject.toml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
[project]
2+
name = "cookiecutter-robust-python"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.9,<4.0"
7+
dependencies = [
8+
"cookiecutter>=2.6.0",
9+
"cruft>=2.16.0",
10+
"loguru>=0.7.3",
11+
"platformdirs>=4.3.8",
12+
]
13+
[project.optional-dependencies]
14+
dev = [
15+
"nox>=2024.3.2",
16+
"commitizen>=3.11.1",
17+
"pre-commit>=3.7.0",
18+
]
19+
20+
test = [
21+
"pytest>=8.1.1",
22+
"pytest-cov>=5.0.0",
23+
]
24+
25+
typecheck = [
26+
"pyright>=1.1.356"
27+
]
28+
29+
lint = [
30+
"ruff>=0.3.5",
31+
"pydocstyle>=6.3.0",
32+
]
33+
34+
security = [
35+
"pip-audit>=2.8.4",
36+
"bandit>=1.7.7",
37+
]
38+
39+
docs = [
40+
"sphinx>=7.3.7",
41+
"myst-parser>=2.0.0",
42+
"furo>=2024.3.4",
43+
"sphinx-copybutton>=0.5",
44+
"sphinx-autodoc-typehints>=1.24.0",
45+
]

scripts/generate-demo-project.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
import shutil
44
import sys
5+
from functools import partial
56
from pathlib import Path
67
from typing import Annotated
78

89
import typer
910

1011
from cookiecutter.main import cookiecutter
1112

12-
FOLDER_TYPE: click.Path = click.Path(dir_okay=True, file_okay=False, resolve_path=True, path_type=Path)
13+
FolderOption: partial[typer.Option] = partial(typer.Option, dir_okay=True, file_okay=False, resolve_path=True, path_type=Path)
1314

1415

1516
def generate_demo_project(repo_folder: Path, demos_cache_folder: Path, demo_name: str) -> Path:
@@ -31,23 +32,22 @@ def _remove_any_existing_demo(parent_path: Path) -> None:
3132
for path in parent_path.iterdir():
3233
shutil.rmtree(path)
3334

35+
cli: typer.Typer = typer.Typer()
3436

35-
@typer.command()
36-
@click.option("--repo-folder", "-r", required=True, type=FOLDER_TYPE)
37-
@click.option("--demos-cache-folder", "-c", required=True, type=FOLDER_TYPE)
38-
@click.option("--demo-name", "-d", required=True, type=str)
37+
38+
@cli.callback(invoke_without_command=True)
3939
def main(
40-
repo_folder: Annotated[Path, typer.Option()],
41-
demos_cache_folder: Path,
42-
demo_name: str
40+
repo_folder: Annotated[Path, FolderOption("--repo-folder", "-r")],
41+
demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c")],
42+
demo_name: Annotated[str, typer.Option("--demo-name", "-d")]
4343
) -> None:
4444
"""Updates the poetry.lock file."""
4545
try:
4646
generate_demo_project(repo_folder=repo_folder, demos_cache_folder=demos_cache_folder, demo_name=demo_name)
4747
except Exception as error:
48-
click.secho(f"error: {error}", fg="red")
48+
typer.secho(f"error: {error}", fg="red")
4949
sys.exit(1)
5050

5151

5252
if __name__ == "__main__":
53-
main(prog_name="generate-demo-project")
53+
cli()

scripts/sync-uv-with-demo.py

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,65 @@
1-
"""Module used to sync the poetry.lock file with one generated using a demo project."""
1+
"""Module used to sync the uv.lock file with one generated using a demo project."""
22

33
import shutil
44
import sys
5+
from functools import partial
56
from pathlib import Path
7+
from typing import Annotated
68

7-
import click
9+
import typer
810

9-
from loguru import logger
11+
from typer.models import OptionInfo
1012

13+
from loguru import logger
1114

12-
FOLDER_TYPE: click.Path = click.Path(dir_okay=True, file_okay=False, resolve_path=True, path_type=Path)
15+
FolderOption: partial[OptionInfo] = partial(
16+
typer.Option, dir_okay=True, file_okay=False, resolve_path=True, path_type=Path
17+
)
1318

1419

15-
def sync_poetry_with_demo(template_folder: Path, demos_cache_folder: Path, demo_name: str) -> None:
20+
def sync_uv_with_demo(template_folder: Path, demos_cache_folder: Path, demo_name: str) -> None:
1621
demo_root: Path = demos_cache_folder / demo_name
17-
demo_poetry_lock_path: Path = _find_poetry_lock_path(demo_root)
18-
output_poetry_lock_path: Path = _find_poetry_lock_path(template_folder)
22+
demo_uv_lock_path: Path = _find_uv_lock_path(demo_root)
23+
output_uv_lock_path: Path = _find_uv_lock_path(template_folder)
1924

20-
_copy_poetry_lock_from_demo(
21-
demo_poetry_lock_path=demo_poetry_lock_path, output_poetry_lock_path=output_poetry_lock_path
25+
_copy_uv_lock_from_demo(
26+
demo_uv_lock_path=demo_uv_lock_path, output_uv_lock_path=output_uv_lock_path
2227
)
23-
logger.info(f"Copied demo from {demo_poetry_lock_path=} to {output_poetry_lock_path=}.")
28+
logger.info(f"Copied demo from {demo_uv_lock_path=} to {output_uv_lock_path=}.")
2429

2530

26-
def _copy_poetry_lock_from_demo(demo_poetry_lock_path: Path, output_poetry_lock_path: Path) -> None:
27-
"""Copies over the poetry.lock file from the provided demo project root."""
31+
def _copy_uv_lock_from_demo(demo_uv_lock_path: Path, output_uv_lock_path: Path) -> None:
32+
"""Copies over the uv.lock file from the provided demo project root."""
2833
shutil.copy(
29-
src=demo_poetry_lock_path,
30-
dst=output_poetry_lock_path,
34+
src=demo_uv_lock_path,
35+
dst=output_uv_lock_path,
3136
)
3237

3338

34-
def _find_poetry_lock_path(search_root: Path) -> Path:
35-
for path in search_root.rglob("poetry.lock"):
39+
def _find_uv_lock_path(search_root: Path) -> Path:
40+
for path in search_root.rglob("uv.lock"):
3641
return path
37-
raise FileNotFoundError(f"Failed to find a poetry.lock within the provided search path: {search_root=}")
42+
raise FileNotFoundError(f"Failed to find a uv.lock within the provided search path: {search_root=}")
43+
44+
45+
cli: typer.Typer = typer.Typer()
3846

3947

40-
@click.command()
41-
@click.option("--template-folder", "-t", required=True, type=FOLDER_TYPE)
42-
@click.option("--demos-cache-folder", "-c", required=True, type=FOLDER_TYPE)
43-
@click.option("--demo-name", "-d", required=True, type=str)
44-
def main(template_folder: Path, demos_cache_folder: Path, demo_name: str) -> None:
45-
"""Updates the poetry.lock file."""
48+
@cli.callback(invoke_without_command=True)
49+
def main(
50+
template_folder: Annotated[Path, FolderOption("--template-folder", "-t", exists=True)],
51+
demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c", exists=True)],
52+
demo_name: Annotated[str, typer.Option("--demo-name", "-d")]
53+
) -> None:
54+
"""Updates the uv.lock file."""
4655
try:
47-
sync_poetry_with_demo(
56+
sync_uv_with_demo(
4857
template_folder=template_folder, demos_cache_folder=demos_cache_folder, demo_name=demo_name
4958
)
5059
except Exception as error:
51-
click.secho(f"error: {error}", fg="red")
60+
typer.secho(f"error: {error}", fg="red")
5261
sys.exit(1)
5362

5463

5564
if __name__ == "__main__":
56-
main(prog_name="prepare-github-release")
65+
cli()

0 commit comments

Comments
 (0)