Skip to content

Commit ac11444

Browse files
authored
feat(naming): prevent invalid project names (#63)
1 parent a3959ef commit ac11444

File tree

2 files changed

+86
-0
lines changed

2 files changed

+86
-0
lines changed

hooks/pre_gen_project.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Pre-project generation hook for validation
4+
"""
5+
6+
import re
7+
import sys
8+
from logging import basicConfig, getLogger
9+
10+
basicConfig(level="WARNING", format="%(levelname)s: %(message)s")
11+
LOG = getLogger("pre_generation_hook")
12+
13+
# Cookiecutter variables
14+
PROJECT_NAME = "{{ cookiecutter.project_name }}"
15+
PROJECT_SLUG = "{{ cookiecutter.project_slug }}"
16+
17+
18+
def validate_project_name() -> None:
19+
"""Validate that project_name starts with an alphabetical character."""
20+
# Check if project_name starts with an alphabetical character
21+
if not re.match(r"^[a-zA-Z]", PROJECT_NAME):
22+
LOG.error(
23+
"Invalid project name '%s': Python project names must start with an alphabetical character (a-z or A-Z).",
24+
PROJECT_NAME,
25+
)
26+
sys.exit(1)
27+
28+
29+
def validate_project_slug() -> None:
30+
"""Validate that project_slug is a valid Python identifier."""
31+
# Check if project_slug is a valid Python identifier
32+
if not PROJECT_SLUG.isidentifier():
33+
LOG.error("Invalid project slug '%s': Must be a valid Python identifier.", PROJECT_SLUG)
34+
sys.exit(1)
35+
36+
37+
if __name__ == "__main__":
38+
validate_project_name()
39+
validate_project_slug()

tests/test_cookiecutter.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,53 @@ def test_autofix_hook(cookies, context):
211211
pytest.fail(f"stdout: {error.stdout.decode('utf-8')}, stderr: {error.stderr.decode('utf-8')}")
212212

213213

214+
@pytest.mark.unit
215+
@pytest.mark.parametrize(
216+
"invalid_name",
217+
[
218+
"123invalid", # starts with number
219+
"_invalid", # starts with underscore
220+
"-invalid", # starts with dash
221+
"9project", # starts with number
222+
"!invalid", # starts with special character
223+
],
224+
)
225+
def test_invalid_project_name_validation(cookies, invalid_name):
226+
"""
227+
Test that project names starting with non-alphabetical characters are rejected
228+
"""
229+
result = cookies.bake(extra_context={"project_name": invalid_name})
230+
231+
# The generation should fail due to validation
232+
assert result.exit_code != 0, f"Expected validation failure for project name: {invalid_name}"
233+
234+
235+
@pytest.mark.unit
236+
@pytest.mark.parametrize(
237+
"valid_name",
238+
[
239+
"ValidProject", # starts with uppercase
240+
"validproject", # starts with lowercase
241+
"My Project", # starts with uppercase, has space
242+
"a1234", # starts with lowercase, has numbers
243+
"Z_project", # starts with uppercase, has underscore
244+
],
245+
)
246+
def test_valid_project_name_validation(cookies, valid_name):
247+
"""
248+
Test that valid project names starting with alphabetical characters are accepted
249+
"""
250+
# Turn off the post generation hooks for faster testing
251+
os.environ["RUN_POST_HOOK"] = "false"
252+
253+
result = cookies.bake(extra_context={"project_name": valid_name})
254+
255+
# The generation should succeed
256+
assert result.exit_code == 0, f"Expected validation success for project name: {valid_name}"
257+
assert result.exception is None
258+
assert result.project_path.is_dir()
259+
260+
214261
@pytest.mark.integration
215262
@pytest.mark.slow
216263
def test_default_project(cookies):

0 commit comments

Comments
 (0)