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