Skip to content
Open
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
4 changes: 3 additions & 1 deletion hooks/pre_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
namespace_import = "{{ cookiecutter.project_namespace_import }}"
if namespace_import and not re.match(NAMESPACE_REGEX, namespace_import):
print(f"ERROR: '{namespace_import}' is not a valid Python namespace import path!")
print(f" It must follow regex '{NAMESPACE_REGEX}', i.e. 'one_two' or 'one_two.three'")
print(
f" It must follow regex '{NAMESPACE_REGEX}', i.e. 'one_two' or 'one_two.three'"
)
sys.exit(1)


Expand Down
2 changes: 0 additions & 2 deletions {{cookiecutter.project_slug}}/.github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ jobs:
strategy:
matrix:
python:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context, we were trying to keep track of active Python versions, as listed here https://devguide.python.org/versions/

Removing Python 3.9 would be fine, since it'll be EOL soon. However, unless we're using Python 3.11 features, I think we should still include Python 3.10. This will ensure compatibility with all supported Python versions by default, and then each project can make the explicit decision to remove compatibility with older versions.

We should add "3.13" while we're here.

runs-on: ubuntu-latest
Expand Down
45 changes: 45 additions & 0 deletions {{cookiecutter.project_slug}}/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Pre-commit hooks for code quality
# See https://pre-commit.com for more information
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.2
hooks:
# Run the formatter
- id: ruff-format
# Run the linter
- id: ruff
args: [--fix]

- repo: local
hooks:
- id: pyright
name: pyright type check
entry: uv run pyright
language: system
types: [python]
pass_filenames: false
require_serial: true

{%- if cookiecutter.docstring_coverage %}
- id: interrogate
name: interrogate docstring coverage
entry: uv run interrogate -c pyproject.toml
language: system
types: [python]
pass_filenames: false
{%- endif %}

- id: pytest
name: pytest (fast tests only)
entry: uv run pytest -x --tb=short -k "not slow"
language: system
types: [python]
pass_filenames: false
require_serial: true
# Only run on pre-commit, not on push
stages: [pre-commit]

# Configuration
ci:
autofix_prs: true
autoupdate_schedule: weekly
167 changes: 167 additions & 0 deletions {{cookiecutter.project_slug}}/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# {{ cookiecutter.project_name }} - Claude Instructions

This document contains project-specific instructions for Claude when working on this codebase.

## Project Overview

{{ cookiecutter.project_description }}

## Code Standards

This project enforces strict code quality standards:

### Code Complexity Limits
- **Max 50 lines per function** - Split larger functions
- **Cyclomatic complexity ≤ 8** - Simplify complex logic
- **Max 5 positional parameters** - Use keyword arguments or dataclasses
- **Max 12 branches per function** - Extract to helper functions
- **Max 6 return statements** - Consolidate exit points

### Style Guidelines
- **Line length**: 100 characters max
- **Docstrings**: Google style on all public functions/classes
- **Type hints**: Required for all function signatures
- **Tests**: Must live beside code (`test_*.py` or `*_test.py`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks to contradict the current structure. The current structure has the source files in src/{{cookiecutter.__module_import}}/*.py and tests in test/test_*.py.

I don't think I've seen Python projects with test files next to the source code files. I have seen it for C and C++ projects though, particularly projects from Google.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some research, some people are saying that for large projects, it's helpful to have unit tests next to the source code to easily test distributions of the project and also to more easily find tests for functionality in that source file.

Others say that including the test files in the source directory will make distribution packages larger, which may or may not be a concern.

I don't think I have strong opinions on this.

Copy link

@ret2libc ret2libc Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've always found it better to have tests in a separate dir, but... whatever works.


## Quick Commands

```bash
# Development setup
make dev

# Run all checks
make check # Runs lint + tests

# Code quality
make lint # ruff format --check + ruff check + pyright
make fix # Auto-fix formatting and lint issues
make typecheck # Run pyright type checker

# Testing
make test # Run pytest with coverage

# Development
{% if cookiecutter.entry_point -%}
make run ARGS="--help" # Run the CLI
{%- endif %}
make doc # Build documentation
```
## Project Structure
```
src/
└── {{ cookiecutter.__project_import.replace('.', '/') }}/
├── __init__.py
{%- if cookiecutter.entry_point %}
├── __main__.py # CLI entry point
├── _cli.py # CLI implementation
{%- endif %}
└── py.typed # Type checking marker

test/
└── test_*.py # Traditional test location
```
Tests can also live beside source files as `test_*.py` or `*_test.py`.
## General Python Guidelines
These are general preferences for Python development:
- **Web frameworks**: Prefer FastAPI over Flask for new projects
- **Data processing**: Consider Polars for performance-critical data operations
- **Async programming**: Use native async/await instead of threading
- **Type checking**: Always use type hints and run pyright
## Common Patterns
### Error Handling
```python
from typing import Result # If using result types

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this thing real or ai-made up? I've never seen it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I also get an error when trying to import this under Python 3.13.

The Google style guide has a section on error handling and exceptions https://google.github.io/styleguide/pyguide.html#24-exceptions


def process_data(path: str) -> Result[Data, str]:
"""Process data from file.
Args:
path: Path to data file.
Returns:
Result with Data on success, error message on failure.
"""
try:
# Implementation
return Ok(data)
except Exception as e:
return Err(f"Failed to process: {e}")
```
### Logging
```python
import logging

logger = logging.getLogger(__name__)
```
{%- if cookiecutter.entry_point %}
### CLI Arguments
Use the existing `_cli.py` structure:
```python
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Enable verbose output"
)
```
{%- endif %}
## Testing Guidelines
- Aim for 100% test coverage (enforced by CI)
- Use `pytest.mark.parametrize` for multiple test cases
- Mock external dependencies
- Test both success and error paths
### Test Markers
Mark slow tests to exclude them from pre-commit hooks:
```python
import pytest
@pytest.mark.slow
def test_integration_with_external_api():
"""This test won't run during pre-commit hooks."""
...
def test_fast_unit_test():
"""This test will run during pre-commit hooks."""
...
```
The pre-commit hook runs `pytest -k "not slow"` to skip slow tests.
## CI/CD
GitHub Actions run on every push/PR:
1. **Linting**: ruff format/check + pyright type checking
2. **Tests**: pytest with coverage
3. **Security**: zizmor workflow scanning
{%- if cookiecutter.documentation == "pdoc" %}
4. **Docs**: Auto-deploy to GitHub Pages
{%- endif %}
## Important Notes
1. **Never commit code that violates the quality standards** - refactor instead
2. **All public APIs need Google-style docstrings**
3. **Type hints are mandatory** - use `pyright`
4. **Tests can live beside code** - prefer colocated tests for better maintainability
## Project-Specific Instructions
<!-- Add any project-specific Claude instructions here -->
---
*This file helps Claude understand project conventions. Update it as patterns emerge.*
18 changes: 15 additions & 3 deletions {{cookiecutter.project_slug}}/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ all:

.PHONY: dev
dev: $(VENV)/pyvenv.cfg
uv run pre-commit install

{%- if cookiecutter.entry_point %}
.PHONY: run
Expand All @@ -59,17 +60,28 @@ $(VENV)/pyvenv.cfg: pyproject.toml
lint: $(VENV)/pyvenv.cfg
uv run ruff format --check && \
uv run ruff check && \
uv run mypy
uv run pyright

{%- if cookiecutter.docstring_coverage %}
uv run interrogate -c pyproject.toml .
{%- endif %}

.PHONY: reformat
reformat:
.PHONY: check
check: lint test

.PHONY: typecheck
typecheck: $(VENV)/pyvenv.cfg
uv run pyright

.PHONY: fix
fix:
uv run ruff format && \
uv run ruff check --fix

# Alias for backwards compatibility
.PHONY: reformat
reformat: fix

.PHONY: test tests
test tests: $(VENV)/pyvenv.cfg
uv run pytest --cov=$(PY_IMPORT) $(T) $(TEST_ARGS)
Expand Down
Loading