Benchmark: kultainen esimerkki + zensical-dokumentointiohjeet

- golden-examples/todo/: 6/6 PASS referenssitoteutus
  - SQLAlchemy 2.0 (DeclarativeBase, Mapped, mapped_column)
  - Pydantic v2 (ConfigDict)
  - PEP 621 pyproject.toml, Python >=3.14
  - Uniikki testidata per testi
- CODE_SYSTEM päivitetty: few-shot kultaisesta esimerkistä
- DOCUMENTATION.md: zensical-dokumentointiohjeet
This commit is contained in:
2026-04-14 07:28:47 +03:00
parent 8f154a578c
commit d6a544909c
7 changed files with 311 additions and 17 deletions

View File

@@ -0,0 +1,84 @@
# Dokumentointiohjeet — Zensical
Hyvä dokumentointi kertoo **mitä asia ON**, ei mitä se tekee. Se on kuin zen-koan: lyhyt, tarkka, riittävä.
## Periaatteet
1. **Yksi rivi riittää.** Jos tarvitset kappaleen, koodi on liian monimutkainen.
2. **Kerro mitä, älä miten.** `"""Tietokantamallit — SQLAlchemy 2.0, SQLite."""` ei `"""This module creates database models using SQLAlchemy..."""`
3. **Älä toista koodia.** Jos funktio on `create_todo`, docstring ei ole "Creates a todo".
4. **Suomi tai englanti, ei molempia.** Valitse yksi kieli per projekti.
5. **Ei täytesanoja.** "This module provides functionality for" → poista.
## Mitä dokumentoidaan
| Kohde | Dokumentointi | Esimerkki |
|-------|--------------|-----------|
| **Moduuli** (.py) | Aina. Yksi rivi: mitä tiedosto sisältää. | `"""Pydantic v2 -skeemat — Create ja Response."""` |
| **Luokka** | Aina. Mitä entiteetti edustaa. | `"""Tehtävä — otsikko, deadline, prioriteetti."""` |
| **Funktio** | Vain jos nimi ei kerro kaikkea. | `get_db``"""Tietokantasessio per pyyntö."""` |
| **CRUD-endpoint** | Ei. Nimi + HTTP-metodi riittää. | `create_todo`, `list_todos` — itsedokumentoivia |
| **Testi** | Ei. Testin nimi on dokumentaatio. | `test_get_todo_not_found` — selvä |
| **Konfiguraatio** | Kommentti vain jos arvo yllättää. | `check_same_thread: False # SQLite + FastAPI` |
## Mitä EI dokumentoida
- Importteja
- Ilmeisiä parametreja (`item_id: int`)
- Tyyppivihjeitä jotka kertovat saman asian
- Geneerisiä "boilerplate"-docstringejä
## Esimerkkejä
### Hyvä (zensical)
```python
"""Tietokantamallit — SQLAlchemy 2.0, Mapped-tyypitys, SQLite."""
class Todo(Base):
"""Tehtävä — otsikko, kuvaus, deadline, prioriteetti ja status."""
...
def get_db():
"""Tietokantasessio per pyyntö."""
...
```
### Huono (verbose)
```python
"""
This module defines the database models for the Todo application.
It uses SQLAlchemy ORM to create the database tables and provides
the session factory for database connections.
"""
class Todo(Base):
"""
Represents a todo item in the database.
Attributes:
id: The unique identifier for the todo item.
title: The title of the todo item.
...
"""
...
```
### Huono (tyhjä)
```python
# Ei docstringejä ollenkaan — lukija ei tiedä mikä tiedoston rooli on
class Todo(Base):
__tablename__ = "todos"
...
```
## Tarkistuslista
Generoitu koodi on hyvin dokumentoitu kun:
- [ ] Jokainen .py-tiedosto alkaa yksirivisellä docstringillä
- [ ] Jokainen luokka kertoo mitä entiteetti edustaa
- [ ] Docstringit ovat saman kielen kuin muu koodi
- [ ] CRUD-endpointeilla ei ole turhia docstringejä
- [ ] Kommentteja on vain siellä missä koodi yllättää

View File

@@ -0,0 +1,61 @@
"""FastAPI CRUD — yksi endpoint-setti per entiteetti."""
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from models import SessionLocal, Todo
from schemas import TodoCreate, TodoResponse
app = FastAPI()
def get_db():
"""Tietokantasessio per pyyntö."""
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/todos/", response_model=TodoResponse, status_code=201)
def create_todo(item: TodoCreate, db: Session = Depends(get_db)):
db_item = Todo(**item.model_dump())
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
@app.get("/todos/", response_model=list[TodoResponse])
def list_todos(db: Session = Depends(get_db)):
return db.query(Todo).all()
@app.get("/todos/{item_id}", response_model=TodoResponse)
def get_todo(item_id: int, db: Session = Depends(get_db)):
item = db.query(Todo).filter(Todo.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="Todo not found")
return item
@app.put("/todos/{item_id}", response_model=TodoResponse)
def update_todo(item_id: int, item: TodoCreate, db: Session = Depends(get_db)):
db_item = db.query(Todo).filter(Todo.id == item_id).first()
if not db_item:
raise HTTPException(status_code=404, detail="Todo not found")
for key, value in item.model_dump().items():
setattr(db_item, key, value)
db.commit()
db.refresh(db_item)
return db_item
@app.delete("/todos/{item_id}", status_code=204)
def delete_todo(item_id: int, db: Session = Depends(get_db)):
db_item = db.query(Todo).filter(Todo.id == item_id).first()
if not db_item:
raise HTTPException(status_code=404, detail="Todo not found")
db.delete(db_item)
db.commit()

View File

@@ -0,0 +1,30 @@
"""Tietokantamallit — SQLAlchemy 2.0, Mapped-tyypitys, SQLite."""
from datetime import date
from sqlalchemy import String, Text, Date, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
DATABASE_URL = "sqlite:///./app.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
pass
class Todo(Base):
"""Tehtävä — otsikko, kuvaus, deadline, prioriteetti ja status."""
__tablename__ = "todos"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
title: Mapped[str] = mapped_column(String(255))
description: Mapped[str | None] = mapped_column(Text, default=None)
due_date: Mapped[date | None] = mapped_column(Date, default=None)
priority: Mapped[int] = mapped_column(default=1)
status: Mapped[str] = mapped_column(String(20), default="pending")
Base.metadata.create_all(bind=engine)

View File

@@ -0,0 +1,11 @@
[project]
name = "todo-app"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
"fastapi",
"uvicorn[standard]",
"sqlalchemy",
"pytest",
"httpx",
]

View File

@@ -0,0 +1,22 @@
"""Pydantic v2 -skeemat — Create sisääntulolle, Response vastaukselle."""
from datetime import date
from pydantic import BaseModel, ConfigDict
class TodoCreate(BaseModel):
"""Uuden tehtävän luonti. Pakolliset: title."""
title: str
description: str | None = None
due_date: date | None = None
priority: int = 1
status: str = "pending"
class TodoResponse(TodoCreate):
"""Palautettava tehtävä — sisältää id:n."""
id: int
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,69 @@
"""Pytest — TestClient, erillinen test.db, uniikki data per testi."""
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from main import app, get_db
from models import Base
test_engine = create_engine(
"sqlite:///./test.db", connect_args={"check_same_thread": False}
)
TestSession = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)
Base.metadata.create_all(bind=test_engine)
def override_get_db():
db = TestSession()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
def test_create_todo():
response = client.post("/todos/", json={"title": "Osta maitoa", "priority": 2})
assert response.status_code == 201
assert response.json()["title"] == "Osta maitoa"
assert "id" in response.json()
def test_list_todos():
client.post("/todos/", json={"title": "Listattava tehtävä"})
response = client.get("/todos/")
assert response.status_code == 200
assert len(response.json()) >= 1
def test_get_todo_by_id():
created = client.post("/todos/", json={"title": "Haettava tehtävä"}).json()
response = client.get(f"/todos/{created['id']}")
assert response.status_code == 200
assert response.json()["id"] == created["id"]
def test_get_todo_not_found():
response = client.get("/todos/99999")
assert response.status_code == 404
def test_update_todo():
created = client.post("/todos/", json={"title": "Vanha otsikko"}).json()
response = client.put(
f"/todos/{created['id']}", json={"title": "Uusi otsikko"}
)
assert response.status_code == 200
assert response.json()["title"] == "Uusi otsikko"
def test_delete_todo():
created = client.post("/todos/", json={"title": "Poistettava"}).json()
response = client.delete(f"/todos/{created['id']}")
assert response.status_code == 204
response = client.get(f"/todos/{created['id']}")
assert response.status_code == 404