Skip to content

Commit 034511f

Browse files
authored
chore: allow forking control flow in route (#1375)
1 parent 5d33195 commit 034511f

File tree

5 files changed

+51
-19
lines changed

5 files changed

+51
-19
lines changed

playwright/_impl/_browser_context.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,12 @@ async def route(
262262
) -> None:
263263
self._routes.insert(
264264
0,
265-
RouteHandler(URLMatcher(self._options.get("baseURL"), url), handler, times),
265+
RouteHandler(
266+
URLMatcher(self._options.get("baseURL"), url),
267+
handler,
268+
True if self._dispatcher_fiber else False,
269+
times,
270+
),
266271
)
267272
if len(self._routes) == 1:
268273
await self._channel.send(

playwright/_impl/_connection.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,9 @@ def dispatch(self, msg: ParsedMessagePayload) -> None:
293293
try:
294294
if self._is_sync:
295295
for listener in object._channel.listeners(method):
296+
# Each event handler is a potentilly blocking context, create a fiber for each
297+
# and switch to them in order, until they block inside and pass control to each
298+
# other and then eventually back to dispatcher as listener functions return.
296299
g = greenlet(listener)
297300
g.switch(self._replace_guids_with_channels(params))
298301
else:

playwright/_impl/_helper.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
)
3838
from urllib.parse import urljoin
3939

40+
from greenlet import greenlet
41+
4042
from playwright._impl._api_structures import NameValue
4143
from playwright._impl._api_types import Error, TimeoutError
4244

@@ -208,23 +210,34 @@ def __init__(
208210
self,
209211
matcher: URLMatcher,
210212
handler: RouteHandlerCallback,
213+
is_sync: bool,
211214
times: Optional[int] = None,
212215
):
213216
self.matcher = matcher
214217
self.handler = handler
215218
self._times = times if times else math.inf
216219
self._handled_count = 0
220+
self._is_sync = is_sync
217221

218222
def matches(self, request_url: str) -> bool:
219223
return self.matcher.matches(request_url)
220224

221225
def handle(self, route: "Route", request: "Request") -> None:
222-
self._handled_count += 1
223-
result = cast(
224-
Callable[["Route", "Request"], Union[Coroutine, Any]], self.handler
225-
)(route, request)
226-
if inspect.iscoroutine(result):
227-
asyncio.create_task(result)
226+
def impl() -> None:
227+
self._handled_count += 1
228+
result = cast(
229+
Callable[["Route", "Request"], Union[Coroutine, Any]], self.handler
230+
)(route, request)
231+
if inspect.iscoroutine(result):
232+
asyncio.create_task(result)
233+
234+
# As with event handlers, each route handler is a potentially blocking context
235+
# so it needs a fiber.
236+
if self._is_sync:
237+
g = greenlet(impl)
238+
g.switch()
239+
else:
240+
impl()
228241

229242
@property
230243
def is_active(self) -> bool:

playwright/_impl/_page.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -236,17 +236,21 @@ def _on_frame_detached(self, frame: Frame) -> None:
236236
self.emit(Page.Events.FrameDetached, frame)
237237

238238
def _on_route(self, route: Route, request: Request) -> None:
239-
for handler_entry in self._routes:
240-
if handler_entry.matches(request.url):
241-
try:
242-
handler_entry.handle(route, request)
243-
finally:
244-
if not handler_entry.is_active:
245-
self._routes.remove(handler_entry)
246-
if len(self._routes) == 0:
247-
asyncio.create_task(self._disable_interception())
248-
return
249-
self._browser_context._on_route(route, request)
239+
# Make this artificially async so that we could chain routes.
240+
async def inner_route() -> None:
241+
for handler_entry in self._routes:
242+
if handler_entry.matches(request.url):
243+
try:
244+
handler_entry.handle(route, request)
245+
finally:
246+
if not handler_entry.is_active:
247+
self._routes.remove(handler_entry)
248+
if len(self._routes) == 0:
249+
asyncio.create_task(self._disable_interception())
250+
return
251+
self._browser_context._on_route(route, request)
252+
253+
asyncio.create_task(inner_route())
250254

251255
def _on_binding(self, binding_call: "BindingCall") -> None:
252256
func = self._bindings.get(binding_call._initializer["name"])
@@ -578,6 +582,7 @@ async def route(
578582
RouteHandler(
579583
URLMatcher(self._browser_context._options.get("baseURL"), url),
580584
handler,
585+
True if self._dispatcher_fiber else False,
581586
times,
582587
),
583588
)

playwright/sync_api/_context_manager.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,14 @@ def __enter__(self) -> SyncPlaywright:
6060
self._watcher = ThreadedChildWatcher()
6161
asyncio.set_child_watcher(self._watcher) # type: ignore
6262

63+
# Create a new fiber for the protocol dispatcher. It will be pumping events
64+
# until the end of times. We will pass control to that fiber every time we
65+
# block while waiting for a response.
6366
def greenlet_main() -> None:
6467
self._loop.run_until_complete(self._connection.run_as_sync())
6568

6669
dispatcher_fiber = greenlet(greenlet_main)
70+
6771
self._connection = Connection(
6872
dispatcher_fiber,
6973
create_remote_object,
@@ -77,9 +81,11 @@ def callback_wrapper(playwright_impl: Playwright) -> None:
7781
self._playwright = SyncPlaywright(playwright_impl)
7882
g_self.switch()
7983

84+
# Switch control to the dispatcher, it'll fire an event and pass control to
85+
# the calling greenlet.
8086
self._connection.call_on_object_with_known_name("Playwright", callback_wrapper)
81-
8287
dispatcher_fiber.switch()
88+
8389
playwright = self._playwright
8490
playwright.stop = self.__exit__ # type: ignore
8591
return playwright

0 commit comments

Comments
 (0)