name: skrift-forms description: "Skrift form system — Pydantic-backed forms with CSRF protection, template rendering, and hook integration."
Skrift Form System
Skrift forms combine Pydantic validation with CSRF protection, template rendering, and hook integration.
Architecture
FormModel/BaseModel → Form(model, request) → validate() → hooks → data
│ │ │ │
│ │ │ ├─ form_{name}_validated
│ │ │ └─ form_validated
│ │ │
│ │ ├─ CSRF verify (hmac.compare_digest)
│ │ ├─ Token rotation (single-use)
│ │ ├─ Checkbox injection (bool fields)
│ │ └─ Pydantic validation
│ │
│ ├─ render() → Template("form", name).try_render()
│ ├─ csrf_field() → hidden input
│ ├─ fields → dict[str, BoundField]
│ └─ errors → dict[str, str]
│
└─ Registered in _form_registry (by form_name)
Defining Forms
FormModel (auto-registers)
from skrift.forms import FormModel
from pydantic import Field, EmailStr
class ContactForm(FormModel, form_name="contact"):
name: str
email: EmailStr
message: str = Field(json_schema_extra={"widget": "textarea"})
@form() Decorator (registers a plain BaseModel)
from pydantic import BaseModel
from skrift.forms import form
@form("newsletter", action="/subscribe", method="post")
class NewsletterForm(BaseModel):
email: str
Field Customization via json_schema_extra
| Key | Values | Purpose |
|-----|--------|---------|
| label | str | Custom label text |
| widget | "textarea", "select", "checkbox" | Widget type |
| input_type | "email", "password", etc. | HTML input type |
| help_text | str | Help text below field |
| choices | list[tuple[str, str]] | Options for select widget |
| attrs | dict[str, str] | Extra HTML attributes |
Full example:
class ContactForm(FormModel, form_name="contact"):
name: str = Field(json_schema_extra={
"label": "Your Name",
"attrs": {"placeholder": "Jane Doe"},
})
email: EmailStr = Field(json_schema_extra={
"label": "Email Address",
"input_type": "email",
"help_text": "We'll never share your email.",
})
subject: str = Field(json_schema_extra={
"widget": "select",
"choices": [
("general", "General Inquiry"),
("support", "Technical Support"),
],
})
message: str = Field(json_schema_extra={
"widget": "textarea",
"attrs": {"rows": "6"},
})
subscribe: bool = Field(default=False, json_schema_extra={
"label": "Subscribe to newsletter",
})
Controller GET/POST Pattern
from litestar import Controller, get, post, Request
from litestar.response import Template as TemplateResponse, Redirect
from skrift.forms import Form
class ContactController(Controller):
path = "/contact"
@get("/")
async def show(self, request: Request) -> TemplateResponse:
form = Form(ContactForm, request)
return TemplateResponse("contact.html", context={"form": form})
@post("/")
async def submit(self, request: Request) -> TemplateResponse | Redirect:
form = Form(ContactForm, request)
if await form.validate():
# form.data is a validated ContactForm instance
await process_contact(form.data)
return Redirect("/contact?thanks=1")
return TemplateResponse("contact.html", context={"form": form})
Template Rendering
Automatic
{{ form.render() }}
{{ form.render(submit_label="Send") }}
Manual
<form method="{{ form.method }}">
{{ form.csrf_field() }}
{% for field in form %}
{{ field.label_tag() }}
{{ field.widget() }}
{% endfor %}
<button type="submit">Send</button>
</form>
Custom Form Template
Template hierarchy: form-{name}.html → form.html → programmatic fallback.
{# templates/form-contact.html #}
<form method="{{ form.method }}" class="contact-form">
{{ form.csrf_field() }}
{% if form.form_error %}
<div class="alert">{{ form.form_error }}</div>
{% endif %}
<div class="row">
<div class="col">
{{ form['name'].label_tag() }}
{{ form['name'].widget(class_="form-input") }}
</div>
<div class="col">
{{ form['email'].label_tag() }}
{{ form['email'].widget(class_="form-input") }}
</div>
</div>
{{ form['message'].label_tag() }}
{{ form['message'].widget(class_="form-input", rows="8") }}
<button type="submit">{{ submit_label }}</button>
</form>
CSRF Flow
Form.__init__()— Createssecrets.token_urlsafe(32)in session if absent (key:_csrf_token)csrf_field()— Renders<input type="hidden" name="_csrf" value="{token}">validate()— Checks submitted_csrfagainst session token viahmac.compare_digest()- Token rotation — New token generated after successful check (single-use)
Registration Mechanism
Forms are registered in a global _form_registry dict (in model.py):
FormModel.__init_subclass__()auto-registers on class creation@form()decorator registers explicitlyget_form_model(name)retrieves by name, raisesLookupErrorif missing
Hook Integration
Two filter hooks fire after successful validation:
# Form-specific: e.g. "form_contact_validated"
self.data = await hooks.apply_filters(f"form_{self.name}_validated", self.data)
# Global: "form_validated"
self.data = await hooks.apply_filters("form_validated", self.data, self.name)
Example hook:
from skrift.hooks import filter
@filter("form_contact_validated")
async def sanitize_contact(data):
data.message = data.message.strip()
return data
@filter("form_validated")
async def log_all_forms(data, name):
print(f"Form '{name}' submitted")
return data
Jinja2 Global
The Form class is registered as a Jinja2 global, making it accessible in templates without passing it through context.
Key Files
| File | Purpose |
|------|---------|
| skrift/forms/core.py | Form — CSRF, validation, rendering |
| skrift/forms/model.py | FormModel base class, _form_registry, get_form_model() |
| skrift/forms/fields.py | BoundField — field bound to form instance |
| skrift/forms/decorators.py | @form() decorator |
| skrift/templates/form.html | Default form template |
Expert Next.js App Router
Developpement
Un skill qui transforme Claude en expert Next.js App Router.
Générateur de README
Developpement
Crée des README.md professionnels et complets pour vos projets.
Rédacteur de Documentation API
Developpement
Génère de la documentation API complète au format OpenAPI/Swagger.