diff --git a/README.md b/README.md index 208158d..532187f 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ To get started: #### Prerequisites -1. **Python 3.10+**: We recommend using [pyenv](https://github.com/pyenv/pyenv) or +1. **Python 3.11+**: We recommend using [pyenv](https://github.com/pyenv/pyenv) or [uv](https://docs.astral.sh/uv/) for managing Python versions. 2. **[Pip](https://pip.pypa.io/en/stable/installation/) or [Pipx](https://pipx.pypa.io/stable/)** 3. **[Copier](https://github.com/copier-org/copier)**: Install Copier using pip or pipx. diff --git a/copier.yml b/copier.yml index 942b9ff..ffaf359 100644 --- a/copier.yml +++ b/copier.yml @@ -71,7 +71,7 @@ repository_visibility: - internal - private -repository_slug: +default_module_name: type: str help: "Not prompted. This is computed for re-use." default: "{{ repository_name | lower | replace('-', '_') | replace(' ', '_') }}" @@ -85,11 +85,11 @@ is_public_repo: module_name: type: str - help: "What is your Python module name? (e.g. app/src/{{ repository_slug }})" - default: "{{ repository_slug }}" + help: "What is your Python module name? (e.g. app / src / {{ default_module_name }})" + default: "{{ default_module_name }}" validator: >- - {% if not (module_name | regex_search('^[a-z][a-z0-9\_\-]+$')) %} - Must use a lowercase letter followed by one or more of (a-z, 0-9, _, -). + {% if not (module_name | regex_search('^[a-z][a-z0-9_]*$')) %} + Must be a valid Python package name: start with a lowercase letter and use only lowercase letters, numbers, and underscores. {% endif %} enable_dependabot_version_updates: diff --git a/tests/test_template_generation.py b/tests/test_template_generation.py index f852333..6af7a88 100644 --- a/tests/test_template_generation.py +++ b/tests/test_template_generation.py @@ -2,6 +2,8 @@ from __future__ import annotations +import subprocess +import sys from pathlib import Path from typing import Any @@ -28,6 +30,16 @@ def read_pre_commit_config(destination: Path) -> str: return (destination / ".pre-commit-config.yaml").read_text(encoding="utf-8") +def assert_python_files_compile(*paths: Path) -> None: + """Assert that rendered Python files are syntactically valid.""" + subprocess.run( + [sys.executable, "-m", "py_compile", *[str(path) for path in paths]], + check=True, + capture_output=True, + text=True, + ) + + def assert_public_visibility_outputs(destination: Path) -> None: """Assert the public-repository specific files and README content.""" assert (destination / "LICENSE").exists() @@ -244,8 +256,6 @@ def test_uv_generation_requires_confirmation( generated_project_dir: Path, ) -> None: """The uv option should reject an explicit decision not to use uv.""" - import subprocess - with pytest.raises(subprocess.CalledProcessError) as exc_info: copier_runner.copy( generated_project_dir, @@ -255,3 +265,38 @@ def test_uv_generation_requires_confirmation( ) assert "You must confirm the use of uv to continue" in exc_info.value.stderr + + +def test_valid_custom_module_name_generates_importable_python( + copier_runner: Any, + generated_project_dir: Path, +) -> None: + """A custom module name using underscores should render valid Python imports.""" + copier_runner.copy( + generated_project_dir, + repository_visibility="public", + package_manager="poetry", + module_name="custom_module", + ) + + assert (generated_project_dir / "custom_module").exists() + assert_python_files_compile( + generated_project_dir / "custom_module" / "__main__.py", + generated_project_dir / "tests" / "unit" / "conftest.py", + ) + + +def test_hyphenated_module_name_is_rejected( + copier_runner: Any, + generated_project_dir: Path, +) -> None: + """A hyphenated module name should fail validation because it is not import-safe.""" + with pytest.raises(subprocess.CalledProcessError) as exc_info: + copier_runner.copy( + generated_project_dir, + repository_visibility="public", + package_manager="poetry", + module_name="bad-name", + ) + + assert "valid Python package name" in exc_info.value.stderr