Skip to content

Commit f6afc05

Browse files
committed
chore(ci): build wheelhouse, use uv for venv and runs, cache wheels & pre-commit, add workflow_dispatch and pages deploy
1 parent f5020a4 commit f6afc05

File tree

5 files changed

+224
-50
lines changed

5 files changed

+224
-50
lines changed

.github/dependabot.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "pip"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"
7+
- package-ecosystem: "github-actions"
8+
directory: "/"
9+
schedule:
10+
interval: "weekly"
11+
- package-ecosystem: "github-pre-commit"
12+
directory: "/"
13+
schedule:
14+
interval: "weekly"

.github/workflows/ci.yaml

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
workflow_dispatch:
9+
10+
jobs:
11+
test:
12+
name: Tests and checks
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
17+
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Python ${{ matrix.python-version }}
23+
uses: actions/setup-python@v4
24+
with:
25+
python-version: ${{ matrix.python-version }}
26+
27+
- name: Cache pip
28+
uses: actions/cache@v4
29+
with:
30+
path: |
31+
~/.cache/pip
32+
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }}
33+
restore-keys: |
34+
${{ runner.os }}-pip-${{ matrix.python-version }}-
35+
36+
- name: Cache pip wheels & pre-commit
37+
uses: actions/cache@v4
38+
with:
39+
path: |
40+
~/.cache/pip/wheels
41+
.wheelhouse
42+
~/.cache/pre-commit
43+
key: ${{ runner.os }}-pip-wheels-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }}
44+
restore-keys: |
45+
${{ runner.os }}-pip-wheels-${{ matrix.python-version }}-
46+
47+
- name: Install build tools and uv
48+
run: |
49+
python -m pip install --upgrade pip setuptools wheel
50+
pip install uv
51+
52+
- name: Build wheelhouse for project and dev deps
53+
run: |
54+
# Build wheels for the project and development extras into .wheelhouse
55+
# This will be fast when the cache is warm.
56+
python -m pip wheel -w .wheelhouse "[dev]" || python -m pip wheel -w .wheelhouse ".[dev]"
57+
58+
- name: Create venv (uv)
59+
run: |
60+
# create a fresh .venv using uv
61+
uv venv
62+
63+
- name: Install project dev dependencies into venv from wheelhouse
64+
run: |
65+
# Install using only the local wheels for reproducibility / speed
66+
uv pip install --no-index --find-links .wheelhouse "[dev]" || uv pip install --no-index --find-links .wheelhouse ".[dev]"
67+
68+
- name: Run ruff (via uv)
69+
run: uv run ruff check .
70+
71+
- name: Check formatting with Black (via uv)
72+
run: uv run python -m black --check .
73+
74+
- name: Run mypy (via uv)
75+
run: uv run python -m mypy .
76+
77+
- name: Run pre-commit hooks (all files) via uv
78+
run: |
79+
uv run pre-commit install
80+
uv run pre-commit run --all-files
81+
82+
- name: Run tests (with coverage) via uv
83+
run: |
84+
uv run pytest -q --cov=python_project_deployment --cov-report=xml:coverage.xml --cov-report=html:htmlcov
85+
86+
- name: Upload coverage xml
87+
uses: actions/upload-artifact@v4
88+
with:
89+
name: coverage-xml
90+
path: coverage.xml
91+
92+
- name: Upload coverage HTML
93+
uses: actions/upload-artifact@v4
94+
with:
95+
name: coverage-html
96+
path: htmlcov
97+
98+
- name: Build Sphinx docs via uv
99+
run: |
100+
uv run python -m sphinx -b html docs docs/_build/html || true
101+
102+
- name: Upload docs artifact
103+
uses: actions/upload-artifact@v4
104+
with:
105+
name: docs-html
106+
path: docs/_build/html
107+
108+
deploy-docs:
109+
name: Publish docs to GitHub Pages
110+
runs-on: ubuntu-latest
111+
needs: test
112+
# Only deploy on pushes to main (avoid publishing from PRs)
113+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
114+
steps:
115+
- name: Checkout
116+
uses: actions/checkout@v4
117+
118+
- name: Download docs artifact
119+
uses: actions/download-artifact@v4
120+
with:
121+
name: docs-html
122+
path: docs/_build/html
123+
124+
- name: Upload pages artifact
125+
uses: actions/upload-pages-artifact@v1
126+
with:
127+
path: docs/_build/html
128+
129+
- name: Deploy to GitHub Pages
130+
uses: actions/deploy-pages@v1
131+
with: {}

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ readme = "README.md"
1010
requires-python = ">=3.10"
1111
license = {text = "MIT"}
1212
authors = [
13-
{name = "Magic Man", email = "magicman@example.com"},
13+
{name = "Magic Man", email = "deleterious420+PythonProjectDeployment@gmail.com"},
1414
]
1515
keywords = ["scaffolding", "template", "python", "package", "project"]
1616
classifiers = [
@@ -55,7 +55,7 @@ addopts = "--cov=python_project_deployment --cov-report=term-missing --cov-repor
5555

5656
[tool.black]
5757
line-length = 100
58-
target-version = ['py310']
58+
target-version = ['py313']
5959

6060
[tool.isort]
6161
profile = "black"
@@ -73,3 +73,4 @@ target-version = "py310"
7373

7474
[tool.ruff.lint]
7575
select = ["E", "F", "I", "N", "W", "B", "C4"]
76+
per-file-ignores = {"tests/*" = ["S101", "S108"]}

python_project_deployment/scaffolder.py

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Core scaffolding logic for creating Python projects."""
22

3+
import logging
34
import shutil
45
import subprocess
56
import sys
67
from pathlib import Path
78

8-
from jinja2 import Environment, FileSystemLoader
9+
from jinja2 import Environment, FileSystemLoader, select_autoescape
910

1011
from python_project_deployment.models import ProjectConfig
1112

@@ -30,8 +31,8 @@ def __init__(self, config: ProjectConfig) -> None:
3031
config: Project configuration with all necessary settings
3132
"""
3233
self.config = config
34+
self.logger = logging.getLogger(__name__)
3335
self.template_dir = Path(__file__).parent / "templates"
34-
from jinja2 import select_autoescape
3536

3637
self.jinja_env = Environment(
3738
loader=FileSystemLoader(str(self.template_dir)),
@@ -119,20 +120,24 @@ def _init_git(self, dest: Path) -> None:
119120
Args:
120121
dest: Project directory path
121122
"""
123+
git_path = shutil.which("git")
124+
if not git_path:
125+
raise RuntimeError("git not found in PATH; please install git")
126+
122127
subprocess.run(
123-
["git", "init", "-q"],
128+
[git_path, "init", "-q"],
124129
cwd=dest,
125130
check=True,
126131
capture_output=True,
127132
)
128133
subprocess.run(
129-
["git", "add", "."],
134+
[git_path, "add", "."],
130135
cwd=dest,
131136
check=True,
132137
capture_output=True,
133138
)
134139
subprocess.run(
135-
["git", "commit", "-q", "-m", "chore: initial scaffold"],
140+
[git_path, "commit", "-q", "-m", "chore: initial scaffold"],
136141
cwd=dest,
137142
check=True,
138143
capture_output=True,
@@ -150,6 +155,7 @@ def _setup_environment(self, dest: Path) -> None:
150155

151156
if uv_path:
152157
# Use uv to create venv and run installs
158+
self.logger.info("Using uv binary: %s", uv_path)
153159
subprocess.run([uv_path, "venv"], cwd=dest, check=True, capture_output=True)
154160
subprocess.run(
155161
[uv_path, "pip", "install", "-e", ".[dev]"],
@@ -176,9 +182,8 @@ def _setup_environment(self, dest: Path) -> None:
176182
if proc.returncode != 0:
177183
raise RuntimeError("pre-commit hooks failed:\n" + proc.stdout + proc.stderr)
178184
except Exception as exc: # pragma: no cover - best-effort
179-
raise RuntimeError(
180-
f"Failed to install/run pre-commit hooks using uv: {exc}"
181-
) from exc
185+
msg = "Failed to install/run pre-commit hooks using uv: " + str(exc)
186+
raise RuntimeError(msg) from exc
182187
else:
183188
# No uv: create a standard venv and install uv + pre-commit into it
184189
try:
@@ -206,6 +211,7 @@ def _setup_environment(self, dest: Path) -> None:
206211

207212
# Run pre-commit install and run using the venv's pre-commit binary
208213
precommit_bin = venv_dir / "bin" / "pre-commit"
214+
self.logger.info("Using venv pre-commit: %s", precommit_bin)
209215
subprocess.run(
210216
[str(precommit_bin), "install"],
211217
cwd=dest,
@@ -222,45 +228,65 @@ def _setup_environment(self, dest: Path) -> None:
222228
if proc.returncode != 0:
223229
raise RuntimeError("pre-commit hooks failed:\n" + proc.stdout + proc.stderr)
224230
except Exception as exc: # pragma: no cover - best-effort
225-
raise RuntimeError(
226-
f"Failed to create venv/install tools/run pre-commit: {exc}"
227-
) from exc
231+
msg = "Failed to create venv/install tools/run pre-commit: " + str(exc)
232+
raise RuntimeError(msg) from exc
228233

229234
def _run_tests(self, dest: Path) -> None:
230235
"""Run pytest with coverage.
231236
232237
Args:
233238
dest: Project directory path
234239
"""
235-
subprocess.run(
236-
[
237-
"uv",
240+
uv_path = shutil.which("uv")
241+
if uv_path:
242+
cmd = [
243+
uv_path,
238244
"run",
239245
"pytest",
240246
f"--cov={self.config.package_name}",
241247
"--cov-report=term-missing",
242-
],
243-
cwd=dest,
244-
check=True,
245-
)
248+
]
249+
self.logger.info("Running tests via uv: %s", uv_path)
250+
else:
251+
python_bin = dest / ".venv" / "bin" / "python"
252+
if python_bin.exists():
253+
cmd = [
254+
str(python_bin),
255+
"-m",
256+
"pytest",
257+
f"--cov={self.config.package_name}",
258+
"--cov-report=term-missing",
259+
]
260+
self.logger.info("Running tests via venv python: %s", python_bin)
261+
else:
262+
cmd = [
263+
sys.executable,
264+
"-m",
265+
"pytest",
266+
f"--cov={self.config.package_name}",
267+
"--cov-report=term-missing",
268+
]
269+
self.logger.info("Running tests via system python: %s", sys.executable)
270+
271+
subprocess.run(cmd, cwd=dest, check=True)
246272

247273
def _build_docs(self, dest: Path) -> None:
248274
"""Build Sphinx documentation.
249275
250276
Args:
251277
dest: Project directory path
252278
"""
253-
subprocess.run(
254-
[
255-
"uv",
256-
"run",
257-
"sphinx-build",
258-
"-b",
259-
"html",
260-
"docs",
261-
"docs/_build/html",
262-
],
263-
cwd=dest,
264-
check=True,
265-
capture_output=True,
266-
)
279+
uv_path = shutil.which("uv")
280+
if uv_path:
281+
cmd = [uv_path, "run", "sphinx-build", "-b", "html", "docs", "docs/_build/html"]
282+
self.logger.info("Building docs via uv: %s", uv_path)
283+
else:
284+
python_bin = dest / ".venv" / "bin" / "python"
285+
if python_bin.exists():
286+
cmd = [str(python_bin), "-m", "sphinx", "-b", "html", "docs", "docs/_build/html"]
287+
self.logger.info("Building docs via venv python: %s", python_bin)
288+
else:
289+
cmd = [sys.executable, "-m", "sphinx", "-b", "html", "docs", "docs/_build/html"]
290+
self.logger.info("Building docs via system python: %s", sys.executable)
291+
292+
subprocess.run(cmd, cwd=dest, check=True, capture_output=True)

0 commit comments

Comments
 (0)