Skip to content

Commit 195df0d

Browse files
Docs improvements (#1162)
<!-- Before submitting: - Read CONTRIBUTING.md for workflow - Read AGENTS.md for architecture (especially if AI-generated) Air's design principles: semantic APIs with clear docstrings, concise docs mindful of context window sizes, max readability, less code is better, zero config is ideal. — airwebframework.org --> ## What This is a collection of doc improvements and edits from the point of view of a new user. ## Pattern N/A ## Reviewer Focus - Consistency was applied to the examples - Updated the examples - Fixed some broken examples - Added an alternate way to pass reserved words. ## Checklist - [X] Diff contains only changes for this task — no unrelated refactoring or cleanup - [ ] Addresses exactly one issue or feature - [ ] New or changed behavior has test coverage - [ ] This is the simplest viable approach - [ ] AI provenance section removed or accurate
2 parents 44318a4 + 968bf25 commit 195df0d

12 files changed

Lines changed: 181 additions & 53 deletions

File tree

‎README.md‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,11 @@ Third-party context providers: [Code Wiki by Google](https://codewiki.google/git
113113

114114
## Two Ways to Build
115115

116-
Air gives you two paths to HTML. Start with whichever fits your workflow.
116+
Air gives you two paths to rendering HTML. Start with whichever fits your workflow.
117117

118-
### Start with HTML
118+
### 1. Start with HTML
119119

120-
Have your AI generate an HTML mockup, or write one yourself. Drop it in a template, wire it up with minimal Python:
120+
Have your AI generate an HTML mockup, or write one yourself. Drop it in a template, then wire it up with minimal Python:
121121

122122
`templates/index.html`:
123123

@@ -147,9 +147,9 @@ def index(request: air.Request):
147147
return jinja(request, name="index.html")
148148
```
149149

150-
### Start with Python
150+
### 2. Start with Python
151151

152-
Write HTML as typed Python classes. Your editor autocompletes attributes, your type checker validates nesting:
152+
Write HTML as typed Python classes. Using Python allows your editor to autocomplete attributes, and your type checker to validate nesting:
153153

154154
`main.py`:
155155

@@ -161,17 +161,21 @@ app = air.Air()
161161

162162
@app.page
163163
def index():
164-
return air.Html(air.H1("Hello, world!"))
164+
return air.Html(
165+
air.H1("Hello, world!"),
166+
)
165167
```
166168

167-
### Run either one
169+
## Running Air's Development Server
170+
171+
Either approach produces the same thing: a working web page.
172+
173+
To see the result, run the following command and open <http://127.0.0.1:8000> in your browser.
168174

169175
```sh
170176
air run
171177
```
172178

173-
Open <http://127.0.0.1:8000> to see the result. Both paths produce the same thing: a working web page.
174-
175179
## Use FastAPI Alongside Air
176180

177181
Air is powered by FastAPI. You get Air's HTML tools for your pages and FastAPI's full capabilities for your API, all in one app.

‎docs/api/dependencies.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ def get_users(is_htmx: bool = Depends(air.is_htmx_request)):
3434
# Return full page for regular requests
3535
return air.Html(
3636
[
37-
air.Head(air.Title("Users")),
37+
air.Head(
38+
air.Title("Users"),
39+
),
3840
air.Body(
3941
[
4042
air.H1("User List"),

‎docs/api/requests.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ app = air.Air()
7373
async def login(request: Request):
7474
form = await request.form()
7575
return air.layouts.mvpcss(
76-
air.Section(air.Aside({"username": form.get("username")}))
76+
air.Section(
77+
air.Aside({"username": form.get("username")}),
78+
),
7779
)
7880
```
7981

‎docs/api/routing.md‎

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
1-
Routing
1+
## Routing
22

3-
If you need to knit several Python modules with their own Air views into one, that's where Routing is used. They allow the near seamless combination of multiple Air apps into one. Larger sites are often built from multiple routers.
3+
If you need to knit several Python modules with their own Air views into one, you will need to use Routing. This allow the near seamless combination of multiple Air apps into one. Larger sites are often built from multiple routers.
44

5-
Let's imagine we have an e-commerce store with a shopping cart app. Use instantiate a `router` object using `air.AirRouter()` just as we would with `air.App()`:
5+
For this example, let's imagine we have an e-commerce store with a shopping cart app with a `cart.py` and `main.py` file.
66

7-
```python
8-
# cart.py
7+
```python title="cart.py"
98
import air
109

1110
router = air.AirRouter()
1211

1312

1413
@router.page
15-
def cart():
14+
def cart_page():
1615
return air.H1("I am a shopping cart")
1716
```
1817

19-
Then in our main page we can load that and tie it into our main `app`.
18+
Then in our main page we can load that and tie it into our `main.py` app.
2019

21-
```python
20+
```python title="main.py"
2221
import air
2322
from cart import router as cart_router
2423

@@ -31,21 +30,24 @@ def index():
3130
return air.H1("Home page")
3231
```
3332

34-
Note that the router allows sharing of sessions and other application states.
33+
`AirRouter` allows the sharing of sessions and other application states between routes.
3534

36-
In addition, we can add links through the `.url()` method available on route functions, which generates URLs programmatically:
35+
In addition, we can add links through the `.url()` method available on route functions:
3736

38-
```python
37+
```python title="main.py"
3938
import air
40-
from cart import router as cart_router, cart
39+
from cart import router as cart_router, cart_page
4140

4241
app = air.Air()
4342
app.include_router(cart_router)
4443

4544

4645
@app.page
4746
def index():
48-
return air.Div(air.H1("Home page"), air.A("View cart", href=cart.url()))
47+
return air.Div(
48+
air.H1("Home page"),
49+
air.A("View cart", href=cart_page.url()),
50+
)
4951
```
5052

5153
## Query Parameters

‎docs/index.md‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ uv add "fastapi[standard]"
129129

130130
## A Simple Example
131131

132+
### main.py
133+
132134
Create a `main.py` with:
133135

134136
```python
@@ -139,13 +141,25 @@ app = air.Air()
139141

140142
@app.get("/")
141143
async def index():
142-
return air.Html(air.H1("Hello, world!", style="color: blue;"))
144+
return air.Html(
145+
air.H1("Hello, world!", style="color: blue;"),
146+
)
143147
```
144148

145149
!!! note
146150

147151
This example uses [Air Tags](api/tags/index.md), which are Python classes that render as HTML. Air Tags are typed and documented, designed to work well with any code completion tool.
148152

153+
### Running Air
154+
155+
To run the development server, run the following command in your terminal:
156+
157+
```sh
158+
air run
159+
```
160+
161+
Open <http://127.0.0.1:8000> to see the above example running.
162+
149163
## Combining FastAPI and Air
150164

151165
Air is just a layer over FastAPI. So it is trivial to combine sophisticated HTML pages and a REST API into one app.
@@ -162,10 +176,14 @@ api = FastAPI()
162176
@app.get("/")
163177
def landing_page():
164178
return air.Html(
165-
air.Head(air.Title("Awesome SaaS")),
179+
air.Head(
180+
air.Title("Awesome SaaS"),
181+
),
166182
air.Body(
167183
air.H1("Awesome SaaS"),
168-
air.P(air.A("API Docs", target="_blank", href="/api/docs")),
184+
air.P(
185+
air.A("API Docs", target="_blank", href="/api/docs"),
186+
),
169187
),
170188
)
171189

‎docs/learn/air_tags.md‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,23 @@ renders as
169169
</script>
170170
```
171171

172+
### Passing reserved words as kwargs
173+
174+
Alternately, we can pass reserved keywords as kwargs.
175+
176+
```python
177+
air.Label("Email", **{"class": "plain", "for": "email"})
178+
```
179+
180+
Renders as:
181+
182+
```html
183+
<label class="plain" for="email">Email</label>
184+
```
185+
172186
### Attributes starting with special characters
173187

174-
To get around that in Python we can't begin function arguments with special characters, we lean into how **Air Tags** is kwargs friendly.
188+
To get around that in Python we can't begin function arguments with special characters, we lean into how **Air Tags** are kwarg-friendly.
175189

176190
```python
177191
air.P("Hello", class_="plain", **{"@data": 6})
@@ -275,7 +289,11 @@ Subclasses are not the only way to create custom Air Tags. You can also use func
275289

276290
```python
277291
def card(*content, header: str, footer: str):
278-
return air.Article(air.Header(header), *content, air.Footer(footer))
292+
return air.Article(
293+
air.Header(header),
294+
*content,
295+
air.Footer(footer),
296+
)
279297
```
280298

281299
We can use this function to create a card:
@@ -365,5 +383,12 @@ air.BaseTag.from_html_to_source("""
365383
This generates:
366384

367385
```python
368-
air.Html(air.Body(air.Main(air.H1("Hello, World", class_="header"))))
386+
air.Html(
387+
air.Head(),
388+
air.Body(
389+
air.Main(
390+
air.H1('Hello, World', class_='header'),
391+
),
392+
),
393+
)
369394
```

‎docs/learn/airmodel.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,16 @@ async def submit_contact(request: air.Request):
264264
form = await ContactForm.from_request(request)
265265
if form.is_valid:
266266
await ContactMessage.create(**form.save_data())
267-
return air.Html(air.H1("Message sent"))
268-
return air.Html(air.Form(form.render(), method="post", action="/contact"))
267+
return air.Html(
268+
air.H1("Message sent"),
269+
)
270+
return air.Html(
271+
air.Form(
272+
form.render(),
273+
method="post",
274+
action="/contact"
275+
),
276+
)
269277
```
270278

271279
`AirForm[ContactMessage]` gives you type-safe validated data. `ContactMessage.create()` writes it to PostgreSQL. Your editor knows the types at every step.

‎docs/learn/cookbook/authentication.md‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ async def index(request: air.Request):
2121
action = air.Tags(
2222
air.H1(request.session["username"]),
2323
air.P(request.session.get("logged_in_at")),
24-
air.P(air.A("Logout", href="/logout")),
24+
air.P(
25+
air.A("Logout", href="/logout"),
26+
),
2527
)
2628
else:
2729
# login the user
@@ -111,7 +113,9 @@ def require_login(request: air.Request):
111113
async def dashboard(request: air.Request, user=Depends(require_login)):
112114
return air.layouts.mvpcss(
113115
air.H1(f"Dashboard for {request.session['user']['username']}"),
114-
air.P(air.A("Logout", href="/logout")),
116+
air.P(
117+
air.A("Logout", href="/logout")
118+
),
115119
)
116120
```
117121

@@ -143,7 +147,12 @@ def require_login(request: air.Request):
143147
# --- Routes ---
144148
@app.page
145149
async def index(request: air.Request):
146-
return air.layouts.mvpcss(air.H1("Landing page"), air.P(air.A("Dashboard", href="/dashboard")))
150+
return air.layouts.mvpcss(
151+
air.H1("Landing page"),
152+
air.P(
153+
air.A("Dashboard", href="/dashboard"),
154+
),
155+
)
147156

148157

149158
@app.page
@@ -179,7 +188,9 @@ async def login():
179188
async def dashboard(request: air.Request, user=Depends(require_login)):
180189
return air.layouts.mvpcss(
181190
air.H1(f"Dashboard for {request.session['user']['username']}"),
182-
air.P(air.A("Logout", href="/logout")),
191+
air.P(
192+
air.A("Logout", href="/logout"),
193+
),
183194
)
184195

185196

‎docs/learn/cookbook/bigger-applications.md‎

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ app = air.Air()
1818

1919
@app.page
2020
def index():
21-
return air.layouts.mvpcss(air.H1("Avatar Data"), air.P(air.A("Dashboard", href="/dashboard")))
21+
return air.layouts.mvpcss(
22+
air.H1("Avatar Data"),
23+
air.P(
24+
air.A("Dashboard", href="/dashboard"),
25+
),
26+
)
2227
```
2328

2429
Now for the dashboard, instead of using the typical `air.Air` tool to instantiate our application, we use `air.AirRouter` like so:
@@ -31,7 +36,12 @@ router = air.AirRouter()
3136

3237
@router.page
3338
def dashboard():
34-
return air.layouts.mvpcss(air.H1("Avatar Data Dashboard"), air.P(air.A("<- Home", href="/")))
39+
return air.layouts.mvpcss(
40+
air.H1("Avatar Data Dashboard"),
41+
air.P(
42+
air.A("<- Home", href="/"),
43+
),
44+
)
3545
```
3646

3747
Now if we go back to our `main.py` we can use the `app.include_router()` method to include the dashboard in our app:
@@ -47,7 +57,10 @@ app.include_router(router)
4757
@app.page
4858
def index():
4959
return air.layouts.mvpcss(
50-
air.H1("Avatar Data"), air.P(air.A("Dashboard", href="/dashboard"))
60+
air.H1("Avatar Data"),
61+
air.P(
62+
air.A("Dashboard", href="/dashboard"),
63+
),
5164
)
5265
```
5366

@@ -73,7 +86,12 @@ app = air.Air(title="Air")
7386

7487
@app.page
7588
def index():
76-
return air.layouts.mvpcss(air.H1("Air landing page"), air.P(air.A("Shop", href="/shop")))
89+
return air.layouts.mvpcss(
90+
air.H1("Air landing page"),
91+
air.P(
92+
air.A("Shop", href="/shop"),
93+
),
94+
)
7795

7896

7997
# Creating a separate app for the shop,
@@ -83,7 +101,9 @@ shop = air.Air(title="Air shop")
83101

84102
@shop.page
85103
def index():
86-
return air.layouts.mvpcss(air.H1("Shop for Air things"))
104+
return air.layouts.mvpcss(
105+
air.H1("Shop for Air things"),
106+
)
87107

88108

89109
# Mount the shop app to the main app
@@ -107,10 +127,14 @@ app = air.Air()
107127
@app.get("/")
108128
def landing_page():
109129
return air.Html(
110-
air.Head(air.Title("Awesome SaaS")),
130+
air.Head(
131+
air.Title("Awesome SaaS"),
132+
),
111133
air.Body(
112134
air.H1("Awesome SaaS"),
113-
air.P(air.A("API Docs", target="_blank", href="/api/docs")),
135+
air.P(
136+
air.A("API Docs", target="_blank", href="/api/docs"),
137+
),
114138
),
115139
)
116140

0 commit comments

Comments
 (0)