Skip to content

Commit 4d302c1

Browse files
committed
chore(codegen): type _impl_obj in the generated API
- Each generated class declares the type of its _impl_obj, so mypy checks every generated call against playwright/_impl: a member or option that the docs have and _impl/ lacks is now a mypy error. - Calls that return nothing are no longer wrapped in mapping.from_maybe_impl(), which mypy rejects once the type is known. - _wrap_handler returns Any and to_milliseconds gets overloads, to match the generated calls. - test_browser_type_repr uses the public browser.browser_type.
1 parent b93d8b9 commit 4d302c1

10 files changed

Lines changed: 1743 additions & 2448 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ The Python port is hand-written code in `playwright/_impl/`, plus a generator (`
1717
2. applies the Python facts that the docs do not contain, from `scripts/codegen/python_rules.js`, and
1818
3. emits the whole public API into `playwright/{async,sync}_api/_generated.py`: signatures, types, docstrings, event overloads, and the call into `_impl/` for every member.
1919

20-
The generator never reads `_impl/`. A new member or option in the docs therefore shows up in `_generated.py` right away, as a call into `_impl/` that does not exist yet. Each generated class types its `_impl_obj`, so **`mypy playwright` lists every such gap** as an error in `_generated.py`. (Temporary: while `scripts/codegen/legacy_order.json` exists, `_impl_obj` is not typed and mypy reports nothing. Find the gaps in the docs diff and with the tests instead.) Three resolutions:
20+
The generator never reads `_impl/`. A new member or option in the docs therefore shows up in `_generated.py` right away, as a call into `_impl/` that does not exist yet. Each generated class types its `_impl_obj`, so **`mypy playwright` lists every such gap** as an error in `_generated.py`. Three resolutions:
2121

2222
- **PORT** — the new API is intended for Python (no `langs.only` filter, or `langs.only` includes `"python"`). Implement it in `_impl/` until mypy is clean.
2323
- **MISMATCH** — the shipped Python API differs from the docs (a callback signature uses unions, a kwarg uses a legacy name, etc.). The shipped implementation wins. Add an entry to `scripts/codegen/python_rules.js` with a comment explaining *why*, and propose the upstream docs change that would make the entry unnecessary.

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Python bindings for [Playwright](https://playwright.dev). The Python client talk
1212
- `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/`.
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, and the calls into `_impl/`. It does not read `_impl/`.
1414
- `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. While the file exists, `_impl_obj` is not typed, so the mypy check described below is not active yet: a docs member that `_impl/` lacks fails only at run time. A follow-up change deletes the file and the `TEMPORARY` code.
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.
1616
- `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.
1717
- `tests/async/`, `tests/sync/` — pytest suites. Most new tests are added to the async file with a sync mirror.
1818
- `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.

‎ROLLING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ pip install -e .
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).
1616
* 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`
17-
* 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`. (Temporary: while `scripts/codegen/legacy_order.json` exists, mypy does not check this yet; rely on the tests.)
17+
* 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/_async_base.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,7 @@ def __init__(self, impl_obj: Any) -> None:
6868
def __str__(self) -> str:
6969
return self._impl_obj.__str__()
7070

71-
def _wrap_handler(
72-
self, handler: Union[Callable[..., Any], Any]
73-
) -> Callable[..., None]:
71+
def _wrap_handler(self, handler: Union[Callable[..., Any], Any]) -> Any:
7472
if callable(handler):
7573
return mapping.wrap_handler(handler)
7674
return handler

‎playwright/_impl/_helper.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
TypeVar,
3737
Union,
3838
cast,
39+
overload,
3940
)
4041
from urllib.parse import ParseResult, urljoin, urlparse, urlunparse
4142

@@ -591,6 +592,20 @@ def is_textual_mime_type(mime_type: str) -> bool:
591592
return bool(TEXTUAL_MIME_TYPE.match(mime_type))
592593

593594

595+
@overload
596+
def to_milliseconds(value: Union[float, datetime.timedelta]) -> float: ...
597+
598+
599+
@overload
600+
def to_milliseconds(value: None) -> None: ...
601+
602+
603+
@overload
604+
def to_milliseconds(
605+
value: Union[float, datetime.timedelta, None],
606+
) -> Optional[float]: ...
607+
608+
594609
def to_milliseconds(
595610
value: Union[float, datetime.timedelta, None],
596611
) -> Optional[float]:

‎playwright/_impl/_sync_base.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,7 @@ def _sync(
110110
wait_for_future(self._loop, self._dispatcher_fiber, task)
111111
return task.result()
112112

113-
def _wrap_handler(
114-
self, handler: Union[Callable[..., Any], Any]
115-
) -> Callable[..., None]:
113+
def _wrap_handler(self, handler: Union[Callable[..., Any], Any]) -> Any:
116114
if callable(handler):
117115
return mapping.wrap_handler(handler)
118116
return handler

0 commit comments

Comments
 (0)