-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathnoxfile.py
More file actions
248 lines (188 loc) · 9.5 KB
/
noxfile.py
File metadata and controls
248 lines (188 loc) · 9.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
"""Noxfile for the cookiecutter-robust-python template."""
# /// script
# dependencies = ["nox>=2025.5.1", "platformdirs>=4.3.8", "python-dotenv>=1.0.0"]
# ///
import os
import shutil
from dataclasses import asdict
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import nox
import platformdirs
from dotenv import load_dotenv
from nox.command import CommandFailed
from nox.sessions import Session
nox.options.default_venv_backend = "uv"
DEFAULT_TEMPLATE_PYTHON_VERSION = "3.10"
REPO_ROOT: Path = Path(__file__).parent.resolve()
SCRIPTS_FOLDER: Path = REPO_ROOT / "scripts"
TEMPLATE_FOLDER: Path = REPO_ROOT / "{{cookiecutter.project_name}}"
# Load environment variables from .env and .env.local (if present)
LOCAL_ENV_FILE: Path = REPO_ROOT / ".env.local"
DEFAULT_ENV_FILE: Path = REPO_ROOT / ".env"
if LOCAL_ENV_FILE.exists():
load_dotenv(LOCAL_ENV_FILE)
if DEFAULT_ENV_FILE.exists():
load_dotenv(DEFAULT_ENV_FILE)
APP_AUTHOR: str = os.getenv("COOKIECUTTER_ROBUST_PYTHON_APP_AUTHOR", "robust-python")
COOKIECUTTER_ROBUST_PYTHON_CACHE_FOLDER: Path = Path(
platformdirs.user_cache_path(
appname="cookiecutter-robust-python",
appauthor=APP_AUTHOR,
ensure_exists=True,
)
).resolve()
DEFAULT_PROJECT_DEMOS_FOLDER = COOKIECUTTER_ROBUST_PYTHON_CACHE_FOLDER / "project_demos"
PROJECT_DEMOS_FOLDER: Path = Path(os.getenv(
"COOKIECUTTER_ROBUST_PYTHON_PROJECT_DEMOS_FOLDER", default=DEFAULT_PROJECT_DEMOS_FOLDER
)).resolve()
DEFAULT_DEMO_NAME: str = "robust-python-demo"
DEMO_ROOT_FOLDER: Path = PROJECT_DEMOS_FOLDER / DEFAULT_DEMO_NAME
GENERATE_DEMO_SCRIPT: Path = SCRIPTS_FOLDER / "generate-demo.py"
GENERATE_DEMO_OPTIONS: tuple[str, ...] = (
*("--demos-cache-folder", PROJECT_DEMOS_FOLDER),
)
LINT_FROM_DEMO_SCRIPT: Path = SCRIPTS_FOLDER / "lint-from-demo.py"
LINT_FROM_DEMO_OPTIONS: tuple[str, ...] = GENERATE_DEMO_OPTIONS
UPDATE_DEMO_SCRIPT: Path = SCRIPTS_FOLDER / "update-demo.py"
UPDATE_DEMO_OPTIONS: tuple[str, ...] = (
*GENERATE_DEMO_OPTIONS,
*("--min-python-version", "3.10"),
*("--max-python-version", "3.14")
)
@dataclass
class RepoMetadata:
"""Metadata for a given repo."""
app_name: str
app_author: str
remote: str
main_branch: str
develop_branch: str
TEMPLATE: RepoMetadata = RepoMetadata(
app_name=os.getenv("COOKIECUTTER_ROBUST_PYTHON__APP_NAME"),
app_author=os.getenv("COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR"),
remote=os.getenv("COOKIECUTTER_ROBUST_PYTHON__REMOTE"),
main_branch=os.getenv("COOKIECUTTER_ROBUST_PYTHON__MAIN_BRANCH"),
develop_branch=os.getenv("COOKIECUTTER_ROBUST_PYTHON__DEVELOP_BRANCH")
)
PYTHON_DEMO: RepoMetadata = RepoMetadata(
app_name=os.getenv("ROBUST_PYTHON_DEMO__APP_NAME"),
app_author=os.getenv("ROBUST_PYTHON_DEMO__APP_AUTHOR"),
remote=os.getenv("ROBUST_PYTHON_DEMO__REMOTE"),
main_branch=os.getenv("ROBUST_PYTHON_DEMO__MAIN_BRANCH"),
develop_branch=os.getenv("ROBUST_PYTHON_DEMO__DEVELOP_BRANCH")
)
MATURIN_DEMO: RepoMetadata = RepoMetadata(
app_name=os.getenv("ROBUST_MATURIN_DEMO__APP_NAME"),
app_author=os.getenv("ROBUST_MATURIN_DEMO__APP_AUTHOR"),
remote=os.getenv("ROBUST_MATURIN_DEMO__REMOTE"),
main_branch=os.getenv("ROBUST_MATURIN_DEMO__MAIN_BRANCH"),
develop_branch=os.getenv("ROBUST_MATURIN_DEMO__DEVELOP_BRANCH")
)
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="generate-demo")
def generate_demo(session: Session) -> None:
"""Generates a project demo using the cookiecutter-robust-python template."""
session.install("cookiecutter", "cruft", "platformdirs", "loguru", "python-dotenv", "typer")
session.run("python", GENERATE_DEMO_SCRIPT, *GENERATE_DEMO_OPTIONS, *session.posargs)
@nox.session(python=False, name="clear-cache")
def clear_cache(session: Session) -> None:
"""Clear the cache of generated project demos.
Not commonly used, but sometimes permissions might get messed up if exiting mid-build and such.
"""
session.log("Clearing cache of generated project demos...")
shutil.rmtree(PROJECT_DEMOS_FOLDER, ignore_errors=True)
session.log("Cache cleared.")
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION)
def lint(session: Session):
"""Lint the template's own Python files and configurations."""
session.log("Installing linting dependencies for the template source...")
session.install("-e", ".", "--group", "lint")
session.log(f"Running Ruff formatter check on template files with py{session.python}.")
session.run("ruff", "format")
session.log(f"Running Ruff check on template files with py{session.python}.")
session.run("ruff", "check", "--verbose", "--fix")
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="lint-from-demo")
def lint_from_demo(session: Session):
"""Lint the generated project's Python files and configurations."""
session.log("Installing linting dependencies for the generated project...")
session.install("-e", ".", "--group", "dev", "--group", "lint")
session.run("python", LINT_FROM_DEMO_SCRIPT, *LINT_FROM_DEMO_OPTIONS, *session.posargs)
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION)
def docs(session: Session):
"""Build the template documentation website."""
session.log("Installing documentation dependencies for the template docs...")
session.install("-e", ".", "--group", "docs")
session.log(f"Building template documentation with py{session.python}.")
# Set path to allow Sphinx to import from template root if needed (e.g., __version__.py)
# session.env["PYTHONPATH"] = str(Path(".").resolve()) # Add template root to PYTHONPATH for Sphinx
docs_build_dir = Path("docs") / "_build" / "html"
session.log(f"Cleaning template docs build directory: {docs_build_dir}")
docs_build_dir.parent.mkdir(parents=True, exist_ok=True)
session.run("sphinx-build", "-b", "html", "docs", str(docs_build_dir), "-E")
session.log("Building template documentation.")
session.run("sphinx-build", "-b", "html", "docs", str(docs_build_dir), "-W")
session.log(f"Template documentation built in {docs_build_dir.resolve()}.")
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION)
def test(session: Session) -> None:
"""Run tests for the template's own functionality.
This could involve:
1. Rendering a project from the template into a temporary directory.
2. Changing into the temporary directory.
3. Running essential checks and tests *in the generated project* using uv run nox.
"""
session.log("Running template tests...")
session.log("Installing template testing dependencies...")
# Sync deps from template's own pyproject.toml, e.g., 'dev' group that includes 'pytest', 'cookiecutter'
session.install("-e", ".", "--group", "dev", "--group", "test")
session.run("pytest", "tests")
@nox.parametrize(
arg_names="demo",
arg_values_list=[PYTHON_DEMO, MATURIN_DEMO],
ids=["robust-python-demo", "robust-maturin-demo"]
)
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="update-demo")
def update_demo(session: Session, demo: RepoMetadata) -> None:
session.log("Installing script dependencies for updating generated project demos...")
session.install("cookiecutter", "cruft", "platformdirs", "loguru", "python-dotenv", "typer")
session.log("Updating generated project demos...")
args: list[str] = [*UPDATE_DEMO_OPTIONS]
if "maturin" in demo.app_name:
args.append("--add-rust-extension")
if session.posargs:
args.extend(session.posargs)
demo_env: dict[str, Any] = {f"ROBUST_DEMO__{key.upper()}": value for key, value in asdict(demo).items()}
session.run("python", UPDATE_DEMO_SCRIPT, *args, env=demo_env)
@nox.session(python=False, name="release-template")
def release_template(session: Session):
"""Run the release process for the TEMPLATE using Commitizen.
Requires uvx in PATH (from uv install). Requires Git.
Assumes Conventional Commits practice is followed for TEMPLATE repository.
Optionally accepts increment level (major, minor, patch) after '--'.
"""
session.log("Running release process for the TEMPLATE using Commitizen...")
try:
session.run("git", "version", success_codes=[0], external=True, silent=True)
except CommandFailed:
session.log("Git command not found. Commitizen requires Git.")
session.skip("Git not available.")
session.log("Checking Commitizen availability via uvx.")
session.run("cz", "--version", successcodes=[0])
increment = session.posargs[0] if session.posargs else None
session.log(
"Bumping template version and tagging release (increment: %s).",
increment if increment else "default",
)
cz_bump_args = ["uvx", "cz", "bump", "--changelog"]
if increment:
cz_bump_args.append(f"--increment={increment}")
session.log("Running cz bump with args: %s", cz_bump_args)
# success_codes=[0, 1] -> Allows code 1 which means 'nothing to bump' if no conventional commits since last release
session.run(*cz_bump_args, success_codes=[0, 1], external=True)
session.log("Template version bumped and tag created locally via Commitizen/uvx.")
session.log("IMPORTANT: Push commits and tags to remote (`git push --follow-tags`) to trigger CD for the TEMPLATE.")
@nox.session(python=False, name="remove-demo-release")
def remove_demo_release(session: Session) -> None:
"""Deletes the latest demo release."""
session.run("git", "branch", "-d", f"release/{session.posargs[0]}", external=True)
session.run("git", "push", "--progress", "--porcelain", "origin", f"release/{session.posargs[0]}", external=True)