Skip to content

chore: roll Playwright to ToT #516

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 19, 2021
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->90.0.4396.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Chromium <!-- GEN:chromium-version -->90.0.4412.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->14.1<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->85.0b10<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->86.0b9<!-- GEN:stop --> | ✅ | ✅ | ✅ |

Headless execution is supported for all browsers on all platforms.

Expand Down
5 changes: 3 additions & 2 deletions playwright/_impl/_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ async def new_context(
storageState: Union[StorageState, str, Path] = None,
) -> BrowserContext:
params = locals_to_params(locals())
normalize_context_params(params)
normalize_context_params(self._connection._is_sync, params)

channel = await self._channel.send("newContext", params)
context = from_channel(channel)
Expand Down Expand Up @@ -151,7 +151,8 @@ def version(self) -> str:
return self._initializer["version"]


def normalize_context_params(params: Dict) -> None:
def normalize_context_params(is_sync: bool, params: Dict) -> None:
params["sdkLanguage"] = "python" if is_sync else "python-async"
if params.get("noViewport"):
del params["noViewport"]
params["noDefaultViewport"] = True
Expand Down
2 changes: 1 addition & 1 deletion playwright/_impl/_browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def expect_event(
) -> EventContextManagerImpl:
if timeout is None:
timeout = self._timeout_settings.timeout()
wait_helper = WaitHelper(self._loop)
wait_helper = WaitHelper(self, f"expect_event({event})")
wait_helper.reject_on_timeout(
timeout, f'Timeout while waiting for event "{event}"'
)
Expand Down
2 changes: 1 addition & 1 deletion playwright/_impl/_browser_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ async def launch_persistent_context(
) -> BrowserContext:
userDataDir = str(Path(userDataDir))
params = locals_to_params(locals())
normalize_context_params(params)
normalize_context_params(self._connection._is_sync, params)
normalize_launch_params(params)
try:
context = from_channel(
Expand Down
61 changes: 57 additions & 4 deletions playwright/_impl/_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import sys
import traceback
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Union
from typing import Any, Callable, Dict, List, Optional, Union

from greenlet import greenlet
from pyee import AsyncIOEventEmitter
Expand Down Expand Up @@ -92,6 +92,32 @@ def __init__(
if self._parent:
self._parent._objects[guid] = self

def _wait_for_event_info_before(self, wait_id: str, name: str) -> None:
self._connection._send_message_to_server(
self._guid,
"waitForEventInfo",
{
"info": {
"name": name,
"waitId": wait_id,
"phase": "before",
"stack": capture_call_stack(),
}
},
)

def _wait_for_event_info_after(
self, wait_id: str, exception: Exception = None
) -> None:
info = {"waitId": wait_id, "phase": "after"}
if exception:
info["error"] = str(exception)
self._connection._send_message_to_server(
self._guid,
"waitForEventInfo",
{"info": info},
)

def _dispose(self) -> None:
# Clean up from parent and connection.
if self._parent:
Expand All @@ -106,7 +132,7 @@ def _dispose(self) -> None:

class ProtocolCallback:
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
self.stack_trace = "".join(traceback.format_stack()[-10:])
self.stack_trace: traceback.StackSummary = traceback.StackSummary()
self.future = loop.create_future()


Expand Down Expand Up @@ -166,14 +192,23 @@ def _send_message_to_server(
) -> ProtocolCallback:
self._last_id += 1
id = self._last_id
callback = ProtocolCallback(self._loop)
if self._is_sync:
task = asyncio.current_task(self._loop)
callback.stack_trace = (
getattr(task, "__pw_stack_trace__", None) if task else None
)
if not callback.stack_trace:
callback.stack_trace = traceback.extract_stack()

message = dict(
id=id,
guid=guid,
method=method,
params=self._replace_channels_with_guids(params, "params"),
metadata={"stack": serialize_call_stack(callback.stack_trace)},
)
self._transport.send(message)
callback = ProtocolCallback(self._loop)
self._callbacks[id] = callback
return callback

Expand All @@ -184,7 +219,9 @@ def _dispatch(self, msg: ParsedMessagePayload) -> None:
error = msg.get("error")
if error:
parsed_error = parse_error(error["error"]) # type: ignore
parsed_error.stack = callback.stack_trace
parsed_error.stack = "".join(
traceback.format_list(callback.stack_trace)[-10:]
)
callback.future.set_exception(parsed_error)
else:
result = self._replace_guids_with_channels(msg.get("result"))
Expand Down Expand Up @@ -267,3 +304,19 @@ def from_channel(channel: Channel) -> Any:

def from_nullable_channel(channel: Optional[Channel]) -> Optional[Any]:
return channel._object if channel else None


def serialize_call_stack(stack_trace: traceback.StackSummary) -> List[Dict]:
stack: List[Dict] = []
for frame in stack_trace:
if "_generated.py" in frame.filename:
break
stack.append(
{"file": frame.filename, "line": frame.lineno, "function": frame.name}
)
stack.reverse()
return stack


def capture_call_stack() -> List[Dict]:
return serialize_call_stack(traceback.extract_stack())
3 changes: 0 additions & 3 deletions playwright/_impl/_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from pathlib import Path

import playwright
from playwright._impl._logger import init_logger


def compute_driver_executable() -> Path:
Expand All @@ -38,5 +37,3 @@ def compute_driver_executable() -> Path:
# RuntimeError: Cannot add child handler, the child watcher does not have a loop attached
asyncio.get_event_loop()
asyncio.get_child_watcher()

init_logger()
10 changes: 6 additions & 4 deletions playwright/_impl/_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,10 @@ async def goto(
),
)

def _setup_navigation_wait_helper(self, timeout: float = None) -> WaitHelper:
wait_helper = WaitHelper(self._loop)
def _setup_navigation_wait_helper(
self, wait_name: str, timeout: float = None
) -> WaitHelper:
wait_helper = WaitHelper(self, wait_name)
wait_helper.reject_on_event(
self._page, "close", Error("Navigation failed because page was closed!")
)
Expand Down Expand Up @@ -146,7 +148,7 @@ def expect_navigation(
if timeout is None:
timeout = self._page._timeout_settings.navigation_timeout()
deadline = monotonic_time() + timeout
wait_helper = self._setup_navigation_wait_helper(timeout)
wait_helper = self._setup_navigation_wait_helper("expect_navigation", timeout)
matcher = URLMatcher(url) if url else None

def predicate(event: Any) -> bool:
Expand Down Expand Up @@ -185,7 +187,7 @@ async def wait_for_load_state(
raise Error("state: expected one of (load|domcontentloaded|networkidle)")
if state in self._load_states:
return
wait_helper = self._setup_navigation_wait_helper(timeout)
wait_helper = self._setup_navigation_wait_helper("wait_for_load_state", timeout)
wait_helper.wait_for_event(
self._event_emitter, "loadstate", lambda s: s == state
)
Expand Down
32 changes: 0 additions & 32 deletions playwright/_impl/_logger.py

This file was deleted.

2 changes: 1 addition & 1 deletion playwright/_impl/_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ def expect_event(
) -> EventContextManagerImpl:
if timeout is None:
timeout = cast(Any, self._parent)._timeout_settings.timeout()
wait_helper = WaitHelper(self._loop)
wait_helper = WaitHelper(self, f"expect_event({event})")
wait_helper.reject_on_timeout(
timeout, f'Timeout while waiting for event "{event}"'
)
Expand Down
2 changes: 1 addition & 1 deletion playwright/_impl/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,7 @@ def expect_event(
) -> EventContextManagerImpl:
if timeout is None:
timeout = self._timeout_settings.timeout()
wait_helper = WaitHelper(self._loop)
wait_helper = WaitHelper(self, f"expect_event({event})")
wait_helper.reject_on_timeout(
timeout, f'Timeout while waiting for event "{event}"'
)
Expand Down
27 changes: 20 additions & 7 deletions playwright/_impl/_sync_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@
# limitations under the License.

import asyncio
from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, cast
import traceback
from typing import (
Any,
Awaitable,
Callable,
Dict,
Generic,
List,
Optional,
TypeVar,
cast,
)

import greenlet

Expand Down Expand Up @@ -70,24 +81,26 @@ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
class SyncBase(ImplWrapper):
def __init__(self, impl_obj: Any) -> None:
super().__init__(impl_obj)
self._loop = impl_obj._loop
self._loop: asyncio.BaseEventLoop = impl_obj._loop
self._dispatcher_fiber = impl_obj._dispatcher_fiber

def __str__(self) -> str:
return self._impl_obj.__str__()

def _sync(self, task: asyncio.Future) -> Any:
def _sync(self, coro: Awaitable) -> Any:
stack_trace = traceback.extract_stack()
g_self = greenlet.getcurrent()
future = self._loop.create_task(task)
task = self._loop.create_task(coro)
setattr(task, "__pw_stack_trace__", stack_trace)

def callback(result: Any) -> None:
g_self.switch()

future.add_done_callback(callback)
while not future.done():
task.add_done_callback(callback)
while not task.done():
self._dispatcher_fiber.switch()
asyncio._set_running_loop(self._loop)
return future.result()
return task.result()

def _wrap_handler(self, handler: Any) -> Callable[..., None]:
if callable(handler):
Expand Down
11 changes: 9 additions & 2 deletions playwright/_impl/_wait_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,27 @@
# limitations under the License.

import asyncio
import uuid
from asyncio.tasks import Task
from typing import Any, Callable, Generic, List, Tuple, TypeVar

from pyee import EventEmitter

from playwright._impl._api_types import Error, TimeoutError
from playwright._impl._connection import ChannelOwner

T = TypeVar("T")


class WaitHelper(Generic[T]):
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
def __init__(self, channel_owner: ChannelOwner, name: str) -> None:
self._result: asyncio.Future = asyncio.Future()
self._loop = loop
self._wait_id = uuid.uuid4().hex
self._loop = channel_owner._loop
self._pending_tasks: List[Task] = []
self._channel_owner = channel_owner
self._registered_listeners: List[Tuple[EventEmitter, str, Callable]] = []
channel_owner._wait_for_event_info_before(self._wait_id, name)

def reject_on_event(
self,
Expand Down Expand Up @@ -65,11 +70,13 @@ def _fulfill(self, result: Any) -> None:
self._cleanup()
if not self._result.done():
self._result.set_result(result)
self._channel_owner._wait_for_event_info_after(self._wait_id)

def _reject(self, exception: Exception) -> None:
self._cleanup()
if not self._result.done():
self._result.set_exception(exception)
self._channel_owner._wait_for_event_info_after(self._wait_id, exception)

def wait_for_event(
self,
Expand Down
Loading