diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py new file mode 100755 index 0000000..61d21bc --- /dev/null +++ b/hooks/pre_gen_project.py @@ -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() diff --git a/tests/test_cookiecutter.py b/tests/test_cookiecutter.py index 59c53aa..fdf9a03 100755 --- a/tests/test_cookiecutter.py +++ b/tests/test_cookiecutter.py @@ -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):