- Use 80% of the way to ASD-STE100 when talking to me and when generating docs.
Python bindings for Playwright. The client in playwright/_impl/ talks to the Node.js
driver in playwright/driver/ over a pipe.
Everything comes from microsoft/playwright at the commit in DRIVER_VERSION: the
driver, the generated API and the code that we port. .upstream/ is a full clone of it
(gitignored). Read upstream code there. Do not edit, commit or switch branches in it.
python scripts/upstream.py checkout # clone .upstream/, or move it to DRIVER_VERSION
python scripts/upstream.py changes # what changed upstream since DRIVER_VERSION at HEADYou need Python 3.10+, git, Node.js and npm.
python3 -m venv env && source env/bin/activate # no python3-venv: uv venv env
pip install -r local-requirements.txt
pip install -e .
python scripts/upstream.py checkout
python -m build --wheel # builds the driver from .upstream/ into playwright/driver/
playwright install chromium # not --with-deps: sudo is denied here
./test.sh [browser] # tests/sync and tests/async, under xvfb
./scripts/lint.sh # the Lint job of CI; run it before you commitWhen DRIVER_VERSION changes, for example after a pull, run
python scripts/upstream.py checkout and build again. Run tests/sync and
tests/async in separate pytest runs: a sync test fails in a process where an async
test ran.
playwright/_impl/ is a line-by-line port of the JS client,
packages/playwright-core/src/client/*.ts. isomorphic/, protocol/ and utils/ port
the parts of packages/isomorphic, packages/protocol and packages/utils that the
client uses. The rules are in playwright/_impl/AGENTS.md.
- One module per upstream file, in snake_case (
browserContext.tsisbrowser_context.py), with the members in the JS order and the JS comments. The docstring names the file:"""Port of packages/playwright-core/src/client/page.ts.(upstream.py changesreads it). - Each difference from the JS has a
# Python:comment. - Modules that start with
_are Python only._api_classes.pylists the implementation of each API class; the generated API imports it._greenlets.pyruns the sync API. - A new
ChannelOwnergoes in_object_factories()inconnection.py, and declares its channel:_channel: "PageChannel". A plain wrapper class (likeClock) takes_loopand_dispatcher_fiberfrom its parent. - Implementation methods take the snake_case arguments that the generated API passes.
An option that can be absent is
Optional[X]with no default, so that mypy finds each caller that misses a new option. - Send messages with the generated channel methods, every parameter by its protocol
name:
await self._channel.click(selector=selector, ..., timeout=self._timeout(timeout)).Noneis not sent. Listen withself._channel.on<Event>(handler). The tests check every sent message against the protocol spec.
./scripts/update_api.sh generates these files from .upstream/, and they are
committed. Never edit them by hand.
playwright/{async,sync}_api/_generated.pyandplaywright/_impl/_api_structures.py, fromdocs/src/api, byscripts/codegen/generate_api.js;- the protocol files of
_impl/(protocol_channels.py,protocol_types.py,protocol/validator.py,_protocol.py), frompackages/protocol/spec; tests/upstream_registry.md, a copy oftests/registry.md.
After a docs change, mypy playwright shows what _impl/ is missing.
* langs:in the docs alone decides if a member is for Python. Nolangsmeans every language. Do not judge by the name of an API.node .upstream/utils/doclint/generateApiJson.jsdumps the docs withlangs.- When Python ships a different API, the Python API wins: fix the upstream docs with a
Python-only declaration (
* langs: python). scripts/codegen/python_rules.jsholds only what the docs can not say, each entry with a one-line comment that says why. The generator fails on an entry that matches nothing.- An inline
[Object]needs a Python name in the docs ("Name" <[Object]>or* alias-python:); the generator then writes its TypedDict. Export it from both__init__.pyfiles. - Other generator errors name the place: fix the docs, or add the type to
toPyingenerate_api.js.scripts/codegen/compare_api.pyshows what changed between two generated files.
tests/{async,sync}/page/ and library/ port upstream's tests/page/ and
tests/library/ test for test, once per flavor. tests/AGENTS.md has the rules.
python scripts/check_test_registry.py checks that each test of the registry is ported,
or listed in tests/upstream_not_ported.txt with the reason (--missing prints what is
left).
- Port with zero deviations. Never change a port to make it pass: fix the client, the
driver or the environment. The only allowed mark is
xfail(reason="python: ...", strict=True), for a known bug of the client. - The test timeout is 30 s, as upstream.
test.slow()is@pytest.mark.slow. - Python-only tests (the sync API, asyncio, regressions) go in
tests/{async,sync}/python/. Keep them few: extend a test before you add one.
Use the playwright-roll skill.
- Never push without "push", "upload", "create PR" or "ship it" in my message, also not to an open PR. Commit locally and tell me.
- Never post comments or reviews on GitHub as me. Write the text and wait for my OK.
- Commit message:
label(scope): description, withfix,feat,chore,docs,testordevops. No Co-Authored-By, no "Generated with". - Branch for an issue:
fix-<number>. PR description: a few bullets, no test plan,Fixes https://github.com/microsoft/playwright-python/issues/<number>.