Skip to content

Commit 1e64bbc

Browse files
Implement comprehensive template enhancements: hooks, typed config, package managers, SBOM, enhanced CI/CD
Co-authored-by: retr0crypticghost <139195952+retr0crypticghost@users.noreply.github.com>
1 parent b464b62 commit 1e64bbc

13 files changed

Lines changed: 1071 additions & 46 deletions

File tree

cookiecutter.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
"github_username": "your-username",
88
"project_description": "A brief description of your project",
99
"python_version": "3.11",
10-
"project_type": ["library", "cli-application", "web-api", "data-science"],
10+
"project_type": ["library", "cli-application"],
11+
"package_manager": ["pip", "uv", "hatch"],
12+
"docs": ["y", "n"],
13+
"typed_config": ["n", "y"],
14+
"sbom": ["n", "y"],
15+
"versioning": ["setuptools-scm", "manual", "hatch"],
1116
"include_docker": "n",
1217
"include_github_actions": "y",
1318
"include_pre_commit": "y",

hooks/post_gen_project.py

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ def remove_directory(dirpath):
2929
print(f"Removed {dirpath}")
3030

3131

32-
def main():
33-
"""Main post-generation logic."""
34-
32+
def cleanup_project_type():
33+
"""Handle project type specific cleanup."""
3534
project_type = "{{ cookiecutter.project_type }}"
3635

3736
if project_type == "library":
@@ -42,6 +41,9 @@ def main():
4241
# Remove CLI module for library projects
4342
cli_module = "src/{{ cookiecutter.package_name }}/cli.py"
4443
remove_file(cli_module)
44+
45+
# Remove CLI tests
46+
remove_file("tests/test_cli.py")
4547

4648
print("✅ Library project configured - removed CLI components")
4749

@@ -52,6 +54,73 @@ def main():
5254
else:
5355
print(f"⚠️ Unknown project type: {project_type}")
5456

57+
58+
def cleanup_docs():
59+
"""Handle documentation cleanup based on docs option."""
60+
docs_enabled = "{{ cookiecutter.docs }}" == "y"
61+
62+
if not docs_enabled:
63+
remove_file("mkdocs.yml")
64+
remove_directory("docs")
65+
66+
# Remove docs workflow
67+
remove_file(".github/workflows/docs.yml")
68+
69+
print("✅ Documentation disabled - removed MkDocs files")
70+
else:
71+
print("✅ Documentation enabled - kept MkDocs files")
72+
73+
74+
def cleanup_typed_config():
75+
"""Handle typed config cleanup."""
76+
typed_config_enabled = "{{ cookiecutter.typed_config }}" == "y"
77+
78+
if not typed_config_enabled:
79+
# Remove Pydantic settings file if it exists
80+
remove_file("src/{{ cookiecutter.package_name }}/settings.py")
81+
print("✅ Typed config disabled - using standard config")
82+
else:
83+
print("✅ Typed config enabled - Pydantic settings available")
84+
85+
86+
def cleanup_sbom():
87+
"""Handle SBOM workflow cleanup."""
88+
sbom_enabled = "{{ cookiecutter.sbom }}" == "y"
89+
90+
if not sbom_enabled:
91+
remove_file(".github/workflows/sbom.yml")
92+
print("✅ SBOM generation disabled - removed workflow")
93+
else:
94+
print("✅ SBOM generation enabled - workflow available")
95+
96+
97+
def cleanup_versioning():
98+
"""Handle versioning cleanup based on versioning option."""
99+
versioning = "{{ cookiecutter.versioning }}"
100+
101+
if versioning == "manual":
102+
# Remove versioning files for manual versioning
103+
remove_file("_version.py") # setuptools-scm version file
104+
print("✅ Manual versioning configured")
105+
elif versioning == "setuptools-scm":
106+
print("✅ setuptools-scm versioning configured")
107+
elif versioning == "hatch":
108+
print("✅ Hatch versioning configured")
109+
110+
111+
def main():
112+
"""Main post-generation logic."""
113+
print("🔧 Configuring generated project...")
114+
115+
# Handle different project types
116+
cleanup_project_type()
117+
118+
# Handle optional features
119+
cleanup_docs()
120+
cleanup_typed_config()
121+
cleanup_sbom()
122+
cleanup_versioning()
123+
55124
print("✅ Project generation completed successfully!")
56125

57126

hooks/pre_gen_project.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Pre-generation hook for python-template cookiecutter.
4+
5+
This script runs before the project is generated and validates
6+
the user inputs, ensuring they meet requirements.
7+
"""
8+
9+
import re
10+
import sys
11+
from pathlib import Path
12+
13+
14+
def validate_package_name(package_name: str) -> bool:
15+
"""
16+
Validate that package_name is a valid Python identifier.
17+
18+
Args:
19+
package_name: The package name to validate
20+
21+
Returns:
22+
bool: True if valid, False otherwise
23+
"""
24+
# Check if it's a valid Python identifier
25+
if not package_name.isidentifier():
26+
return False
27+
28+
# Check for Python keywords
29+
import keyword
30+
if keyword.iskeyword(package_name):
31+
return False
32+
33+
# Check for reserved names
34+
reserved_names = {
35+
'test', 'tests', 'lib', 'src', 'docs', 'examples', 'example',
36+
'setup', 'build', 'dist', 'egg-info', '__pycache__'
37+
}
38+
if package_name.lower() in reserved_names:
39+
return False
40+
41+
return True
42+
43+
44+
def normalize_project_slug(project_slug: str) -> str:
45+
"""
46+
Normalize project slug to follow best practices.
47+
48+
Args:
49+
project_slug: The project slug to normalize
50+
51+
Returns:
52+
str: Normalized project slug
53+
"""
54+
# Convert to lowercase
55+
slug = project_slug.lower()
56+
57+
# Replace spaces and underscores with hyphens
58+
slug = re.sub(r'[\s_]+', '-', slug)
59+
60+
# Remove invalid characters
61+
slug = re.sub(r'[^a-z0-9\-]', '', slug)
62+
63+
# Remove leading/trailing hyphens and collapse multiple hyphens
64+
slug = re.sub(r'-+', '-', slug).strip('-')
65+
66+
return slug
67+
68+
69+
def validate_inputs():
70+
"""Validate all cookiecutter inputs."""
71+
errors = []
72+
73+
# Get cookiecutter variables
74+
package_name = "{{ cookiecutter.package_name }}"
75+
project_slug = "{{ cookiecutter.project_slug }}"
76+
project_type = "{{ cookiecutter.project_type }}"
77+
package_manager = "{{ cookiecutter.package_manager }}"
78+
python_version = "{{ cookiecutter.python_version }}"
79+
80+
# Validate package name
81+
if not validate_package_name(package_name):
82+
errors.append(f"❌ Invalid package_name '{package_name}': Must be a valid Python identifier")
83+
errors.append(" - Cannot contain spaces, hyphens, or special characters")
84+
errors.append(" - Cannot be a Python keyword")
85+
errors.append(" - Cannot be a reserved name (test, tests, lib, src, etc.)")
86+
87+
# Validate project type
88+
valid_project_types = ["library", "cli-application"]
89+
if project_type not in valid_project_types:
90+
errors.append(f"❌ Invalid project_type '{project_type}': Must be one of {valid_project_types}")
91+
92+
# Validate package manager
93+
valid_package_managers = ["pip", "uv", "hatch"]
94+
if package_manager not in valid_package_managers:
95+
errors.append(f"❌ Invalid package_manager '{package_manager}': Must be one of {valid_package_managers}")
96+
97+
# Validate Python version format
98+
try:
99+
version_parts = python_version.split('.')
100+
if len(version_parts) != 2:
101+
raise ValueError("Invalid format")
102+
major, minor = int(version_parts[0]), int(version_parts[1])
103+
if major != 3 or minor < 11:
104+
errors.append(f"❌ Invalid python_version '{python_version}': Must be 3.11 or higher")
105+
except ValueError:
106+
errors.append(f"❌ Invalid python_version '{python_version}': Must be in format 'X.Y' (e.g., '3.11')")
107+
108+
# Check for potential slug issues (warn but don't fail)
109+
normalized_slug = normalize_project_slug(project_slug)
110+
if project_slug != normalized_slug:
111+
print(f"⚠️ Project slug '{project_slug}' would be normalized to '{normalized_slug}'")
112+
print(" Consider using the normalized version for better compatibility")
113+
114+
if errors:
115+
print("🛑 Cookiecutter validation failed:")
116+
print()
117+
for error in errors:
118+
print(error)
119+
print()
120+
print("💡 Tips:")
121+
print(" - Use underscores for package_name: my_package")
122+
print(" - Use hyphens for project_slug: my-project")
123+
print(" - Choose from supported project types: library, cli-application")
124+
print(" - Choose from supported package managers: pip, uv, hatch")
125+
sys.exit(1)
126+
127+
print("✅ Input validation passed!")
128+
129+
130+
def main():
131+
"""Main pre-generation logic."""
132+
print("🔍 Validating cookiecutter inputs...")
133+
validate_inputs()
134+
135+
136+
if __name__ == "__main__":
137+
main()

0 commit comments

Comments
 (0)