Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions .claude/skills/playwright-roll/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,30 @@ The upstream documentation source of truth is `docs/src/api/*.md` in the playwri

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

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

```sh
export PW_SRC_DIR=../playwright
git -C "$PW_SRC_DIR" fetch origin 'release-*:release-*'
git -C "$PW_SRC_DIR" checkout release-<new> # e.g. release-1.62
export PW_SRC_DIR=../playwright-for-python # the roll checkout, nothing else
# First roll on this machine: clone it (no npm install needed).
git clone https://github.com/microsoft/playwright.git "$PW_SRC_DIR"
# Every roll: move it to the release branch.
git -C "$PW_SRC_DIR" fetch origin
git -C "$PW_SRC_DIR" checkout release-<new> # e.g. release-1.62
git -C "$PW_SRC_DIR" merge --ff-only origin/release-<new>
```

Check the checkout before you generate, and again before you commit:

```sh
git -C "$PW_SRC_DIR" rev-parse --abbrev-ref HEAD # must print release-<new>
git -C "$PW_SRC_DIR" status --porcelain # must print nothing
git -C "$PW_SRC_DIR" rev-parse HEAD origin/release-<new> # both lines must be equal
```

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

### 3. Identify the commit range

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

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

```sh
PW_SRC_DIR=../playwright ./scripts/update_api.sh # PW_SRC_DIR already exported in step 2
./scripts/update_api.sh # uses PW_SRC_DIR from step 2; run the checkout checks there first
```

The script, for `sync` and then `async`:
Expand Down Expand Up @@ -250,10 +266,10 @@ Failure modes and fixes:
| 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. |
| 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`. |
| 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. |
| `_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. |
| 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`. |
| 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. |
| 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. |
| 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. |
| `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. |
| `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. |
| 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`). |
Expand Down Expand Up @@ -281,6 +297,8 @@ For each PORT, add one async test and a matching sync test. Conventions:

### 8. Final verification

Repeat the checkout checks from step 2: the committed `_generated.py` must come from `release-<new>`.

```sh
pre-commit run --all-files
mypy playwright # 2 pre-existing errors in _json_pipe.py and _artifact.py are unrelated
Expand Down
3 changes: 1 addition & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ Python bindings for [Playwright](https://playwright.dev). The Python client talk
- `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/`.
- `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/`.
- `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.
- `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.
- `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.
- `tests/async/`, `tests/sync/` — pytest suites. Most new tests are added to the async file with a sync mirror.
- `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.
- `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`).
- `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).
- 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.
- 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.
- `ROLLING.md`, `CONTRIBUTING.md` — human-facing setup and roll docs.

## Setup
Expand Down
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,14 @@ For more details look at the [CI configuration](./.github/workflows/ci.yml).

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

```bash
PW_SRC_DIR=../playwright ./scripts/update_api.sh
PW_SRC_DIR=../playwright-for-python ./scripts/update_api.sh
pre-commit run --all-files
```

Expand Down
2 changes: 1 addition & 1 deletion ROLLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pip install -e .
```
* 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`
* 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).
* 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`
* 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`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not plan to have a "separate checkout used only for rolls"!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will!

* 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`.
* commit changes & send PR
* wait for bots to pass & merge the PR
196 changes: 98 additions & 98 deletions playwright/_impl/_api_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@
from typing import Any, Dict, List, Literal, Optional, Sequence, TypedDict, Union


class BrowserBindResult(TypedDict):
endpoint: str


class ClientCertificate(TypedDict, total=False):
origin: str
certPath: Optional[Union[str, Path]]
cert: Optional[bytes]
keyPath: Optional[Union[str, Path]]
key: Optional[bytes]
pfxPath: Optional[Union[str, Path]]
pfx: Optional[bytes]
passphrase: Optional[str]


class Cookie(TypedDict, total=False):
name: str
value: str
Expand All @@ -30,28 +45,28 @@ class Cookie(TypedDict, total=False):
partitionKey: Optional[str]


class StorageStateCookie(TypedDict, total=False):
name: str
value: str
domain: str
path: str
expires: float
httpOnly: bool
secure: bool
sameSite: Literal["Lax", "None", "Strict"]
class DebuggerLocation(TypedDict):
file: str
line: Optional[int]
column: Optional[int]


class SetCookieParam(TypedDict, total=False):
class DebuggerPausedDetails(TypedDict):
location: DebuggerLocation
title: str


class FilePayload(TypedDict):
name: str
value: str
url: Optional[str]
domain: Optional[str]
path: Optional[str]
expires: Optional[float]
httpOnly: Optional[bool]
secure: Optional[bool]
sameSite: Optional[Literal["Lax", "None", "Strict"]]
partitionKey: Optional[str]
mimeType: str
buffer: bytes


class DropPayload(TypedDict, total=False):
files: Optional[
Union[str, Path, Sequence[Union[str, Path]], FilePayload, Sequence[FilePayload]]
]
data: Optional[Dict[str, str]]


class FloatRect(TypedDict):
Expand Down Expand Up @@ -79,6 +94,11 @@ class LocalStorageEntry(TypedDict):
value: str


class NameValue(TypedDict):
name: str
value: str


class OriginState(TypedDict):
origin: str
localStorage: List[LocalStorageEntry]
Expand All @@ -103,20 +123,16 @@ class ProxySettings(TypedDict, total=False):
password: Optional[str]


class StorageState(TypedDict, total=False):
cookies: List[StorageStateCookie]
origins: List[OriginState]
class RemoteAddr(TypedDict):
ipAddress: str
port: int


class ClientCertificate(TypedDict, total=False):
origin: str
certPath: Optional[Union[str, Path]]
cert: Optional[bytes]
keyPath: Optional[Union[str, Path]]
key: Optional[bytes]
pfxPath: Optional[Union[str, Path]]
pfx: Optional[bytes]
passphrase: Optional[str]
class RequestSizes(TypedDict):
requestBodySize: int
requestHeadersSize: int
responseBodySize: int
responseHeadersSize: int


class ResourceTiming(TypedDict):
Expand All @@ -131,52 +147,22 @@ class ResourceTiming(TypedDict):
responseEnd: float


class RequestSizes(TypedDict):
requestBodySize: int
requestHeadersSize: int
responseBodySize: int
responseHeadersSize: int


class ViewportSize(TypedDict):
width: int
height: int


class SourceLocation(TypedDict):
url: str
line: int
column: int
lineNumber: int
columnNumber: int


class WebErrorLocation(TypedDict):
url: str
line: int
column: int


class FilePayload(TypedDict):
name: str
mimeType: str
buffer: bytes


class DropPayload(TypedDict, total=False):
files: Optional[
Union[str, Path, Sequence[Union[str, Path]], FilePayload, Sequence[FilePayload]]
]
data: Optional[Dict[str, str]]
class ScreencastActionStyle(TypedDict, total=False):
point: Optional[str]
highlight: Optional[str]
title: Optional[str]


class RemoteAddr(TypedDict):
ipAddress: str
port: int
class ScreencastFrame(TypedDict):
data: bytes
timestamp: float
viewportWidth: int
viewportHeight: int


class BrowserBindResult(TypedDict):
endpoint: str
class ScreencastSize(TypedDict):
width: int
height: int


class SecurityDetails(TypedDict):
Expand All @@ -187,39 +173,54 @@ class SecurityDetails(TypedDict):
validTo: Optional[float]


class NameValue(TypedDict):
class SetCookieParam(TypedDict, total=False):
name: str
value: str
url: Optional[str]
domain: Optional[str]
path: Optional[str]
expires: Optional[float]
httpOnly: Optional[bool]
secure: Optional[bool]
sameSite: Optional[Literal["Lax", "None", "Strict"]]
partitionKey: Optional[str]


class TracingGroupLocation(TypedDict):
file: str
line: Optional[int]
column: Optional[int]
class SourceLocation(TypedDict):
url: str
line: int
column: int
lineNumber: int
columnNumber: int


class DebuggerLocation(TypedDict):
class StorageStateCookie(TypedDict, total=False):
name: str
value: str
domain: str
path: str
expires: float
httpOnly: bool
secure: bool
sameSite: Literal["Lax", "None", "Strict"]


class StorageState(TypedDict, total=False):
cookies: List[StorageStateCookie]
origins: List[OriginState]


class TracingGroupLocation(TypedDict):
file: str
line: Optional[int]
column: Optional[int]


class DebuggerPausedDetails(TypedDict):
location: DebuggerLocation
title: str


class ScreencastSize(TypedDict):
class ViewportSize(TypedDict):
width: int
height: int


class ScreencastActionStyle(TypedDict, total=False):
point: Optional[str]
highlight: Optional[str]
title: Optional[str]


class VirtualCredential(TypedDict):
id: str
rpId: str
Expand All @@ -229,6 +230,12 @@ class VirtualCredential(TypedDict):
signCount: int


class WebErrorLocation(TypedDict):
url: str
line: int
column: int


class WebMCPToolAnnotations(TypedDict, total=False):
readOnly: Optional[bool]
untrustedContent: Optional[bool]
Expand All @@ -240,10 +247,3 @@ class WebMCPTool(TypedDict, total=False):
description: str
inputSchema: Optional[Any]
annotations: Optional[WebMCPToolAnnotations]


class ScreencastFrame(TypedDict):
data: bytes
timestamp: float
viewportWidth: int
viewportHeight: int
Loading
Loading