Skip to content

Commit f5020a4

Browse files
committed
chore: scaffolder - add pre-commit, bandit, unpinned dev deps, README docs and venv handling
1 parent aee4992 commit f5020a4

File tree

11 files changed

+313
-52
lines changed

11 files changed

+313
-52
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
2+
# Scaffold Python Package – Create & Launch New Project
3+
4+
## 🎯 Purpose
5+
Implement a scaffolding project that allows you to launch new Python packages with full setup: `uv` venv management, `pyproject.toml`, `pytest + coverage`, `Sphinx` docs, Git repo, CI config.
6+
7+
## ✅ Inputs
8+
- `package_name` — Package name (PEP 8 valid, e.g. `my_cool_pkg`)
9+
- `target_dir` — Absolute path to parent directory where the project folder will be created
10+
11+
## ✅ Sequence of Tasks
12+
13+
### 1. Validate inputs
14+
- [ ] Run shell script to check `package_name` matches `[a-zA-Z_][a-zA-Z0-9_]*`
15+
- [ ] Ensure `target_dir` is an absolute path
16+
```bash
17+
set -e
18+
PKG="${package_name}"
19+
DIR="${target_dir}"
20+
python3 - <<'PY'
21+
import re, sys, os
22+
pkg=sys.argv[1]; dir_=sys.argv[2]
23+
if not re.fullmatch(r'[a-zA-Z_][a-zA-Z0-9_]*', pkg):
24+
print(f"Invalid package_name: {pkg}"); sys.exit(1)
25+
if not os.path.isabs(dir_):
26+
print(f"target_dir must be absolute: {dir_}"); sys.exit(1)
27+
print("Inputs OK.")
28+
PY "$PKG" "$DIR"
29+
````
30+
31+
### 2. Prepare template workspace
32+
33+
* [ ] Create a temporary workspace `.cline_tmp_pkg_template`
34+
* [ ] Within it, create directory structure for TEMPLATE:
35+
36+
* `TEMPLATE/{{PKG}}/`
37+
* `TEMPLATE/tests/`
38+
* `TEMPLATE/docs/`
39+
* `TEMPLATE/.github/workflows/`
40+
* [ ] Add the following files in TEMPLATE (with placeholder `{{PKG}}` inside):
41+
42+
* `pyproject.toml`
43+
* `README.md`
44+
* `LICENSE`
45+
* `.gitignore`
46+
* `.github/workflows/ci.yaml`
47+
* `{{PKG}}/__init__.py`, `{{PKG}}/hello.py`
48+
* `tests/test_hello.py`
49+
* `docs/conf.py`, `docs/index.rst`
50+
* `requirements-dev.txt`
51+
* `Makefile`
52+
* Optional `setup.cfg` or patch file for dev-extras
53+
54+
### 3. Materialize project & substitute placeholders
55+
56+
* [ ] Compute `DEST="${target_dir}/${package_name}"`
57+
* [ ] Fail if `DEST` already exists
58+
* [ ] Copy all files from workspace TEMPLATE to `${DEST}`
59+
* [ ] Recursively replace placeholder `{{PKG}}` with actual `package_name` in all files
60+
* [ ] Print “Project created at: ${DEST}
61+
62+
### 4. Initialize git repository
63+
64+
* [ ] `cd` into `${DEST}`
65+
* [ ] `git init`
66+
* [ ] `git add .`
67+
* [ ] `git commit -m "chore: initial scaffold"`
68+
* [ ] Print “Git repo initialized.”
69+
70+
### 5. Setup `uv` environment & install dev extras
71+
72+
* [ ] `cd "${DEST}"`
73+
* [ ] `uv venv`
74+
* [ ] `uv pip install -e ".[dev]"`
75+
* [ ] Print “Environment ready.”
76+
77+
### 6. Run tests & coverage
78+
79+
* [ ] `cd "${DEST}"`
80+
* [ ] `uv run pytest --cov="${package_name}" --cov-report=term-missing`
81+
82+
### 7. Build Sphinx documentation
83+
84+
* [ ] `cd "${DEST}"`
85+
* [ ] `uv run sphinx-build -b html docs docs/_build/html`
86+
* [ ] Print “Docs built at: docs/_build/html”
87+
88+
### 8. Output summary
89+
90+
* [ ] Print “Scaffold complete.”
91+
* [ ] Print project path: `${DEST}`
92+
* [ ] Print next steps:
93+
94+
```text
95+
cd "${DEST}"
96+
source .venv/bin/activate # (or use uv run)
97+
uv run python -c "import ${package_name}; print(${package_name}.hello())"
98+
```
99+
100+
## 📁 Generated Structure
101+
102+
```
103+
${package_name}/
104+
.github/workflows/ci.yaml
105+
docs/
106+
conf.py
107+
index.rst
108+
_build/ (after docs build)
109+
tests/
110+
test_hello.py
111+
${package_name}/
112+
__init__.py
113+
hello.py
114+
.gitignore
115+
LICENSE
116+
Makefile
117+
README.md
118+
pyproject.toml
119+
requirements-dev.txt
120+
setup.cfg
121+
```
122+
123+
## 📝 Notes
124+
125+
* Uses `uv` for virtualenv management.
126+
* Configured for `pytest + pytest-cov`.
127+
* Sphinx docs with theme `furo` (can edit in `docs/conf.py`).
128+
* Dev extras via `pyproject.toml` optional-deps.
129+
* CI workflow provided in `.github/workflows/ci.yaml`.
130+
131+
```

pyproject.toml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ dev = [
4343
scaffold-python = "python_project_deployment.cli:main"
4444

4545
[project.urls]
46-
Homepage = "https://github.com/magicman/python-project-deployment"
47-
Repository = "https://github.com/magicman/python-project-deployment"
46+
Homepage = "https://github.com/Magic-Man-us/PythonProjectDeployment"
47+
Repository = "https://github.com/Magic-Man-us/PythonProjectDeployment"
4848

4949
[tool.hatch.build.targets.wheel]
5050
packages = ["python_project_deployment"]
@@ -54,12 +54,12 @@ testpaths = ["tests"]
5454
addopts = "--cov=python_project_deployment --cov-report=term-missing --cov-report=html"
5555

5656
[tool.black]
57-
line-length = 88
57+
line-length = 100
5858
target-version = ['py310']
5959

6060
[tool.isort]
6161
profile = "black"
62-
line_length = 88
62+
line_length = 100
6363

6464
[tool.mypy]
6565
python_version = "3.10"
@@ -68,6 +68,8 @@ warn_unused_configs = true
6868
disallow_untyped_defs = true
6969

7070
[tool.ruff]
71-
line-length = 88
71+
line-length = 100
7272
target-version = "py310"
73+
74+
[tool.ruff.lint]
7375
select = ["E", "F", "I", "N", "W", "B", "C4"]

python_project_deployment/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
from python_project_deployment.models import ProjectConfig
66
from python_project_deployment.scaffolder import Scaffolder
77

8-
__all__ = ["Scaffolder", "ProjectConfig", "__version__"]
8+
__all__ = ["__version__", "ProjectConfig", "Scaffolder"]

python_project_deployment/cli.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ def main(
5050
verbose: bool,
5151
) -> None:
5252
"""Scaffold a new Python package with best practices.
53-
53+
5454
PACKAGE_NAME: Valid Python package identifier (e.g., my_awesome_pkg)
5555
TARGET_DIR: Absolute path to parent directory for the project
56-
56+
5757
Example:
5858
scaffold-python my_package /home/user/projects
5959
"""
@@ -113,7 +113,8 @@ def main(
113113
click.echo("\n📖 Next steps:")
114114
click.echo(f" 1. cd {result_path}")
115115
click.echo(" 2. source .venv/bin/activate # (or use uv run)")
116-
click.echo(f" 3. uv run python -c \"import {package_name}; print({package_name}.hello())\"")
116+
cmd = f'uv run python -c "import {package_name}; print({package_name}.hello())"'
117+
click.echo(f" 3. {cmd}")
117118

118119
click.echo("\n🔍 Available commands:")
119120
click.echo(" • Run tests: pytest")
@@ -140,6 +141,7 @@ def main(
140141
click.secho(f"\n❌ Unexpected Error: {e}", fg="red", bold=True)
141142
if verbose:
142143
import traceback
144+
143145
click.echo("\n" + traceback.format_exc())
144146
sys.exit(1)
145147

python_project_deployment/models.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
class ProjectConfig(BaseModel):
1010
"""Configuration model for a Python project to be scaffolded.
11-
11+
1212
Attributes:
1313
package_name: Valid Python package identifier
1414
target_dir: Absolute path to parent directory where project will be created
@@ -50,32 +50,33 @@ class ProjectConfig(BaseModel):
5050
def validate_package_name(cls, v: str) -> str:
5151
"""Validate that package_name is a valid Python identifier."""
5252
if not re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_]*", v):
53-
raise ValueError(
54-
f"Invalid package_name: '{v}'. "
53+
msg = (
54+
"Invalid package_name: '" + v + "'. "
5555
"Must start with letter or underscore, "
5656
"followed by letters, numbers, or underscores."
5757
)
58+
raise ValueError(msg)
5859
return v
5960

6061
@field_validator("target_dir")
6162
@classmethod
6263
def validate_target_dir(cls, v: Path) -> Path:
6364
"""Validate that target_dir is an absolute path."""
6465
if not v.is_absolute():
65-
raise ValueError(
66-
f"target_dir must be an absolute path, got: {v}"
67-
)
66+
msg = "target_dir must be an absolute path, got: " + str(v)
67+
raise ValueError(msg)
6868
return v
6969

7070
@model_validator(mode="after")
7171
def validate_destination_not_exists(self) -> "ProjectConfig":
7272
"""Validate that destination directory doesn't already exist."""
7373
destination = self.target_dir / self.package_name
7474
if destination.exists():
75-
raise ValueError(
76-
f"Destination already exists: {destination}. "
75+
msg = (
76+
"Destination already exists: " + str(destination) + ". "
7777
"Please choose a different package_name or target_dir."
7878
)
79+
raise ValueError(msg)
7980
return self
8081

8182
@property

0 commit comments

Comments
 (0)