This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
uv run mkdocs build # Build static HTML to site/
uv run mkdocs serve # Local preview server at http://127.0.0.1:8000Deploy to production:
uv run mkdocs build && rsync --delete -PaczL site mg.sb:/srv/http/uproot.science/This is MkDocs documentation for uproot, a framework for browser-based behavioral experiments.
mkdocs.yml— Site configuration and navigation structuredocs/— Markdown source filesdocs/assets/— Logo, favicon, imagesrequirements.txt— mkdocs-material dependency
The navigation follows a learning arc: orient → build → connect → harden → operate → look up.
- Getting started — Installation, tutorial, project structure
- Building experiments — Pages, forms, data, results, SmoothOperators, live methods, Alpine.js
- Multiplayer experiments — Groups, synchronization, real-time, chat
- Advanced features — Timeouts, dropouts, uploads, custom data models, custom settings forms
- Running experiments — Admin, sessions/rooms, export, deployment
- Reference — Fields, page methods, API, admin API, CLI
Key structural decisions:
- Sessions and rooms are a single page (
running/rooms.md), not two — sessions was too thin to stand alone - Data export is split in two:
running/export.mdtargets the median user (download the ZIP briefcase, analyze in R/Python), whilerunning/export-advanced.mdholds REST API/CLI export, database dumps, anduproot.read - Alpine.js lives in Building (not Advanced) — it is a building tool like live methods
- App static files live in
_static/(notstatic/); project-wide files live in_static/at the project root - Rounds/randomization do not have their own pages — they are covered by SmoothOperators (
building/operators.md) - Results follows Data in the Building section (natural collect → display arc)
The typical reader is a behavioral researcher — an economist, psychologist, or other social scientist — who writes analysis scripts in Python or R but is not a professional developer. Every page must be easy to understand for that reader. When in doubt, explain more and link more, never less.
- Assume little; gloss and link the rest. Never assume knowledge of web development, async Python, decorators, classmethods, HTTP, WebSockets, or Jinja. When a page needs such a concept, explain it in one plain sentence at first use and link to an authoritative external resource for depth (official Python docs, MDN, Jinja, Alpine.js, Bootstrap).
- Link internal resources at first mention. The first time a page mentions a concept documented elsewhere in these docs, link to that page with a relative link (e.g.
[live methods](../building/live.md)). Never make readers hunt through the navigation. - Concrete before abstract. Open with a minimal working example, then explain it. Readers should see what a feature looks like before reading theory about it.
- One new idea at a time. Start with the simplest version that works; add parameters, options, and edge cases afterward, not inline. Advanced material goes at the bottom of the page or on a separate page.
- Say why before how. Introduce every feature with the problem it solves, in one or two sentences, before showing the API.
- Complete, pasteable examples. Code blocks must run as shown in a real project — no hidden setup, no
...where code is required. When a full app is needed for context, link to uproot-examples. - Plain language. Short sentences, active voice, second person (“you”). Prefer everyday words to jargon (“saves”, not “persists”; “runs when”, not “is invoked upon”). If a technical term is unavoidable, define it at first use.
- Consistent terminology. Use exactly one name per concept across all pages (e.g., always “page order”, never “page sequence” or “page flow”).
- Typographical quotes and apostrophes in body text. In regular text — that is, outside code blocks — use typographical quotes (“...”) instead of straight quotes ("..."), and use the typographical apostrophe (’) instead of the straight apostrophe (').
- No spaces around em dashes. Do not surround em dashes (—) by spaces.
- Signpost prerequisites. Pages that build on earlier material say so in the first paragraph, with links (“This page assumes you have read Forms.”).
- Anticipate mistakes. Where users commonly go wrong, show the mistake, the error they will see, and the fix —
!!! warningadmonitions work well for this. - Prose teaches; tables look things up. Guide pages explain in flowing prose with examples; reference pages enumerate in tables and lists. Do not turn a tutorial into a wall of tables.
- Use sentence case for headings, not Title Case.
- Keep pages focused on what users want to accomplish.
- Link to uproot-examples for complete working code.
- Use admonitions (
!!! note,!!! warning) for callouts. - Use tabbed content (
=== "Tab") for platform-specific instructions. - uproot-examples uses the
masterbranch (notmain), so links should be e.g.https://github.com/mrpg/uproot-examples/tree/master/.... - Use
:material-github:prefix for GitHub links in docs. - No manual imports needed in code examples—uproot projects have everything available automatically via
from uproot.smithereens import *. - Avoid guide pages that are just thin wrappers pointing to another page — either cover the topic properly or do not give it its own page.
../uproot/— The main uproot framework (source insrc/uproot/)../uproot-examples/— Example experiments to reference in docs
Page— Standard page with optional form fieldsNoshowPage— Runs code without displaying (for setup/scoring)GroupCreatingWait— Waits and forms groups of participantsSynchronizingWait— Waits for all group members to arrive
Random— Shuffles pages into random orderBracket— Groups pages as an atomic unit (use with Random)Rounds— Repeats pages n times (fixed count vian=parameter)Repeat— Repeats indefinitely untilplayer.add_round = FalseBetween— Randomly selects one option (for between-subjects designs)
player— Individual participant data storagegroup— Shared group data (multiplayer experiments)session— Session-level datagroup.players— All players in a group (returnsStorageBunch)session.players— All players in a session (returnsStorageBunch)session.groups— All groups in a session;session.groups(app="myapp")keeps only groups created in that appplayer.other_in_group— The other player (2-person groups;other_in_group(player)still works)player.others_in_group— All other players in the groupplayer.other_in_session— The other player in a 2-person sessionplayer.others_in_session— All other players in the sessionsession.settings— Session settings as a read-only dotted dict (set via admin JSON)
StorageBunch is the collection type returned by group.players, session.players, etc. It supports:
- Iteration:
for p in group.players - Unpacking:
p1, p2 = group.players filter(*comparisons)— Filter using_comparisonsfind_one(**kwargs)— Find exactly one match (raises on 0 or 2+)each(*keys, simplify=True)— Extract field values into a listassign(key, values)— Set a field on all items from an iterableapply(fn)— Call a function once per item (supports async)
_ (from uproot.smithereens) is a FieldReferent — a placeholder that builds lazy comparisons evaluated per item. Source: src/uproot/queries.py.
# _ supports ==, !=, >, >=, <, <= and chained attribute access
cooperators = group.players.filter(_.cooperate == True)
eligible = session.players.filter(_.present == True, _.age >= 18)
same_round = session.players.filter(_.group.round == 3)Bare _.field (no operator) tests for truthiness. To check for False, write _.field == False.
class MyPage(Page):
@classmethod
def templatevars(page, player):
return dict(...) # Variables for template (may also return None)
@classmethod
def show(page, player):
return True/False # Conditional display
@classmethod
def before_next(page, player):
... # Run before advancing to next pageclass Context(PlayerContext):
@property
def my_value(self):
return self.player.some_field # Available as player.context.my_value in templatesDefined in fields dict or async fields() method (all from uproot.fields):
StringField,TextAreaField,EmailField,IBANField— Text inputsIntegerField,DecimalField,FloatField— Numeric inputs (supportmin,max,addon_start,addon_end)RadioField,SelectField— Single selection (choicesparam; RadioField supportslayout="horizontal")BooleanField— CheckboxLikertField— Rating scale (min,max,label_min,label_max,breakpoint);LikertFieldClassicis the non-responsive table layoutDecimalRangeField,FloatRangeField— Slider (min,max,step,label_min,label_max,hide_popover,anchoring)BoundedChoiceField— Multi-select checkboxes (choices,min,maxselections)DateField— Date pickerFileField— File upload (always a stealth field, handled viahandle_stealth_fields)
Common parameters: label, optional, description, default, render_kw, class_wrapper
show(page, player)→ skip page if Falseearly(page, player, request=)→ earliest hook, has access to HTTP requestbefore_always_once(page, player)→ runs once per page displaybefore_once(page, player)→ runs once per player (first visit only)fields(page, player)→ dynamic form fieldstemplatevars(page, player)/jsvars(page, player)→ template/JS data- (page renders)
validate(page, player, data)→ return str, list[str], or dict[str, str] for errorsbefore_form_save(page, player, data)→ runs after validation, before fields are savedstealth_fields/handle_stealth_fields(page, player, data)→ manual field handlingmay_proceed(page, player)→ gate before advancingafter_once(page, player)→ after first submission onlyafter_always_once(page, player)→ after each submissiontimeout(page, player)/timeout_reached(page, player)→ page timeouts
Inside standard page methods, uproot auto-tracks mutations to lists/dicts. Outside page methods (helpers, @live methods), use a context manager:
with player as p:
p.scores.append(100)Using a context manager is always safe, even when not strictly required.
@livedecorator makes page methods callable from JS viauproot.invoke("method", args)notify(sender, recipients, data, event=, where=)— broadcast to playerssend_to(recipients, data, event=, where=)— server-initiated pushspawn(coroutine)— run an async background task (supervised, logged, cancelled at shutdown, does not survive restarts; use context managers for data mutations)reload(player),move_to_page(player, PageClass),move_to_end(player)
watch_for_dropout(player, handler, tolerance=30.0)— monitor for disconnectionmark_dropout(pid)— manual dropout- Handler is an async function receiving
player
Entrymetaclass for defining entry types (immutable dataclasses)create_model(session)→ModelIdentifieradd_entry(mid, player, EntryType, **fields)— auto-fills identifier fieldsget_entries(mid, EntryType)→ list of(UUID, time, entry)tuplesfilter_entries(mid, EntryType, **filters),get_latest_entry(mid, EntryType)
DESCRIPTION— shown in adminSUGGESTED_MULTIPLE— hint for session player count (admin shows this when creating sessions)LANDING_PAGE— if True, shows landing page before appC— constants class, available in templates;C.__export__copies named constants towindow.Cnew_session(session)— once per session init (lazy: runs when first player arrives)new_player(player)— once per player initrestart()— on server restart (can be async)digest(session)— data for admin digest view (pair withAdminDigest.html,mainblock only)pipeline(session)— admin-runnable job; returnlist[dict]for a downloadable table; optionaldata=rng()— OS-seededrandom.Random; storable on player; prefer overrandomlanguage(player)— per-player ISO 639 code; defaultupd.LANGUAGEpage_order— can be a list or a callable takingplayer=(nested lists are flattened, so SmoothOperators work inside sublists)
- Extend
"Base.html"(participant-facing) or"_uproot/Page.html" - Blocks:
{% block title %},{% block head %},{% block pre_main %},{% block main %},{% block main_full_width %},{% block main2 %},{% block late %} {{ fields() }}renders all form fields;{{ field(form.name) }}renders one{{ chat(session.chat) }}renders chat widget- Built-in filters:
| to(n)(decimal places),| fmtnum(pre=, post=, places=, sep=, decsep=)(rounds half up;uproot.fmtnum(value, {pre, post, places, sep, decsep})in JavaScript gives identical output) - All Python builtins available in templates (
sum(),max(),min(),len(),range(),enumerate(),zip()) {% set buttons = False %}to hide navigation buttons- Base template disable switches:
disable_bootstrap,disable_uproot_fonts,disable_tabular_numbers,disable_terms,disable_auto_start,disable_connection_lost_modal - Admin template disable switch:
disable_navigation player.along("round")— iterate all rounds as(round_number, data)tuplesplayer.within(round=n)— access data from a specific round
Global: uproot setup <path>, uproot api <endpoint>, uproot check-translations [path] (reports untranslated phrases for each language with a YAML file in the project; --untranslated lists field texts translated nowhere), uproot --version
Project (run from project dir): uproot run, uproot start [config] (creates and opens a quick room, prints its URL, then runs the server; --simulate enables simulated responses — prefer advertising this for trying out experiments), uproot reset, uproot dump --file, uproot restore --file, uproot new <app>, uproot newpage <app> <page>, uproot examples, uproot deployment
- Web UI at
/admin/with session/room management - Player actions: advance, revert, move to end, reload, send message, mark dropout, redirect, set fields, group/ungroup
- Admin chat: per-player private messaging during sessions (enable/disable participant replies)
- App testing: “Simulate responses” option runs
simulate.json player pages - Data browser, digest view
- REST API at
/admin/api/v1/with Bearer token auth (upd.API_KEYS.add(key))
- SQLite by default (
uproot.sqlite3), works well in production — PostgreSQL is available but never required - Environment vars:
UPROOT_DATABASE,UPROOT_SQLITE3,UPROOT_POSTGRESQL,UPROOT_ORIGIN,UPROOT_SUBDIRECTORY,UPROOT_API_KEY upd.ADMINS["admin"] = ...(Ellipsis = auto-login on localhost)upd.LANGUAGE—"de","en","es","fr","ja"- Rooms:
upd.DEFAULT_ROOMS.append(room(name, config=, labels=, capacity=, open=));from_file("labels.txt")loads labels (one per line,#comments);labels=[]accepts any non-empty code
- Every download is a ZIP “briefcase”: one top-level folder named after the session, containing
README.txt,DATA_DICTIONARY.json,page_times.csv(or.jsonl),SHA256SUMS, and one folder per format (ultralong/,sparse/,latest/, optionallylatest_by_<gvar>/), each split into one file per storage kind (player.csv,group.csv,session.csv,model.csv) - Formats:
ultralong(one row per field change),sparse(wide event log),latest(one row per storage with final values); file type CSV or JSONL applies to the whole briefcase filters=truecleans up internal_uproot_*fields (renames_uproot_group→group,_uproot_session→session)- REST:
GET /sessions/{sname}/data/export/returns the ZIP briefcase;GET /sessions/{sname}/data/jsonl/streams a single format uproot dump/uproot restorefor full database backupuproot.read: offline analysis in Python —from uproot.read import read; db = read("uproot.sqlite3")