You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 478282f
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: .claude/skills/playwright-roll/SKILL.md
+10-7Lines changed: 10 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -162,7 +162,7 @@ Implement the change in `playwright/_impl/<module>.py`. Use the upstream JS impl
162
162
|`(await this._channel.foo()).value`|`await self._channel.send("foo", None)` (Python's `send()` auto-unwraps single-key responses; only call `send_return_as_dict` when the protocol returns multiple keys.) |
163
163
|`(await this._channel.foo()).artifact` (multi-key, may be empty) |`result = await self._channel.send_return_as_dict("foo", None); (result or {}).get("artifact")` — `send_return_as_dict` returns **`None`** (not `{}`) when the protocol response carries no fields. |
164
164
|`try { ... } catch (e) { if (isTargetClosedError(e)) return; throw e; }`|`try: ...; except Exception as e: if is_target_closed_error(e): return; raise` (import from `playwright._impl._errors`) |
165
-
| Inline `[Object]` return like `{endpoint: string}`| A `TypedDict` in `playwright/_impl/_api_structures.py`. The generator names the struct from its docs `* alias:` (or a `structRenames` / `structNamesByPlace` entry in `python_rules.js`) and imports that name from `_api_structures.py`. See `RemoteAddr`, `BrowserBindResult`, `DebuggerPausedDetails`. |
165
+
| Inline `[Object]` return like `{endpoint: string}`| A `TypedDict` in `playwright/_impl/_api_structures.py`, which the generator writes. It names the struct from its docs `* alias:` (or a `structRenames` / `structNamesByPlace` entry in `python_rules.js`). See `RemoteAddr`, `BrowserBindResult`, `DebuggerPausedDetails`. |
166
166
|`binary` event/return field | The Python channel layer hands you a base64 string. Decode with `base64.b64decode(value)` before exposing as `bytes`. See `Screencast._dispatch_frame`. |
167
167
168
168
When implementing a new ChannelOwner subclass (one constructed by the protocol with `(parent, type, guid, initializer)`):
@@ -172,8 +172,9 @@ When implementing a new ChannelOwner subclass (one constructed by the protocol w
172
172
When implementing a non-ChannelOwner wrapper class (a plain class that holds a Page/Context reference, like `Screencast`, `Clock`):
173
173
- Set `self._loop = parent._loop` and `self._dispatcher_fiber = parent._dispatcher_fiber` in `__init__`. The generated `AsyncBase`/`SyncBase` wrappers read these; missing them gives `AttributeError: 'X' object has no attribute '_loop'` at first use.
174
174
175
-
When adding a new TypedDict in `_api_structures.py`:
176
-
- Its name must match what the generator emits: the struct's `* alias:` in the docs, or a `structRenames` / `structNamesByPlace` entry in `python_rules.js`. The generator fails with "No Python name for the struct at …" until one exists.
175
+
When the docs add a new struct (an inline `[Object]` with named fields):
176
+
- The generator writes its TypedDict into `_api_structures.py`. Never edit that file by hand; internal types go in `_internal_structures.py`.
177
+
- It needs a Python name: the struct's `* alias:` in the docs, or a `structRenames` / `structNamesByPlace` entry in `python_rules.js`. The generator fails with "No Python name for the struct at …" until one exists. Nested structs need names too.
177
178
- Re-export it from both `playwright/async_api/__init__.py` and `playwright/sync_api/__init__.py`: assignment line plus an entry in `__all__`. Same pattern as `ViewportSize`, `RemoteAddr`.
178
179
179
180
If the new API was previously recorded in `python_rules.js` (for example in `skipArgs`), **remove that entry** when implementing it. The generator fails on entries that no longer match anything, so stale entries cannot pile up.
@@ -203,7 +204,7 @@ The generator fails with "the entry … no longer matches anything; remove it" w
203
204
204
205
- "Internal tooling" classes/methods whose `langs` field is empty (`Screencast.*`, `Debugger.*`, `Page.pick_locator`, `BrowserContext.debugger`, `Browser.bind/unbind`, `Page.{clear,console}_*`). The 1.59 roll suppressed all of these initially, then had to walk every one back. Verify `langs` first.
205
206
- A renamed positional parameter (mypy: unexpected keyword argument `newName` for the impl method). Just rename in `_impl/` and update the channel-send dict key.
206
-
- A struct return the generator cannot name ("No Python name for the struct at …"). Add a TypedDict and name it — don't type it `Dict`.
207
+
- A struct return the generator cannot name ("No Python name for the struct at …"). Name it, so that the generator writes a TypedDict — don't type it `Dict`.
207
208
208
209
#### N/A
209
210
@@ -247,11 +248,12 @@ Failure modes and fixes:
247
248
| mypy: `"XImpl" has no attribute "y"` in `_generated.py`| The docs have a new member `X.y`; `_impl/` does not. | PORT it. |
248
249
| mypy: `Unexpected keyword argument "z" for "y" of "XImpl"`| New parameter or option on an existing method. | Add the kwarg (docs camelCase name) in `_impl/`. If Python must not have it, `skipArgs`. |
249
250
| 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. |
250
-
| generator: `No Python name for the struct at X.y.z`| A new inline `[Object]` with no `* alias:` in the docs. | Add the TypedDict and name it: `alias-python` upstream, or `structNamesByPlace` / `structRenames`. |
251
+
| 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`. |
252
+
| 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. |
251
253
| 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`. |
252
254
| 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. |
253
255
| 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. |
254
-
| generator: `legacy_order.json: … has no …; remove it`| A memberor parameter 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. |
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. |
255
257
|`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. |
256
258
|`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. |
257
259
| 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`). |
@@ -303,6 +305,7 @@ All keys use docs names: `Class.member` or `Class.member.arg`, with the upstream
|`structTotal`|`total=` of a TypedDict, where it differs from "total=False if any field is optional". |
306
309
|`implModules`|`_impl/` module of classes not in `_<snake_case name>.py`. |
307
310
308
311
## Tips & gotchas
@@ -313,7 +316,7 @@ All keys use docs names: `Class.member` or `Class.member.arg`, with the upstream
313
316
-**Watch for revert pairs in the same range.** 1.59 added and reverted `Browser.isRemote` (#39613 / #39620) inside the same release. Walking chronologically lets you skip the add when you see the revert later.
314
317
-**Watch for rename-revert pairs.** 1.59 had `Locator.normalize` → `Locator.toCode` (#39648) → `Locator.normalize` (#39754). Final state wins; only port the last.
315
318
-**Doc renames almost always include a wire-protocol rename.** Whenever you see `### param: X.y.oldName` → `### param: X.y.newName` in a doc commit, also `git -C "$PW_SRC_DIR" show <sha> -- packages/protocol/spec/` and the corresponding `*Dispatcher.ts` file. If the wire field changed too, the channel-send dict key in `_impl/` must change. Suppressing the doc-side mismatch is hiding a real bug — the previous Python code is silently sending an unknown field that the new server ignores.
316
-
-**TypedDicts beat `Dict[str, X]` for any structured return.** When the docs describe a return as `[Object]` with named fields, define a `TypedDict`in`_api_structures.py`, name it for the generator, and re-export it from both public `__init__.py` files. Zero runtime cost (it's still a `dict`), and the docstrings expand its fields from the docs.
319
+
-**TypedDicts beat `Dict[str, X]` for any structured return.** When the docs describe a return as `[Object]` with named fields, name the struct for the generator, which writes its `TypedDict`into`_api_structures.py`, and re-export it from both public `__init__.py` files. Zero runtime cost (it's still a `dict`), and the docstrings expand its fields from the docs.
317
320
-**Positional renames are free.** A param with no default before any `*` separator is positional-or-keyword in Python, but realistic call sites pass it positionally. Renaming such a param doesn't break callers.
318
321
-**The "Backport changes" GitHub issue can be misleading.** In the 1.59 roll its checkboxes were all marked `[x]` with annotations like "✅ IMPLEMENTED", but several of those features had not actually been merged into the Python port. Trust the `docs/src/api/` walk over the issue.
319
322
-**The docs parser has quirks.** In a type expression, everything after a function's `:` is its return type, so `[function]([A]):[any]|[B]` is one function returning `any|B`. Put a function with a return type last in a union. The parser also drops the second argument of a function type when the first is a union (see the `WebSocketRoute.onClose.handler` override, which builds the type by hand). And when a `### param:` header name differs from the backticked name on its type line, the header name is the docs name and the type-line name becomes the alias (see `argNames`).
Copy file name to clipboardExpand all lines: CLAUDE.md
+5-5Lines changed: 5 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,11 +8,11 @@ Python bindings for [Playwright](https://playwright.dev). The Python client talk
8
8
9
9
## Layout
10
10
11
-
-`playwright/_impl/` — hand-written client implementation (one module per object: `_browser.py`, `_page.py`, `_locator.py`, `_network.py`, etc.). Edit these to add or change behavior.
12
-
-`playwright/async_api/_generated.py`, `playwright/sync_api/_generated.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/`.
13
-
-`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, and the calls into `_impl/`. It does not read `_impl/`.
14
-
-`scripts/codegen/python_rules.js` — the Python facts that the docs do not contain (positional parameters, struct names, 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. 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.
11
+
-`playwright/_impl/` — hand-written client implementation (one module per object: `_browser.py`, `_page.py`, `_locator.py`, `_network.py`, etc.). Edit these to add or change behavior. Internal types are in `_internal_structures.py`.
12
+
-`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/`.
13
+
-`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/`.
14
+
-`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.
16
16
-`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.
17
17
-`tests/async/`, `tests/sync/` — pytest suites. Most new tests are added to the async file with a sync mirror.
18
18
-`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.
0 commit comments