Skip to content
Closed
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
15 changes: 10 additions & 5 deletions .claude/skills/playwright-roll/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The Python port is hand-written code in `playwright/_impl/`, plus a generator (`

1. introspects the Python `_impl` classes via `inspect`,
2. emits typed wrapper classes into `playwright/{async,sync}_api/_generated.py`, and
3. diffs the introspected surface against `playwright/driver/package/api.json` (built into the new driver from source).
3. diffs the introspected surface against `driver/playwright-<sha>-api.json`

Anything in `api.json` that is missing or differently typed in `_impl/` causes generation to fail. Three resolutions:

Expand Down Expand Up @@ -68,9 +68,11 @@ playwright install chromium # NOT --with-deps; sudo is denied
The wheel build clones `microsoft/playwright` at the commit in `DRIVER_SHA`
into `driver/playwright-src`, runs `npm ci && npm run build`, and runs upstream's
`utils/build/build-playwright-driver.sh` to produce the per-platform driver
bundles (`driver/playwright-<sha>-*.zip`), then unpacks the driver under
`playwright/driver/package/`. From this point,
`playwright/driver/package/api.json` reflects the new release. This requires
bundles (`driver/playwright-<sha>-*.zip`). It also stages
`driver/playwright-<sha>-api.json` (the Python-only codegen input, generated by
`scripts/build_driver.sh`) and unpacks the driver under
`playwright/driver/package/`. From this point, `driver/playwright-<sha>-api.json`
reflects the new release. This requires
**Node.js, npm, git and bash** on PATH; the first build is slow (full upstream
build + per-platform Node downloads).

Expand Down Expand Up @@ -117,7 +119,10 @@ Before tagging anything as MISMATCH or N/A based on appearance, dump the actual

```python
import json
data = json.load(open("playwright/driver/package/api.json"))
import pathlib

sha = pathlib.Path("DRIVER_SHA").read_text().strip()
data = json.load(open(f"driver/playwright-{sha}-api.json"))
classes = {c["name"]: c for c in data}
for cls_name in ["Page", "BrowserContext", "Screencast", "Debugger"]:
cls = classes.get(cls_name)
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: driver-bundles
path: driver/playwright-*.zip
path: |
driver/**
!driver/playwright-src/**
if-no-files-found: error
# The bundles are already-compressed zips; skip re-compression.
compression-level: 0
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ Python bindings for [Playwright](https://playwright.dev). The Python client talk

- `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.
- `playwright/async_api/_generated.py`, `playwright/sync_api/_generated.py` — **auto-generated**. Never edit by hand; rerun `./scripts/update_api.sh` after changing `_impl/` or the driver.
- `scripts/generate_api.py`, `scripts/generate_async_api.py`, `scripts/generate_sync_api.py`, `scripts/documentation_provider.py` — codegen and validation. They diff the Python implementation against the driver's `playwright/driver/package/api.json` and abort if either side is out of sync.
- `scripts/generate_api.py`, `scripts/generate_async_api.py`, `scripts/generate_sync_api.py`, `scripts/documentation_provider.py` — codegen and validation. They diff the Python implementation against `driver/playwright-<sha>-api.json` and abort if either side is out of sync.
- `scripts/expected_api_mismatch.txt` — explicit allowlist of "documented in JS, not in Python" or "named differently in Python" gaps. Lines that no longer apply must be removed.
- `tests/async/`, `tests/sync/` — pytest suites. Most new tests are added to the async file with a sync mirror.
- `DRIVER_SHA` — the single source of truth for which Playwright commit the driver is built from (one line, the 40-char `microsoft/playwright` commit SHA). Read by `setup.py`, `scripts/build_driver.sh`, and CI. The wheel build clones `microsoft/playwright` at this commit and builds the driver from source (via `scripts/build_driver.sh` + upstream's `utils/build/build-playwright-driver.sh`). The SHA is baked into the staged bundle filenames (`driver/playwright-<sha>-<suffix>.zip`), so it doubles as the build cache key.
- `scripts/build_driver.sh` — clones and builds the upstream driver bundles into `driver/`. A portable bash script (shareable with the other language forks) that needs Node.js, npm, git and bash; invoked from `setup.py`'s `bdist_wheel`. Reads the pin from `DRIVER_SHA`; takes no arguments.
- `scripts/build_driver.sh` — clones and builds the upstream driver bundles into `driver/`, and stages `driver/playwright-<sha>-api.json` alongside them. A portable bash script (shareable with the other language forks) that needs Node.js, npm, git and bash; invoked from `setup.py`'s `bdist_wheel`. Reads the pin from `DRIVER_SHA`; takes no arguments.
- `ROLLING.md`, `CONTRIBUTING.md` — human-facing setup and roll docs.

## Setup
Expand All @@ -39,7 +39,7 @@ If the system lacks `python3-venv`, `uv venv env` is an acceptable substitute (t
- Type-check: `mypy playwright`.
- Run tests: `pytest --browser chromium [-k name]`. Browsers are installed via `playwright install chromium` (do **not** use `--with-deps`, which requires sudo).

When changing public API, edit `_impl/`, then run `./scripts/update_api.sh`. The script regenerates `_generated.py` and validates against the driver's `api.json`. If validation fails, fix the mismatch in `_impl/`, in `expected_api_mismatch.txt`, or in `documentation_provider.py` — not by hand-editing `_generated.py`.
When changing public API, edit `_impl/`, then run `./scripts/update_api.sh`. The script regenerates `_generated.py` and validates against `driver/playwright-<sha>-api.json` (staged by `scripts/build_driver.sh`, which `update_api.sh` runs first). If validation fails, fix the mismatch in `_impl/`, in `expected_api_mismatch.txt`, or in `documentation_provider.py` — not by hand-editing `_generated.py`.

## Rolling Playwright to a new version

Expand Down
7 changes: 5 additions & 2 deletions ROLLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pip install -e .

## Fix typing issues with Playwright ToT

To generate the API from a local Playwright checkout (e.g. tip of tree) instead of
the pinned `DRIVER_SHA`, overwrite the staged `api.json` and rerun codegen:

1. `cd playwright`
1. `API_JSON_MODE=1 node utils/doclint/generateApiJson.js > ../playwright-python/playwright/driver/package/api.json`
1. `./scripts/update_api.sh`
1. `API_JSON_MODE=1 node utils/doclint/generateApiJson.js > "../playwright-python/driver/playwright-$(cat ../playwright-python/DRIVER_SHA)-api.json"`
1. `cd ../playwright-python && ./scripts/update_api.sh`
8 changes: 8 additions & 0 deletions scripts/build_driver.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@ fi
# Bundle suffixes produced by utils/build/build-playwright-driver.sh. Keep in
# sync with the "zip_name" values in setup.py.
SUFFIXES=(mac mac-arm64 linux linux-arm64 win32_x64 win32_arm64)
API_JSON_FILE="$DRIVER_DIR/playwright-$EXPECTED_SHA-api.json"

bundles_present() {
local suffix
for suffix in "${SUFFIXES[@]}"; do
[[ -f "$DRIVER_DIR/playwright-$EXPECTED_SHA-$suffix.zip" ]] || return 1
done
[[ -f "$API_JSON_FILE" ]] || return 1
return 0
}

Expand Down Expand Up @@ -143,6 +145,11 @@ copy_bundles() {
done
}

generate_api_json() {
echo "Generating api.json"
API_JSON_MODE=1 node "$SOURCE_DIR/utils/doclint/generateApiJson.js" > "$API_JSON_FILE"
}

# Fast path: the bundles for this exact pin are already staged, so there is
# nothing to (re)build. This keeps repeat invocations cheap and lets consumers
# that only downloaded the prebuilt bundles skip the build entirely (no Node).
Expand All @@ -155,3 +162,4 @@ require_tools
clone_source
build_source
copy_bundles
generate_api_json
11 changes: 4 additions & 7 deletions scripts/documentation_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import json
import pathlib
import re
import subprocess
from sys import stderr
from typing import Any, Dict, List, Set, Union, get_args, get_origin, get_type_hints
from urllib.parse import urljoin
Expand All @@ -32,12 +31,10 @@ def __init__(self, is_async: bool) -> None:
self.api: Any = {}
self.links: Dict[str, str] = {}
self.printed_entries: List[str] = []
process_output = subprocess.run(
["python", "-m", "playwright", "print-api-json"],
check=True,
capture_output=True,
)
self.api = json.loads(process_output.stdout)
repo_root = pathlib.Path(__file__).resolve().parents[1]
driver_sha = (repo_root / "DRIVER_SHA").read_text().strip()
api_json_path = repo_root / "driver" / f"playwright-{driver_sha}-api.json"
self.api = json.loads(api_json_path.read_text())
self.errors: Set[str] = set()
self.class_aliases: Dict[str, str] = {
"Disposable": "AsyncContextManager" if is_async else "SyncContextManager",
Expand Down
3 changes: 3 additions & 0 deletions scripts/update_api.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/bin/bash

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bash "$SCRIPT_DIR/build_driver.sh"

function update_api {
echo "Generating $1"
file_name="$1"
Expand Down
Loading