diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index b004920d2..e3c12fd61 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -262,7 +262,12 @@ async def route( ) -> None: self._routes.insert( 0, - RouteHandler(URLMatcher(self._options.get("baseURL"), url), handler, times), + RouteHandler( + URLMatcher(self._options.get("baseURL"), url), + handler, + True if self._dispatcher_fiber else False, + times, + ), ) if len(self._routes) == 1: await self._channel.send( diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 46202b7dc..36b529212 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -293,6 +293,9 @@ def dispatch(self, msg: ParsedMessagePayload) -> None: try: if self._is_sync: for listener in object._channel.listeners(method): + # Each event handler is a potentilly blocking context, create a fiber for each + # and switch to them in order, until they block inside and pass control to each + # other and then eventually back to dispatcher as listener functions return. g = greenlet(listener) g.switch(self._replace_guids_with_channels(params)) else: diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index d4709d473..acef6ba20 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -37,6 +37,8 @@ ) from urllib.parse import urljoin +from greenlet import greenlet + from playwright._impl._api_structures import NameValue from playwright._impl._api_types import Error, TimeoutError @@ -208,23 +210,34 @@ def __init__( self, matcher: URLMatcher, handler: RouteHandlerCallback, + is_sync: bool, times: Optional[int] = None, ): self.matcher = matcher self.handler = handler self._times = times if times else math.inf self._handled_count = 0 + self._is_sync = is_sync def matches(self, request_url: str) -> bool: return self.matcher.matches(request_url) def handle(self, route: "Route", request: "Request") -> None: - self._handled_count += 1 - result = cast( - Callable[["Route", "Request"], Union[Coroutine, Any]], self.handler - )(route, request) - if inspect.iscoroutine(result): - asyncio.create_task(result) + def impl() -> None: + self._handled_count += 1 + result = cast( + Callable[["Route", "Request"], Union[Coroutine, Any]], self.handler + )(route, request) + if inspect.iscoroutine(result): + asyncio.create_task(result) + + # As with event handlers, each route handler is a potentially blocking context + # so it needs a fiber. + if self._is_sync: + g = greenlet(impl) + g.switch() + else: + impl() @property def is_active(self) -> bool: diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 98916923a..aeddfc112 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -236,17 +236,21 @@ def _on_frame_detached(self, frame: Frame) -> None: self.emit(Page.Events.FrameDetached, frame) def _on_route(self, route: Route, request: Request) -> None: - for handler_entry in self._routes: - if handler_entry.matches(request.url): - try: - handler_entry.handle(route, request) - finally: - if not handler_entry.is_active: - self._routes.remove(handler_entry) - if len(self._routes) == 0: - asyncio.create_task(self._disable_interception()) - return - self._browser_context._on_route(route, request) + # Make this artificially async so that we could chain routes. + async def inner_route() -> None: + for handler_entry in self._routes: + if handler_entry.matches(request.url): + try: + handler_entry.handle(route, request) + finally: + if not handler_entry.is_active: + self._routes.remove(handler_entry) + if len(self._routes) == 0: + asyncio.create_task(self._disable_interception()) + return + self._browser_context._on_route(route, request) + + asyncio.create_task(inner_route()) def _on_binding(self, binding_call: "BindingCall") -> None: func = self._bindings.get(binding_call._initializer["name"]) @@ -578,6 +582,7 @@ async def route( RouteHandler( URLMatcher(self._browser_context._options.get("baseURL"), url), handler, + True if self._dispatcher_fiber else False, times, ), ) diff --git a/playwright/sync_api/_context_manager.py b/playwright/sync_api/_context_manager.py index 926639cf3..318aab4bb 100644 --- a/playwright/sync_api/_context_manager.py +++ b/playwright/sync_api/_context_manager.py @@ -60,10 +60,14 @@ def __enter__(self) -> SyncPlaywright: self._watcher = ThreadedChildWatcher() asyncio.set_child_watcher(self._watcher) # type: ignore + # Create a new fiber for the protocol dispatcher. It will be pumping events + # until the end of times. We will pass control to that fiber every time we + # block while waiting for a response. def greenlet_main() -> None: self._loop.run_until_complete(self._connection.run_as_sync()) dispatcher_fiber = greenlet(greenlet_main) + self._connection = Connection( dispatcher_fiber, create_remote_object, @@ -77,9 +81,11 @@ def callback_wrapper(playwright_impl: Playwright) -> None: self._playwright = SyncPlaywright(playwright_impl) g_self.switch() + # Switch control to the dispatcher, it'll fire an event and pass control to + # the calling greenlet. self._connection.call_on_object_with_known_name("Playwright", callback_wrapper) - dispatcher_fiber.switch() + playwright = self._playwright playwright.stop = self.__exit__ # type: ignore return playwright