Skip to content

Commit aa4b256

Browse files
committed
chore(codegen): use alphabetical order in the generated API
- Removes scripts/codegen/legacy_order.json and the code that applied it. - Classes, members, keyword-only parameters, events and TypedDicts are in alphabetical order; a class comes after the class that it extends or uses. Positional parameters, union members and TypedDict fields keep the docs order. Docstrings are wrapped after member links are rendered. compare_api.py shows no other difference. - The roll skill, ROLLING.md, CONTRIBUTING.md and update_api.sh now ask for a microsoft/playwright checkout used only for rolls, on the exact release-<new> branch, and list the checks that it is on that branch, clean, and up to date with origin.
1 parent 478282f commit aa4b256

10 files changed

Lines changed: 35523 additions & 36005 deletions

File tree

‎.claude/skills/playwright-roll/SKILL.md‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,30 @@ The upstream documentation source of truth is `docs/src/api/*.md` in the playwri
3939

4040
### 2. Bump the driver pin, download it, and generate api.json
4141

42-
You need a nearby `microsoft/playwright` checkout for the docs walk and for
43-
codegen. Point `PW_SRC_DIR` at it and check out the release branch for the new
44-
version there (the branch has the release's docs plus any cherry-picks):
42+
The docs walk and codegen read the upstream docs from a `microsoft/playwright`
43+
checkout at `$PW_SRC_DIR`. **Use a separate checkout that exists only for
44+
rolls, on the exact release branch you roll to**: `release-<new>`, the
45+
`major.minor` of the new `DRIVER_VERSION` (the branch has the release's docs
46+
plus any cherry-picks). Never point `PW_SRC_DIR` at a checkout that someone
47+
works in: on `main` or a feature branch the docs differ, and the generator
48+
silently emits that API instead of the release's.
4549

4650
```sh
47-
export PW_SRC_DIR=../playwright
48-
git -C "$PW_SRC_DIR" fetch origin 'release-*:release-*'
49-
git -C "$PW_SRC_DIR" checkout release-<new> # e.g. release-1.62
51+
export PW_SRC_DIR=../playwright-for-python # the roll checkout, nothing else
52+
# First roll on this machine: clone it (no npm install needed).
53+
git clone https://github.com/microsoft/playwright.git "$PW_SRC_DIR"
54+
# Every roll: move it to the release branch.
55+
git -C "$PW_SRC_DIR" fetch origin
56+
git -C "$PW_SRC_DIR" checkout release-<new> # e.g. release-1.62
57+
git -C "$PW_SRC_DIR" merge --ff-only origin/release-<new>
58+
```
59+
60+
Check the checkout before you generate, and again before you commit:
61+
62+
```sh
63+
git -C "$PW_SRC_DIR" rev-parse --abbrev-ref HEAD # must print release-<new>
64+
git -C "$PW_SRC_DIR" status --porcelain # must print nothing
65+
git -C "$PW_SRC_DIR" rev-parse HEAD origin/release-<new> # both lines must be equal
5066
```
5167

5268
Then bump the pins and assemble the driver:
@@ -73,7 +89,7 @@ the walk.
7389

7490
### 3. Identify the commit range
7591

76-
Use the nearby `microsoft/playwright` checkout at `$PW_SRC_DIR` (from step 2).
92+
Use the roll checkout at `$PW_SRC_DIR` (from step 2).
7793
Bring it up to date and ensure release branches/tags are present before walking
7894
the range:
7995

@@ -221,7 +237,7 @@ Tick the box in `/tmp/roll-<new>-commits.md` with one line: `[x] <sha> <subject>
221237
### 5. Regenerate
222238

223239
```sh
224-
PW_SRC_DIR=../playwright ./scripts/update_api.sh # PW_SRC_DIR already exported in step 2
240+
./scripts/update_api.sh # uses PW_SRC_DIR from step 2; run the checkout checks there first
225241
```
226242

227243
The script, for `sync` and then `async`:
@@ -250,10 +266,10 @@ Failure modes and fixes:
250266
| mypy: incompatible argument type in `_generated.py` | The docs type changed, or `_impl/` declares a narrower type. | Match the type in `_impl/`, or record a `typeOverrides` entry for a justified divergence. |
251267
| generator: `No Python name for the struct at X.y.z` | A new inline `[Object]` with no `* alias:` in the docs. | Name it: `alias-python` upstream, or `structNamesByPlace` / `structRenames`. |
252268
| generator: `The struct X has different fields at A and B` | Two places in the docs share one struct name, but their fields differ. | Give one place its own name in `structNamesByPlace`, or fix the docs. |
269+
| `_generated.py` gains or loses members that the commit walk never mentioned | `PW_SRC_DIR` is not on `release-<new>` (for example on `main` or a feature branch), or is behind `origin/release-<new>`. | Run the checkout checks from step 2, fix the checkout, regenerate. |
253270
| generator: `Unknown docs type "T" at X.y` | A docs type the type table in `generate_api.js` (`toPy`) does not know. | Add it to `toPy`. |
254271
| generator: `required parameter z follows an optional one` | The docs order params so that Python would need a default before a non-default. | Add a `positional` entry with the Python order. |
255272
| generator: `the entry "…" no longer matches anything; remove it` | An upstream change made a rules entry unnecessary (or renamed what it points at). | Delete it, or re-key it to the new docs name. |
256-
| generator: `legacy_order.json: … has no …; remove it` | A member, parameter or TypedDict in the temporary legacy order was removed or renamed upstream. | Delete that name from `scripts/codegen/legacy_order.json`. New members need no entry; they go last. |
257273
| `pyright … reportInconsistentOverload` (single-event class) | A class gained its first event, so there is one `Literal[…]` overload + impl, which pyright wants ≥2 of. | The generator already handles this — `emitEvents` adds a second `@typing.overload` with `event: str` when a class has exactly one event. |
258274
| `pre-commit` keeps reformatting `_generated.py` on each run | First run after regen always reformats once; rerun until idle. | `pre-commit run --all-files` to settle. |
259275
| A kwarg in `_impl/` that the docs don't mention (e.g. legacy name from a doc rename) | Nothing fails: the generator simply never passes it. | If the param is positional with no default, just rename it in `_impl/`. Check `packages/protocol/spec/*.yml` and the server dispatcher — if the wire field renamed too, also update the channel-send dict key. Only use `extraOptions` for genuinely hidden internal kwargs (`default_browser_type`). |
@@ -281,6 +297,8 @@ For each PORT, add one async test and a matching sync test. Conventions:
281297

282298
### 8. Final verification
283299

300+
Repeat the checkout checks from step 2: the committed `_generated.py` must come from `release-<new>`.
301+
284302
```sh
285303
pre-commit run --all-files
286304
mypy playwright # 2 pre-existing errors in _json_pipe.py and _artifact.py are unrelated

‎CLAUDE.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ Python bindings for [Playwright](https://playwright.dev). The Python client talk
1212
- `playwright/async_api/_generated.py`, `playwright/sync_api/_generated.py`, and the public TypedDicts in `playwright/_impl/_api_structures.py` — **auto-generated** from the upstream API docs. Never edit by hand; rerun `./scripts/update_api.sh` after a roll or after changing `scripts/codegen/`.
1313
- `scripts/codegen/generate_api.js` — the generator. A Node.js script that loads the upstream doc parser (`utils/doclint/api_parser.js`) from `$PW_SRC_DIR`, filters the docs for Python, and writes the whole public API: signatures, types, docstrings, event overloads, the calls into `_impl/`, and the TypedDicts. It does not read `_impl/`.
1414
- `scripts/codegen/python_rules.js` — the Python facts that the docs do not contain (positional parameters, struct names and `total=`, type overrides, hidden members). Policy: the shipped Python API wins; each entry is a candidate for an upstream docs fix, and the generator fails on entries that no longer match anything. Every entry needs a one-line rationale comment above it.
15-
- `scripts/codegen/legacy_order.json` — **temporary**. The old generator's order of classes, members, keyword-only parameters and union members, and the hand-written order of the TypedDicts. The generator applies it, and more old habits marked `TEMPORARY` in the generator, so that the switch to the docs generator has a small diff. New members go last. If the generator reports an entry that no longer matches, remove the entry. A follow-up change deletes the file and the `TEMPORARY` code.
1615
- `scripts/codegen/compare_api.py` — compares two generated files (signatures, annotations, call bodies, docstrings) and lists every difference. Use it to check that a generator change does not change the API.
1716
- `tests/async/`, `tests/sync/` — pytest suites. Most new tests are added to the async file with a sync mirror.
1817
- `DRIVER_VERSION` — the single source of truth for which Playwright release the driver is assembled from (one line, the `playwright-core` npm version, e.g. `1.61.0`, no `v` prefix). Read by `setup.py`, `scripts/build_driver.py`, and CI. The wheel build downloads `playwright-core` at this version from npm plus the matching Node.js binary and assembles the per-platform bundles — no source build. The version is baked into the staged bundle filenames (`driver/playwright-<version>-<suffix>.zip`), so it doubles as the build cache key.
1918
- `NODE_VERSION` — the Node.js version bundled with the driver (one line, e.g. `24.16.0`). Maintained at roll time by `scripts/update_node_version.py` (latest LTS, mirroring upstream's `utils/build/update-playwright-node.mjs`).
2019
- `scripts/build_driver.py` — assembles the per-platform driver bundles into `driver/` by downloading the `playwright-core` npm package (`DRIVER_VERSION`) and the official Node.js binaries (`NODE_VERSION`). Fetches `playwright-core` with `npm pack` (needs Node.js/npm on PATH; honours a root `.npmrc`) and the Node.js binaries over plain HTTP; invoked from `setup.py`'s `bdist_wheel` with the target platform's suffix (no arg builds all six).
21-
- The API docs are **not** shipped in the bundle. `scripts/update_api.sh` reads them from a nearby `microsoft/playwright` checkout (`$PW_SRC_DIR`, at the release branch for `DRIVER_VERSION`). Needed only when regenerating the API, never at runtime.
20+
- The API docs are **not** shipped in the bundle. `scripts/update_api.sh` reads them from a separate `microsoft/playwright` checkout (`$PW_SRC_DIR`) on the exact release branch for `DRIVER_VERSION`, never from a checkout on `main` or a feature branch. Needed only when regenerating the API, never at runtime.
2221
- `ROLLING.md`, `CONTRIBUTING.md` — human-facing setup and roll docs.
2322

2423
## Setup

‎CONTRIBUTING.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,14 @@ For more details look at the [CI configuration](./.github/workflows/ci.yml).
7777

7878
`update_api.sh` generates `playwright/{async,sync}_api/_generated.py` and the
7979
public TypedDicts in `playwright/_impl/_api_structures.py` from the API docs in
80-
a nearby `microsoft/playwright` checkout (at the release branch matching
81-
`DRIVER_VERSION`); point `PW_SRC_DIR` at it. The generator is
80+
a separate `microsoft/playwright` checkout on the exact release branch for
81+
`DRIVER_VERSION` (for example `release-1.64`); point `PW_SRC_DIR` at it. The
82+
generator is
8283
`scripts/codegen/generate_api.js`; Python facts that the docs do not contain
8384
live in `scripts/codegen/python_rules.js`.
8485

8586
```bash
86-
PW_SRC_DIR=../playwright ./scripts/update_api.sh
87+
PW_SRC_DIR=../playwright-for-python ./scripts/update_api.sh
8788
pre-commit run --all-files
8889
```
8990

‎ROLLING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pip install -e .
1313
```
1414
* change the driver pin in `DRIVER_VERSION` (the `playwright-core` npm version, e.g. `1.61.0`) and refresh `NODE_VERSION`: `python scripts/update_node_version.py`
1515
* download the new driver: `python -m build --wheel` (fetches `playwright-core` via `npm pack` + the matching Node.js binary and assembles the bundle; no source build).
16-
* generate API from the docs (needs a nearby `microsoft/playwright` checkout at the release branch for the new version, e.g. `release-1.64`): `PW_SRC_DIR=../playwright ./scripts/update_api.sh`
16+
* generate API from the docs. This needs a separate `microsoft/playwright` checkout used only for rolls, on the exact release branch for the new version (e.g. `release-1.64`), clean and up to date with `origin`: `PW_SRC_DIR=../playwright-for-python ./scripts/update_api.sh`
1717
* run `mypy playwright`: every error in `_generated.py` points at an implementation method in `playwright/_impl/` that lacks a new member or option. Implement it there, or record the difference in `scripts/codegen/python_rules.js`.
1818
* commit changes & send PR
1919
* wait for bots to pass & merge the PR

‎playwright/_impl/_api_structures.py‎

Lines changed: 98 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@
1818
from typing import Any, Dict, List, Literal, Optional, Sequence, TypedDict, Union
1919

2020

21+
class BrowserBindResult(TypedDict):
22+
endpoint: str
23+
24+
25+
class ClientCertificate(TypedDict, total=False):
26+
origin: str
27+
certPath: Optional[Union[str, Path]]
28+
cert: Optional[bytes]
29+
keyPath: Optional[Union[str, Path]]
30+
key: Optional[bytes]
31+
pfxPath: Optional[Union[str, Path]]
32+
pfx: Optional[bytes]
33+
passphrase: Optional[str]
34+
35+
2136
class Cookie(TypedDict, total=False):
2237
name: str
2338
value: str
@@ -30,28 +45,28 @@ class Cookie(TypedDict, total=False):
3045
partitionKey: Optional[str]
3146

3247

33-
class StorageStateCookie(TypedDict, total=False):
34-
name: str
35-
value: str
36-
domain: str
37-
path: str
38-
expires: float
39-
httpOnly: bool
40-
secure: bool
41-
sameSite: Literal["Lax", "None", "Strict"]
48+
class DebuggerLocation(TypedDict):
49+
file: str
50+
line: Optional[int]
51+
column: Optional[int]
4252

4353

44-
class SetCookieParam(TypedDict, total=False):
54+
class DebuggerPausedDetails(TypedDict):
55+
location: DebuggerLocation
56+
title: str
57+
58+
59+
class FilePayload(TypedDict):
4560
name: str
46-
value: str
47-
url: Optional[str]
48-
domain: Optional[str]
49-
path: Optional[str]
50-
expires: Optional[float]
51-
httpOnly: Optional[bool]
52-
secure: Optional[bool]
53-
sameSite: Optional[Literal["Lax", "None", "Strict"]]
54-
partitionKey: Optional[str]
61+
mimeType: str
62+
buffer: bytes
63+
64+
65+
class DropPayload(TypedDict, total=False):
66+
files: Optional[
67+
Union[str, Path, Sequence[Union[str, Path]], FilePayload, Sequence[FilePayload]]
68+
]
69+
data: Optional[Dict[str, str]]
5570

5671

5772
class FloatRect(TypedDict):
@@ -79,6 +94,11 @@ class LocalStorageEntry(TypedDict):
7994
value: str
8095

8196

97+
class NameValue(TypedDict):
98+
name: str
99+
value: str
100+
101+
82102
class OriginState(TypedDict):
83103
origin: str
84104
localStorage: List[LocalStorageEntry]
@@ -103,20 +123,16 @@ class ProxySettings(TypedDict, total=False):
103123
password: Optional[str]
104124

105125

106-
class StorageState(TypedDict, total=False):
107-
cookies: List[StorageStateCookie]
108-
origins: List[OriginState]
126+
class RemoteAddr(TypedDict):
127+
ipAddress: str
128+
port: int
109129

110130

111-
class ClientCertificate(TypedDict, total=False):
112-
origin: str
113-
certPath: Optional[Union[str, Path]]
114-
cert: Optional[bytes]
115-
keyPath: Optional[Union[str, Path]]
116-
key: Optional[bytes]
117-
pfxPath: Optional[Union[str, Path]]
118-
pfx: Optional[bytes]
119-
passphrase: Optional[str]
131+
class RequestSizes(TypedDict):
132+
requestBodySize: int
133+
requestHeadersSize: int
134+
responseBodySize: int
135+
responseHeadersSize: int
120136

121137

122138
class ResourceTiming(TypedDict):
@@ -131,52 +147,22 @@ class ResourceTiming(TypedDict):
131147
responseEnd: float
132148

133149

134-
class RequestSizes(TypedDict):
135-
requestBodySize: int
136-
requestHeadersSize: int
137-
responseBodySize: int
138-
responseHeadersSize: int
139-
140-
141-
class ViewportSize(TypedDict):
142-
width: int
143-
height: int
144-
145-
146-
class SourceLocation(TypedDict):
147-
url: str
148-
line: int
149-
column: int
150-
lineNumber: int
151-
columnNumber: int
152-
153-
154-
class WebErrorLocation(TypedDict):
155-
url: str
156-
line: int
157-
column: int
158-
159-
160-
class FilePayload(TypedDict):
161-
name: str
162-
mimeType: str
163-
buffer: bytes
164-
165-
166-
class DropPayload(TypedDict, total=False):
167-
files: Optional[
168-
Union[str, Path, Sequence[Union[str, Path]], FilePayload, Sequence[FilePayload]]
169-
]
170-
data: Optional[Dict[str, str]]
150+
class ScreencastActionStyle(TypedDict, total=False):
151+
point: Optional[str]
152+
highlight: Optional[str]
153+
title: Optional[str]
171154

172155

173-
class RemoteAddr(TypedDict):
174-
ipAddress: str
175-
port: int
156+
class ScreencastFrame(TypedDict):
157+
data: bytes
158+
timestamp: float
159+
viewportWidth: int
160+
viewportHeight: int
176161

177162

178-
class BrowserBindResult(TypedDict):
179-
endpoint: str
163+
class ScreencastSize(TypedDict):
164+
width: int
165+
height: int
180166

181167

182168
class SecurityDetails(TypedDict):
@@ -187,39 +173,54 @@ class SecurityDetails(TypedDict):
187173
validTo: Optional[float]
188174

189175

190-
class NameValue(TypedDict):
176+
class SetCookieParam(TypedDict, total=False):
191177
name: str
192178
value: str
179+
url: Optional[str]
180+
domain: Optional[str]
181+
path: Optional[str]
182+
expires: Optional[float]
183+
httpOnly: Optional[bool]
184+
secure: Optional[bool]
185+
sameSite: Optional[Literal["Lax", "None", "Strict"]]
186+
partitionKey: Optional[str]
193187

194188

195-
class TracingGroupLocation(TypedDict):
196-
file: str
197-
line: Optional[int]
198-
column: Optional[int]
189+
class SourceLocation(TypedDict):
190+
url: str
191+
line: int
192+
column: int
193+
lineNumber: int
194+
columnNumber: int
199195

200196

201-
class DebuggerLocation(TypedDict):
197+
class StorageStateCookie(TypedDict, total=False):
198+
name: str
199+
value: str
200+
domain: str
201+
path: str
202+
expires: float
203+
httpOnly: bool
204+
secure: bool
205+
sameSite: Literal["Lax", "None", "Strict"]
206+
207+
208+
class StorageState(TypedDict, total=False):
209+
cookies: List[StorageStateCookie]
210+
origins: List[OriginState]
211+
212+
213+
class TracingGroupLocation(TypedDict):
202214
file: str
203215
line: Optional[int]
204216
column: Optional[int]
205217

206218

207-
class DebuggerPausedDetails(TypedDict):
208-
location: DebuggerLocation
209-
title: str
210-
211-
212-
class ScreencastSize(TypedDict):
219+
class ViewportSize(TypedDict):
213220
width: int
214221
height: int
215222

216223

217-
class ScreencastActionStyle(TypedDict, total=False):
218-
point: Optional[str]
219-
highlight: Optional[str]
220-
title: Optional[str]
221-
222-
223224
class VirtualCredential(TypedDict):
224225
id: str
225226
rpId: str
@@ -229,6 +230,12 @@ class VirtualCredential(TypedDict):
229230
signCount: int
230231

231232

233+
class WebErrorLocation(TypedDict):
234+
url: str
235+
line: int
236+
column: int
237+
238+
232239
class WebMCPToolAnnotations(TypedDict, total=False):
233240
readOnly: Optional[bool]
234241
untrustedContent: Optional[bool]
@@ -240,10 +247,3 @@ class WebMCPTool(TypedDict, total=False):
240247
description: str
241248
inputSchema: Optional[Any]
242249
annotations: Optional[WebMCPToolAnnotations]
243-
244-
245-
class ScreencastFrame(TypedDict):
246-
data: bytes
247-
timestamp: float
248-
viewportWidth: int
249-
viewportHeight: int

0 commit comments

Comments
 (0)