From 1fce280af28e33476592c084f313db62f3c431e3 Mon Sep 17 00:00:00 2001 From: Rens Date: Wed, 12 Aug 2026 15:07:06 +0200 Subject: [PATCH] add module docstring for fasthtml.core --- fasthtml/__init__.py | 6 ++++ fasthtml/core.py | 62 ++++++++++++++++++++++++++++++++--- nbs/api/00_core.ipynb | 76 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 134 insertions(+), 10 deletions(-) diff --git a/fasthtml/__init__.py b/fasthtml/__init__.py index 88cf9950..17d1f1e2 100644 --- a/fasthtml/__init__.py +++ b/fasthtml/__init__.py @@ -1,2 +1,8 @@ +"""The fastest way to create an HTML app + +Modules: + +- `fasthtml.core`: The `FastHTML` subclass of `Starlette`.""" + __version__ = "0.14.12" from .core import * diff --git a/fasthtml/core.py b/fasthtml/core.py index 46b4d1b9..792f4d56 100644 --- a/fasthtml/core.py +++ b/fasthtml/core.py @@ -1,5 +1,45 @@ """The `FastHTML` subclass of `Starlette`. +Create an app with `app = FastHTML()`. The `@rt` decorator adds routes (`rt = app.route`), inferring what it can from the function: + + @rt + def index(): ... # GET,POST / + @rt + def foo(): ... # GET,POST /foo + @rt('/hi') + def get(): ... # GET /hi (function named after a verb handles just that verb) + @rt('/cards/{id}') + class Cards: ... # class route group: each verb-named method (get, patch, ...) becomes a handler + +The decorated function doubles as a URL builder, so links never need hardcoded paths: + + A('More', href=foo.to(a=1)) # `.to()` fills path params; the rest become query params. + Div('...', hx_get=foo) # route functions work directly as htmx attr values: hx-get="/foo" + +Handlers return FT trees (from `fastcore.xml`), rendered as HTML automatically: a full page for regular requests, a bare fragment for HTMX requests. In a full page, top-level `Title`, `Meta`, `Link`, and `Style` items go in ``; the rest goes in ``. Strings are sent as HTML, dicts as JSON, and a Starlette `Response` is sent untouched. A return tuple may mix content with special items: any `HttpHeader` (e.g. from `cookie()` or `HtmxResponseHeaders()`) becomes a response header, any `BackgroundTask` runs after the response is sent, and the rest renders as content. `Redirect` picks the right redirect mechanism for HTMX vs regular requests, and `FtResponse` wraps FT content when you need the status code or headers. + +Handler parameters are filled from the request: each is looked up in path, cookie, header (snake_case names match Hyphen-Case headers), query, then form data, and cast by calling its annotation on the value. `bool`, `int`, and `date` get smart string parsing; `UploadFile` passes through. Repeated params take the last value unless annotated `list[T]`, which collects them all. A param with no annotation and no special name is ignored, with a warning. A dataclass, TypedDict, namedtuple, or any annotated class collects the whole form body; a `__from_request__` classmethod customizes construction. A missing required param is a 400, a failed cast a 404. These special names need no annotation: + + req, ws the Request / WebSocket connection + sess session dict + app the FastHTML app + state app.state + scope ASGI scope + auth scope['auth'] + htmx HtmxHeaders (parsed HX-* request headers) + body raw body text + data parsed form data as a dict + api ApiReturn (API vs browser dual responses) + send websocket send function + hdrs,ftrs the per-request copies of the app's `hdrs`/`ftrs` (head and footer content) + htmlkw the per-request copy of the app's `` attrs + bodykw the per-request copy of the app's `` attrs + resp the handler's response (injected into `after` functions) + +`before`/`after` functions (app-level, with `Beforeware` skip patterns, or per-route `before=`) get the same param injection as handlers. A `before` returning a value responds with it and skips the handler (the usual auth-guard pattern). + +See the [Handling Handlers tutorial](https://www.fastht.ml/docs/ref/handlers.html.md) for worked examples of the above. + Docs: https://www.fastht.ml/docs/api/core.html.md""" # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/00_core.ipynb. @@ -72,6 +112,7 @@ def snake2hyphens(s:str): @dataclass class HtmxHeaders: + "Parsed HX-* request headers" boosted:str|None=None; current_url:str|None=None; history_restore_request:str|None=None; prompt:str|None=None request:str|None=None; request_type:str|None=None; source:str|None=None; target:str|None=None; trigger_name:str|None=None; trigger:str|None=None @@ -120,7 +161,9 @@ def _form_arg(k, v, d): # %% ../nbs/api/00_core.ipynb #5fc04751 @dataclass -class HttpHeader: k:str;v:str +class HttpHeader: + "A response header as a k,v pair" + k:str;v:str # %% ../nbs/api/00_core.ipynb #94e18161 def _to_htmx_header(s): return 'HX-' + s.replace('_', '-').title() @@ -191,6 +234,7 @@ async def _from_body(conn, p, data): # %% ../nbs/api/00_core.ipynb #88b6da3f class ApiReturn: + "Request param: call with browser and API responses; returns kwargs as JSON if the client accepts `application/json`, else `norm`" @classmethod async def __from_request__(cls, data, req): return cls(req.headers.get('accept')=='application/json') def __init__(self, isapi=False): self.isapi = isapi @@ -278,6 +322,7 @@ def flat_xt(lst): # %% ../nbs/api/00_core.ipynb #aacff5ac class Beforeware: + "Wrap a `before` function with `skip` path patterns that bypass it" def __init__(self, f, skip=None): self.f,self.skip = f,skip or [] def __repr__(self): return f'Beforeware({self.f}, skip={self.skip})' @@ -326,6 +371,7 @@ def EventStream(s): # %% ../nbs/api/00_core.ipynb #0dd0a414 def signal_shutdown(): + "Return an `asyncio.Event` set when uvicorn receives a shutdown signal" from uvicorn.main import Server event = asyncio.Event() @patch @@ -614,6 +660,7 @@ def __aiter__(self): return self.gen async def __anext__(self): return await self.gen.__anext__() class Lifespan: + "Combine `on_startup`/`on_shutdown` callbacks and an optional lifespan generator into one context manager" def __init__(self, startup=None, shutdown=None, ls=None): startup,shutdown = listify(startup),listify(shutdown) store_attr() @@ -632,6 +679,7 @@ def on_event(self, event_type): # %% ../nbs/api/00_core.ipynb #3327a1e9 class FastHTML(Starlette): + "An HTML-first Starlette app: handler params filled from the request, FT returns rendered as pages or HTMX fragments" def __init__(self, debug=False, routes=None, middleware=None, title: str = "FastHTML page", exception_handlers=None, on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, exts=None, before=None, after=None, surreal=True, htmx=True, htmx4=False, default_hdrs=True, sess_cls=SessionMiddleware, @@ -895,6 +943,7 @@ async def _request(): return await self.cli.request(method, url, **kwargs) # %% ../nbs/api/00_core.ipynb #d5223a9a class RouteFuncs: + "Attr-access store of named route functions (HTTP verb names excluded)" def __init__(self): super().__setattr__('_funcs', {}) def __setattr__(self, name, value): self._funcs[name] = value def __getattr__(self, name): @@ -966,6 +1015,7 @@ def cookie(key: str, value="", max_age=None, expires=None, path="/", domain=None # %% ../nbs/api/00_core.ipynb #8816f277 def reg_re_param(m, s): + "Register a Starlette URL convertor named `m` matching regex `s`" cls = get_class(f'{m}Conv', sup=StringConvertor, regex=s) register_url_convertor(m, cls()) @@ -990,6 +1040,7 @@ async def get(fname:str): return FileResponse(f'{static_path}/{fname}{ext}') # %% ../nbs/api/00_core.ipynb #f63b7a03 class StaticNoCache(StaticFiles): + "StaticFiles that sends `Cache-Control: no-cache`, so browsers revalidate on every use" def file_response(self, *args, **kwargs): resp = super().file_response(*args, **kwargs) resp.headers.setdefault("Cache-Control", "no-cache") @@ -1054,11 +1105,11 @@ async def _inner(*args, **kw): # %% ../nbs/api/00_core.ipynb #1960d7ff class MiddlewareBase: + "Base for middleware: passes non-http/ws scopes through, else returns the `HTTPConnection`" async def __call__(self, scope, receive, send) -> None: - if scope["type"] not in ["http", "websocket"]: - await self._app(scope, receive, send) - return - return HTTPConnection(scope) + if scope["type"] in ("http", "websocket"): return HTTPConnection(scope) + await self._app(scope, receive, send) + # %% ../nbs/api/00_core.ipynb #83a20f93 class FtResponse: @@ -1077,6 +1128,7 @@ def __response__(self, req): # %% ../nbs/api/00_core.ipynb #9dc1025e def unqid(seeded=False): + "Random unique id, base64-encoded and prefixed with '_' so it's usable as an HTML id" id4 = UUID(int=random.getrandbits(128), version=4) if seeded else uuid4() res = b64encode(id4.bytes) return '_' + res.decode().rstrip('=').translate(str.maketrans('+/', '_-')) diff --git a/nbs/api/00_core.ipynb b/nbs/api/00_core.ipynb index d8827e07..a64df81e 100644 --- a/nbs/api/00_core.ipynb +++ b/nbs/api/00_core.ipynb @@ -19,6 +19,53 @@ "> The `FastHTML` subclass of `Starlette`." ] }, + { + "cell_type": "markdown", + "id": "1f8f308f", + "metadata": {}, + "source": [ + "#| export\n", + "Create an app with `app = FastHTML()`. The `@rt` decorator adds routes (`rt = app.route`), inferring what it can from the function:\n", + "\n", + " @rt\n", + " def index(): ... # GET,POST /\n", + " @rt\n", + " def foo(): ... # GET,POST /foo\n", + " @rt('/hi')\n", + " def get(): ... # GET /hi (function named after a verb handles just that verb)\n", + " @rt('/cards/{id}')\n", + " class Cards: ... # class route group: each verb-named method (get, patch, ...) becomes a handler\n", + "\n", + "The decorated function doubles as a URL builder, so links never need hardcoded paths:\n", + "\n", + " A('More', href=foo.to(a=1)) # `.to()` fills path params; the rest become query params.\n", + " Div('...', hx_get=foo) # route functions work directly as htmx attr values: hx-get=\"/foo\"\n", + "\n", + "Handlers return FT trees (from `fastcore.xml`), rendered as HTML automatically: a full page for regular requests, a bare fragment for HTMX requests. In a full page, top-level `Title`, `Meta`, `Link`, and `Style` items go in ``; the rest goes in ``. Strings are sent as HTML, dicts as JSON, and a Starlette `Response` is sent untouched. A return tuple may mix content with special items: any `HttpHeader` (e.g. from `cookie()` or `HtmxResponseHeaders()`) becomes a response header, any `BackgroundTask` runs after the response is sent, and the rest renders as content. `Redirect` picks the right redirect mechanism for HTMX vs regular requests, and `FtResponse` wraps FT content when you need the status code or headers.\n", + "\n", + "Handler parameters are filled from the request: each is looked up in path, cookie, header (snake_case names match Hyphen-Case headers), query, then form data, and cast by calling its annotation on the value. `bool`, `int`, and `date` get smart string parsing; `UploadFile` passes through. Repeated params take the last value unless annotated `list[T]`, which collects them all. A param with no annotation and no special name is ignored, with a warning. A dataclass, TypedDict, namedtuple, or any annotated class collects the whole form body; a `__from_request__` classmethod customizes construction. A missing required param is a 400, a failed cast a 404. These special names need no annotation:\n", + "\n", + " req, ws the Request / WebSocket connection\n", + " sess session dict\n", + " app the FastHTML app\n", + " state app.state\n", + " scope ASGI scope\n", + " auth scope['auth']\n", + " htmx HtmxHeaders (parsed HX-* request headers)\n", + " body raw body text\n", + " data parsed form data as a dict\n", + " api ApiReturn (API vs browser dual responses)\n", + " send websocket send function\n", + " hdrs,ftrs the per-request copies of the app's `hdrs`/`ftrs` (head and footer content)\n", + " htmlkw the per-request copy of the app's `` attrs\n", + " bodykw the per-request copy of the app's `` attrs\n", + " resp the handler's response (injected into `after` functions)\n", + "\n", + "`before`/`after` functions (app-level, with `Beforeware` skip patterns, or per-route `before=`) get the same param injection as handlers. A `before` returning a value responds with it and skips the handler (the usual auth-guard pattern).\n", + "\n", + "See the [Handling Handlers tutorial](https://www.fastht.ml/docs/ref/handlers.html.md) for worked examples of the above." + ] + }, { "cell_type": "markdown", "id": "46e2e6e8", @@ -224,6 +271,7 @@ "\n", "@dataclass\n", "class HtmxHeaders:\n", + " \"Parsed HX-* request headers\"\n", " boosted:str|None=None; current_url:str|None=None; history_restore_request:str|None=None; prompt:str|None=None\n", " request:str|None=None; request_type:str|None=None; source:str|None=None; target:str|None=None; \n", " trigger_name:str|None=None; trigger:str|None=None\n", @@ -404,7 +452,9 @@ "source": [ "#| export\n", "@dataclass\n", - "class HttpHeader: k:str;v:str" + "class HttpHeader:\n", + " \"A response header as a k,v pair\"\n", + " k:str;v:str" ] }, { @@ -692,6 +742,7 @@ "source": [ "#| export\n", "class ApiReturn:\n", + " \"Request param: call with browser and API responses; returns kwargs as JSON if the client accepts `application/json`, else `norm`\"\n", " @classmethod\n", " async def __from_request__(cls, data, req): return cls(req.headers.get('accept')=='application/json')\n", " def __init__(self, isapi=False): self.isapi = isapi\n", @@ -1126,6 +1177,7 @@ "source": [ "#| export\n", "class Beforeware:\n", + " \"Wrap a `before` function with `skip` path patterns that bypass it\"\n", " def __init__(self, f, skip=None): self.f,self.skip = f,skip or []\n", " def __repr__(self): return f'Beforeware({self.f}, skip={self.skip})'" ] @@ -1264,6 +1316,7 @@ "source": [ "#| export\n", "def signal_shutdown():\n", + " \"Return an `asyncio.Event` set when uvicorn receives a shutdown signal\"\n", " from uvicorn.main import Server\n", " event = asyncio.Event()\n", " @patch\n", @@ -1851,6 +1904,7 @@ " async def __anext__(self): return await self.gen.__anext__()\n", "\n", "class Lifespan:\n", + " \"Combine `on_startup`/`on_shutdown` callbacks and an optional lifespan generator into one context manager\"\n", " def __init__(self, startup=None, shutdown=None, ls=None):\n", " startup,shutdown = listify(startup),listify(shutdown)\n", " store_attr()\n", @@ -1877,6 +1931,7 @@ "source": [ "#| export\n", "class FastHTML(Starlette):\n", + " \"An HTML-first Starlette app: handler params filled from the request, FT returns rendered as pages or HTMX fragments\"\n", " def __init__(self, debug=False, routes=None, middleware=None, title: str = \"FastHTML page\", exception_handlers=None,\n", " on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, exts=None,\n", " before=None, after=None, surreal=True, htmx=True, htmx4=False, default_hdrs=True, sess_cls=SessionMiddleware,\n", @@ -3936,6 +3991,7 @@ "source": [ "#| export\n", "class RouteFuncs:\n", + " \"Attr-access store of named route functions (HTTP verb names excluded)\"\n", " def __init__(self): super().__setattr__('_funcs', {})\n", " def __setattr__(self, name, value): self._funcs[name] = value\n", " def __getattr__(self, name):\n", @@ -4332,6 +4388,7 @@ "source": [ "#| export\n", "def reg_re_param(m, s):\n", + " \"Register a Starlette URL convertor named `m` matching regex `s`\"\n", " cls = get_class(f'{m}Conv', sup=StringConvertor, regex=s)\n", " register_url_convertor(m, cls())" ] @@ -4417,6 +4474,7 @@ "source": [ "#| export\n", "class StaticNoCache(StaticFiles):\n", + " \"StaticFiles that sends `Cache-Control: no-cache`, so browsers revalidate on every use\"\n", " def file_response(self, *args, **kwargs):\n", " resp = super().file_response(*args, **kwargs)\n", " resp.headers.setdefault(\"Cache-Control\", \"no-cache\")\n", @@ -4651,11 +4709,11 @@ "source": [ "#| export\n", "class MiddlewareBase:\n", + " \"Base for middleware: passes non-http/ws scopes through, else returns the `HTTPConnection`\"\n", " async def __call__(self, scope, receive, send) -> None:\n", - " if scope[\"type\"] not in [\"http\", \"websocket\"]:\n", - " await self._app(scope, receive, send)\n", - " return\n", - " return HTTPConnection(scope)" + " if scope[\"type\"] in (\"http\", \"websocket\"): return HTTPConnection(scope)\n", + " await self._app(scope, receive, send)\n", + " " ] }, { @@ -4966,6 +5024,7 @@ "source": [ "#| export\n", "def unqid(seeded=False):\n", + " \"Random unique id, base64-encoded and prefixed with '_' so it's usable as an HTML id\"\n", " id4 = UUID(int=random.getrandbits(128), version=4) if seeded else uuid4()\n", " res = b64encode(id4.bytes)\n", " return '_' + res.decode().rstrip('=').translate(str.maketrans('+/', '_-'))" @@ -5120,6 +5179,13 @@ "display_name": "python3", "language": "python", "name": "python3" + }, + "solveit": { + "default_code": true, + "mode": "learning", + "use_thinking": true, + "use_tools": true, + "ver": 2 } }, "nbformat": 4,