Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions hooks/pre_gen_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""
Pre-project generation hook for validation
"""

import re
import sys
from logging import basicConfig, getLogger

basicConfig(level="WARNING", format="%(levelname)s: %(message)s")
LOG = getLogger("pre_generation_hook")

# Cookiecutter variables
PROJECT_NAME = "{{ cookiecutter.project_name }}"
PROJECT_SLUG = "{{ cookiecutter.project_slug }}"


def validate_project_name() -> None:
"""Validate that project_name starts with an alphabetical character."""
# Check if project_name starts with an alphabetical character
if not re.match(r"^[a-zA-Z]", PROJECT_NAME):
LOG.error(
"Invalid project name '%s': Python project names must start with an alphabetical character (a-z or A-Z).",
PROJECT_NAME,
)
sys.exit(1)


def validate_project_slug() -> None:
"""Validate that project_slug is a valid Python identifier."""
# Check if project_slug is a valid Python identifier
if not PROJECT_SLUG.isidentifier():
LOG.error("Invalid project slug '%s': Must be a valid Python identifier.", PROJECT_SLUG)
sys.exit(1)


if __name__ == "__main__":
validate_project_name()
validate_project_slug()
47 changes: 47 additions & 0 deletions tests/test_cookiecutter.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,53 @@ def test_autofix_hook(cookies, context):
pytest.fail(f"stdout: {error.stdout.decode('utf-8')}, stderr: {error.stderr.decode('utf-8')}")


@pytest.mark.unit
@pytest.mark.parametrize(
"invalid_name",
[
"123invalid", # starts with number
"_invalid", # starts with underscore
"-invalid", # starts with dash
"9project", # starts with number
"!invalid", # starts with special character
],
)
def test_invalid_project_name_validation(cookies, invalid_name):
"""
Test that project names starting with non-alphabetical characters are rejected
"""
result = cookies.bake(extra_context={"project_name": invalid_name})

# The generation should fail due to validation
assert result.exit_code != 0, f"Expected validation failure for project name: {invalid_name}"


@pytest.mark.unit
@pytest.mark.parametrize(
"valid_name",
[
"ValidProject", # starts with uppercase
"validproject", # starts with lowercase
"My Project", # starts with uppercase, has space
"a1234", # starts with lowercase, has numbers
"Z_project", # starts with uppercase, has underscore
],
)
def test_valid_project_name_validation(cookies, valid_name):
"""
Test that valid project names starting with alphabetical characters are accepted
"""
# Turn off the post generation hooks for faster testing
os.environ["RUN_POST_HOOK"] = "false"

result = cookies.bake(extra_context={"project_name": valid_name})

# The generation should succeed
assert result.exit_code == 0, f"Expected validation success for project name: {valid_name}"
assert result.exception is None
assert result.project_path.is_dir()


@pytest.mark.integration
@pytest.mark.slow
def test_default_project(cookies):
Expand Down
Loading