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
68 changes: 68 additions & 0 deletions .cursor/rules/copier-template.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
description: Copier template development patterns and conventions
globs: ["copier.yaml", "template/**"]
alwaysApply: true
---

# Copier Template Development

This is a Copier template project, not a Python application.

## Template File Conventions

### File Extensions
- `.jinja` suffix → file will be rendered with Jinja2
- No `.jinja` suffix → file copied as-is
- Conditional filenames: `{% if condition %}filename{% endif %}.jinja`

### Directory Names
- `{{variable}}/` → directory name from user input
- `{% if condition %}dirname{% endif %}/` → conditional directory

## Jinja2 Patterns

### Variables
- All variables come from `copier.yaml` questions
- Access with `{{ variable_name }}`
- Use filters: `{{ name | lower | replace(' ', '_') }}`

### Conditionals
```jinja
{% if include_feature %}
content only when feature enabled
{% endif %}

{%- if condition -%} {# strips surrounding whitespace #}
```

### Whitespace Control
- `{%-` strips whitespace before
- `-%}` strips whitespace after
- Use for clean output without extra blank lines

## copier.yaml Structure

### Question Types
- `type: str` - text input
- `type: bool` - yes/no
- `type: int` - number
- `choices:` - selection list

### Computed Variables
```yaml
_hidden_var:
default: "{{ other_var | filter }}"
when: false # Not shown to user
```

### Validation
```yaml
variable:
validator: "{% if not variable %}Error message{% endif %}"
```

## Testing Templates

- Tests use `copier.run_copy()` with `unsafe=True`
- Test files live in `tests/`, not `template/tests/`
- Test both enabled and disabled feature combinations
65 changes: 24 additions & 41 deletions .cursor/rules/development.mdc
Original file line number Diff line number Diff line change
@@ -1,61 +1,44 @@
---
description: Development workflow, commands, and tooling for the project
description: Development workflow for the Copier template project
alwaysApply: true
---

# Development Workflow

## Package Management
- Use `uv` for all dependency management and Python environment operations
- Never use `pip` directly; always use `uv` commands
- Use `uv` for dependency management
- Sync dependencies: `uv sync --all-extras`

## Task Runner
- Use `poe` (poethepoet) for running project tasks
- All poe tasks are defined in `scripts/app.toml`
- Run tasks with: `uv run poe <task-name>`

## Available Commands

### Dependency Management
```bash
uv run poe sync # Sync all dependencies
uv run poe install-hooks # Install pre-commit hooks
```

### Code Quality
```bash
uv run poe sync # Sync dependencies
uv run poe format # Format code with ruff
uv run poe lint # Lint and auto-fix with ruff
uv run poe check # Run type checker (mypy)
uv run poe test # Run tests with pytest
uv run poe lint # Lint with ruff
uv run poe check # Type check with mypy
uv run poe test # Run template tests
uv run poe flc # Format + Lint + Check
uv run poe flct # Format + Lint + Check + Test
uv run poe generate # Generate test project to /tmp
```

### Combined Workflows
```bash
uv run poe flc # Format → Lint → Check (recommended before commits)
uv run poe flct # Format → Lint → Check → Test (full validation)
```
## Testing Template Changes

## Code Quality Tools
1. **Unit tests**: `uv run poe test`
- Tests template generation with various options
- Verifies generated files exist and have correct content
- Validates generated project passes linting/tests

### Ruff
- Line length: 88 characters (Black default)
- Auto-fix enabled by default
- Comprehensive linting ruleset (`select = ["ALL"]`)
- Specific ignores: ANN (type hints), D (docstrings), COM812, ISC001
2. **Manual testing**: `uv run poe generate`
- Creates project at `/tmp/test-project`
- Inspect generated files manually

### MyPy
- Strict mode enabled
- Handles all type checking (not ruff)
## Code Quality

### Pytest
- Code coverage enabled by default
- Reports show missing coverage
- Fails if tests don't cover changes
### What Gets Checked
- `tests/` - Python test code (ruff, mypy)
- `template/` - Excluded from linting (contains Jinja templates)

## Workflow Best Practices
- Run `uv run poe flc` before committing changes
- Run `uv run poe flct` for complete validation
- Use pre-commit hooks for automated checks
- Address linter errors immediately (don't ignore or suppress)
### Pre-commit
- Run `uv run poe flc` before committing
- CI runs full validation on PRs
120 changes: 63 additions & 57 deletions .cursor/rules/testing.mdc
Original file line number Diff line number Diff line change
@@ -1,74 +1,80 @@
---
description: Testing standards and best practices using pytest
globs: ["tests/**/*.py", "**/test_*.py"]
description: Testing patterns for Copier template projects
globs: ["tests/**/*.py"]
alwaysApply: false
---

# Testing Guidelines

## Test Framework
- Use `pytest` as the testing framework (not `unittest`)
- Run tests with: `uv run poe test`
- Tests are located in the `tests/` directory
# Template Testing

## Test Structure

### Fixtures
- **Use fixtures when possible** for test setup and teardown
- Prefer fixtures over setup/teardown methods
- Use fixture scopes appropriately (`function`, `class`, `module`, `session`)
- Name fixtures descriptively based on what they provide
- **Use `conftest.py` for shared fixtures**:
- Place global fixtures in `tests/conftest.py` for all tests
- Use nested `conftest.py` files in subdirectories for package-specific fixtures
- Fixtures in `conftest.py` are automatically discovered by pytest

### Test Organization
- One test file per module: `test_<module_name>.py`
- **Avoid large test files with independent content**
- When testing different things, create separate files unless there's a reason to keep them together
- Split tests by functionality, feature, or component
- Keep test files focused and manageable in size
- Group related tests in classes when it improves organization
- Use descriptive test names that explain what is being tested
- Follow the Arrange-Act-Assert (AAA) pattern

### Test Best Practices
- Keep tests focused and test one thing at a time
- Use parametrize for testing multiple scenarios
- Mock external dependencies and I/O operations
- Ensure tests are independent and can run in any order
- Write tests that are fast and deterministic

## Example Patterns
Tests verify that the Copier template generates valid projects.

```python
import pytest
from copier import run_copy

def test_feature(template_path: Path, tmp_path: Path) -> None:
run_copy(
str(template_path),
tmp_path,
data={"project_slug": "test", ...},
unsafe=True, # Required for local templates
)

@pytest.fixture
def sample_config():
"""Fixture providing test configuration."""
return {"key": "value"}
# Assert generated files exist and have correct content
assert (tmp_path / "expected_file.py").exists()
```

## Common Test Patterns

### Testing Conditional Features
```python
def test_feature_enabled(template_path, tmp_path):
run_copy(..., data={..., "include_feature": True})
assert (tmp_path / "feature_file").exists()

def test_feature_disabled(template_path, tmp_path):
run_copy(..., data={..., "include_feature": False})
assert not (tmp_path / "feature_file").exists()
```

### Testing Generated Content
```python
def test_variable_substitution(template_path, tmp_path):
run_copy(..., data={"project_slug": "my_project"})
content = (tmp_path / "pyproject.toml").read_text()
assert 'name = "my_project"' in content
```

### Testing Generated Project Validity
```python
def test_project_passes_validation(template_path, tmp_path):
run_copy(...)

def test_feature_with_fixture(sample_config):
"""Test using a fixture."""
assert sample_config["key"] == "value"
# Sync deps and run checks
subprocess.run(["uv", "sync"], cwd=tmp_path, check=True)
subprocess.run(["uv", "run", "ruff", "check", "."], cwd=tmp_path, check=True)
subprocess.run(["uv", "run", "mypy", "."], cwd=tmp_path, check=True)
```

## Fixtures

@pytest.mark.parametrize("input,expected", [
(1, 2),
(2, 4),
(3, 6),
])
def test_parametrized(input, expected):
"""Test multiple scenarios with parametrize."""
assert input * 2 == expected
```python
@pytest.fixture
def template_path() -> Path:
return Path(__file__).parent.parent

@pytest.fixture
def default_answers() -> dict[str, str | bool]:
return {
"project_slug": "test_project",
# ... all required answers
}
```

## Coverage
- Code coverage is enabled by default
- Aim for meaningful coverage, not just high percentages
- Focus on testing critical paths and edge cases
- Coverage reports show missing lines to guide test writing
## Best Practices

- Test all boolean option combinations
- Verify generated project passes its own tests
- Check both file existence and content
- Use `tmp_path` fixture for isolation
Loading