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
1 change: 0 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ jobs:
--data "license=MIT" \
--data "include_docker=${{ matrix.include_docker }}" \
--data "include_github_actions=${{ matrix.include_github_actions }}" \
--data "include_claude_code=false" \
--trust

- name: 📂 List generated files
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,4 @@ __marimo__/
.streamlit/secrets.toml

.DS_Store
.claude/
3 changes: 2 additions & 1 deletion CLAUDE.md → AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ python-template/
│ ├── *.jinja # Jinja templates (rendered)
│ └── * # Static files (copied as-is)
├── tests/ # Tests for template generation
└── CLAUDE.md # This file
└── AGENTS.md # This file
```

## Template File Conventions
Expand All @@ -35,6 +35,7 @@ python-template/
### Available Commands
```bash
uv run poe sync # Sync dependencies
uv run poe setup # Sync + install hooks
uv run poe format # Format code with ruff
uv run poe lint # Lint with ruff
uv run poe check # Type check with mypy
Expand Down
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,23 @@ A [Copier](https://copier.readthedocs.io/) template for modern Python projects.
- 🔍 **[Mypy](https://mypy.readthedocs.io/)** - Static type checker
- 🧪 **[Pytest](https://docs.pytest.org/)** - Testing framework with coverage

### Infrastructure (Optional)
### Always Included

- 🛫 **Pre-commit hooks** - Automated code quality checks
- 🤖 **AGENTS.md** - Best practices for AI-assisted development

### Optional

- 🐳 **Docker** - Multi-stage builds optimized for Python/UV
- 🔄 **GitHub Actions** - CI/CD pipeline
- 🤖 **Claude Code** - Best practices for AI-assisted development

---

## 🚀 Quick Start

### Prerequisites

- Python 3.12+
- Python 3.13+
- [Copier](https://copier.readthedocs.io/) (`pipx install copier` or `uv tool install copier`)

### Generate a New Project
Expand All @@ -54,11 +57,11 @@ Copier will ask you a series of questions to customize your project:
| `author_name` | Author's full name | - |
| `author_email` | Author's email | - |
| `github_username` | GitHub username/organization | - |
| `python_version` | Minimum Python version | 3.13 |
| `python_version` | Minimum Python version | 3.14 |
| `license` | Project license | MIT |
| `include_docker` | Include Docker support | Yes |
| `include_github_actions` | Include GitHub Actions CI/CD | Yes |
| `include_claude_code` | Include Claude Code configuration | No |


### Example

Expand Down Expand Up @@ -88,17 +91,15 @@ Let's configure your new project!
🎤 GitHub username or organization name.
johndoe
🎤 Minimum Python version for your project.
3.13 (stable, recommended)
3.14 (stable, recommended)
🎤 Open source license for your project.
MIT (permissive, simple)
🎤 Include Docker support?
Yes
🎤 Include GitHub Actions CI/CD?
Yes
🎤 Include Claude Code configuration?
No

create .copier-answers.yml
create AGENTS.md
create .gitignore
create .pre-commit-config.yaml
create .python-version
Expand Down Expand Up @@ -180,7 +181,7 @@ python-template/
│ ├── scripts/
│ │ └── app.toml.jinja
│ ├── .github/ # GitHub Actions (conditional)
│ ├── CLAUDE.md.jinja # Claude Code config (conditional)
│ ├── AGENTS.md.jinja # AI agent instructions (always included)
│ ├── pyproject.toml.jinja
│ ├── README.md.jinja
│ ├── Dockerfile.jinja # Docker support (conditional)
Expand Down
14 changes: 3 additions & 11 deletions copier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,10 @@ python_version:
help: |
Minimum Python version for your project.
This sets requires-python and configures tooling.
default: "3.13"
default: "3.14"
choices:
"3.12 (maintenance)": "3.12"
"3.13 (stable, recommended)": "3.13"
"3.14 (latest)": "3.14"
"3.13 (maintenance)": "3.13"
"3.14 (stable, recommended)": "3.14"

license:
type: str
Expand Down Expand Up @@ -124,13 +123,6 @@ include_github_actions:
Adds workflows for linting, testing, and type checking.
default: true

include_claude_code:
type: bool
help: |
Include Claude Code configuration?
Adds a CLAUDE.md file with Python best practices for AI-assisted development.
default: false

# ─────────────────────────────────────────────────────────
# Computed Variables (internal use)
# ─────────────────────────────────────────────────────────
Expand Down
12 changes: 12 additions & 0 deletions template/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{yaml,yml,toml}]
indent_size = 2
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
All tasks use `poe` (poethepoet), defined in `scripts/app.toml`:

```
uv run poe setup # Sync dependencies + install hooks
uv run poe sync # Sync all dependencies
uv run poe install-hooks # Install pre-commit hooks
uv run poe format # Format code with ruff
Expand Down Expand Up @@ -44,7 +45,6 @@ Run `uv run poe flc` before committing. Run `uv run poe flct` for full validatio

### Type Hints
- Use `str | None` instead of `Optional[str]`
- Use `list[str]` instead of `List[str]`, `dict[str, int]` instead of `Dict[str, int]`
- Always include type hints for function parameters and return types

### Style
Expand Down
12 changes: 3 additions & 9 deletions template/README.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,13 @@

This project uses [UV](https://docs.astral.sh/) as package manager. You need to have it installed in your system.

Once you have that, you can run:
Once you have that, run:

```bash
uv run poe sync
uv run poe setup
```

to create a virtual environment and install all the dependencies, including the development ones.

You also need to install the pre-commit hooks with:

```bash
uv run poe install-hooks
```
This will create a virtual environment, install all dependencies, and set up pre-commit hooks.

### Formatting, Linting and Testing

Expand Down
8 changes: 6 additions & 2 deletions template/scripts/app.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ cmd = "uv sync --all-extras"
help = "Install pre commit hooks."
cmd = "uv run pre-commit install"

[tool.poe.tasks.setup]
help = "Sync dependencies and install pre-commit hooks."
sequence = [ "sync", "install-hooks" ]

[tool.poe.tasks.format]
help = "Format code."
cmd = "uv run ruff format ${POE_ROOT}"
Expand All @@ -16,11 +20,11 @@ cmd = "uv run ruff check --fix ${POE_ROOT}"

[tool.poe.tasks.check]
help = "Run type checker."
cmd = "uv run mypy ${POE_ROOT}"
cmd = "uv run mypy {{ project_slug }}"

[tool.poe.tasks.test]
help = "Run tests."
cmd = "uv run pytest ${POE_ROOT}"
cmd = "uv run pytest"

[tool.poe.tasks.flc]
help = "`flc`: Format, lint, and check."
Expand Down
6 changes: 3 additions & 3 deletions template/tests/test_core.py.jinja
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from {{ project_slug }}.main import foo
from {{ project_slug }}.main import hello


def test_foo() -> None:
assert foo() == "bar"
def test_hello() -> None:
assert hello() == "Hello from {{ project_slug }}!"
6 changes: 3 additions & 3 deletions template/{{project_slug}}/main.py.jinja
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
def foo() -> str:
return "bar"
def hello() -> str:
return "Hello from {{ project_slug }}!"


if __name__ == "__main__":
foo()
print(hello()) # noqa: T201
32 changes: 8 additions & 24 deletions tests/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ def default_answers() -> dict[str, str | bool]:
"license": "MIT",
"include_docker": True,
"include_github_actions": True,
"include_claude_code": False,
}


Expand All @@ -49,6 +48,8 @@ def test_template_generates_successfully(
assert (tmp_path / ".gitignore").exists()
assert (tmp_path / ".pre-commit-config.yaml").exists()
assert (tmp_path / "scripts" / "app.toml").exists()
assert (tmp_path / "AGENTS.md").exists()
assert (tmp_path / ".editorconfig").exists()

# Check project structure
assert (tmp_path / "test_project").is_dir()
Expand Down Expand Up @@ -168,41 +169,24 @@ def test_template_skips_github_actions_when_disabled(
assert not (tmp_path / ".github").exists()


def test_template_generates_claude_code_when_enabled(
def test_template_generates_agents_md(
template_path: Path,
default_answers: dict[str, str | bool],
tmp_path: Path,
) -> None:
"""Test that CLAUDE.md is generated when include_claude_code is True."""
answers = {**default_answers, "include_claude_code": True}
"""Test that AGENTS.md is always generated."""
run_copy(
str(template_path),
tmp_path,
data=answers,
data=default_answers,
unsafe=True,
)

assert (tmp_path / "CLAUDE.md").exists()
assert (tmp_path / "AGENTS.md").exists()

# Verify Python version is templated
claude_md = (tmp_path / "CLAUDE.md").read_text()
assert "3.13" in claude_md


def test_template_skips_claude_code_when_disabled(
template_path: Path,
default_answers: dict[str, str | bool],
tmp_path: Path,
) -> None:
"""Test that CLAUDE.md is not generated when disabled."""
run_copy(
str(template_path),
tmp_path,
data=default_answers,
unsafe=True,
)

assert not (tmp_path / "CLAUDE.md").exists()
agents_md = (tmp_path / "AGENTS.md").read_text()
assert "3.13" in agents_md


def test_template_generates_license_mit(
Expand Down