Skip to content

Commit d9a922d

Browse files
Skn0ttCopilot
andauthored
feat: add AsyncContext/Disposable support (#3052)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 581316c commit d9a922d

17 files changed

Lines changed: 418 additions & 76 deletions

playwright/_impl/_browser_context.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from playwright._impl._console_message import ConsoleMessage
4949
from playwright._impl._debugger import Debugger
5050
from playwright._impl._dialog import Dialog
51+
from playwright._impl._disposable import Disposable, DisposableStub
5152
from playwright._impl._errors import Error, TargetClosedError
5253
from playwright._impl._event_context_manager import EventContextManagerImpl
5354
from playwright._impl._fetch import APIRequestContext
@@ -394,16 +395,18 @@ async def set_offline(self, offline: bool) -> None:
394395

395396
async def add_init_script(
396397
self, script: str = None, path: Union[str, Path] = None
397-
) -> None:
398+
) -> Disposable:
398399
if path:
399400
script = (await async_readfile(path)).decode()
400401
if not isinstance(script, str):
401402
raise Error("Either path or script parameter must be specified")
402-
await self._channel.send("addInitScript", None, dict(source=script))
403+
return from_channel(
404+
await self._channel.send("addInitScript", None, dict(source=script))
405+
)
403406

404407
async def expose_binding(
405408
self, name: str, callback: Callable, handle: bool = None
406-
) -> None:
409+
) -> Disposable:
407410
for page in self._pages:
408411
if name in page._bindings:
409412
raise Error(
@@ -412,16 +415,18 @@ async def expose_binding(
412415
if name in self._bindings:
413416
raise Error(f'Function "{name}" has been already registered')
414417
self._bindings[name] = callback
415-
await self._channel.send(
416-
"exposeBinding", None, dict(name=name, needsHandle=handle or False)
418+
return from_channel(
419+
await self._channel.send(
420+
"exposeBinding", None, dict(name=name, needsHandle=handle or False)
421+
)
417422
)
418423

419-
async def expose_function(self, name: str, callback: Callable) -> None:
420-
await self.expose_binding(name, lambda source, *args: callback(*args))
424+
async def expose_function(self, name: str, callback: Callable) -> Disposable:
425+
return await self.expose_binding(name, lambda source, *args: callback(*args))
421426

422427
async def route(
423428
self, url: URLMatch, handler: RouteHandlerCallback, times: int = None
424-
) -> None:
429+
) -> DisposableStub:
425430
self._routes.insert(
426431
0,
427432
RouteHandler(
@@ -433,6 +438,7 @@ async def route(
433438
),
434439
)
435440
await self._update_interception_patterns()
441+
return DisposableStub(lambda: self.unroute(url, handler), self)
436442

437443
async def unroute(
438444
self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None

playwright/_impl/_disposable.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import asyncio
16+
import inspect
17+
import traceback
18+
from typing import Awaitable, Callable, Dict
19+
20+
import greenlet
21+
22+
from playwright._impl._connection import ChannelOwner
23+
from playwright._impl._errors import Error, is_target_closed_error
24+
25+
26+
class Disposable(ChannelOwner):
27+
def __init__(
28+
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
29+
) -> None:
30+
super().__init__(parent, type, guid, initializer)
31+
32+
async def dispose(self) -> None:
33+
try:
34+
await self._channel.send(
35+
"dispose",
36+
None,
37+
)
38+
except Exception as e:
39+
if not is_target_closed_error(e):
40+
raise e
41+
42+
async def close(self) -> None:
43+
await self.dispose()
44+
45+
def __repr__(self) -> str:
46+
return "<Disposable>"
47+
48+
49+
class DisposableStub:
50+
def __init__(
51+
self,
52+
dispose_fn: Callable[[], Awaitable[None]],
53+
parent: ChannelOwner,
54+
) -> None:
55+
self._dispose_fn = dispose_fn
56+
self._loop = parent._loop
57+
self._dispatcher_fiber = parent._dispatcher_fiber
58+
59+
async def dispose(self) -> None:
60+
await self._dispose_fn()
61+
62+
async def __aenter__(self) -> "DisposableStub":
63+
return self
64+
65+
async def __aexit__(self, *args: object) -> None:
66+
await self.dispose()
67+
68+
def __enter__(self) -> "DisposableStub":
69+
return self
70+
71+
def __exit__(self, *args: object) -> None:
72+
self._sync(self.dispose())
73+
74+
def _sync(self, coro: object) -> object:
75+
__tracebackhide__ = True
76+
if self._loop.is_closed():
77+
coro.close() # type: ignore
78+
raise Error("Event loop is closed! Is Playwright already stopped?")
79+
g_self = greenlet.getcurrent()
80+
task = self._loop.create_task(coro) # type: ignore
81+
setattr(task, "__pw_stack__", inspect.stack(0))
82+
setattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10))
83+
task.add_done_callback(lambda _: g_self.switch())
84+
while not task.done():
85+
self._dispatcher_fiber.switch() # type: ignore
86+
asyncio._set_running_loop(self._loop)
87+
return task.result()
88+
89+
async def close(self) -> None:
90+
await self.dispose()
91+
92+
def __repr__(self) -> str:
93+
return "<Disposable>"

playwright/_impl/_object_factory.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from playwright._impl._connection import ChannelOwner
2323
from playwright._impl._debugger import Debugger
2424
from playwright._impl._dialog import Dialog
25+
from playwright._impl._disposable import Disposable
2526
from playwright._impl._element_handle import ElementHandle
2627
from playwright._impl._fetch import APIRequestContext
2728
from playwright._impl._frame import Frame
@@ -69,6 +70,8 @@ def create_remote_object(
6970
return Debugger(parent, type, guid, initializer)
7071
if type == "Dialog":
7172
return Dialog(parent, type, guid, initializer)
73+
if type == "Disposable":
74+
return Disposable(parent, type, guid, initializer)
7275
if type == "ElementHandle":
7376
return ElementHandle(parent, type, guid, initializer)
7477
if type == "Frame":

playwright/_impl/_page.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from_nullable_channel,
5050
)
5151
from playwright._impl._console_message import ConsoleMessage
52+
from playwright._impl._disposable import Disposable, DisposableStub
5253
from playwright._impl._download import Download
5354
from playwright._impl._element_handle import ElementHandle, determine_screenshot_type
5455
from playwright._impl._errors import Error, TargetClosedError, is_target_closed_error
@@ -501,23 +502,25 @@ async def add_style_tag(
501502
) -> ElementHandle:
502503
return await self._main_frame.add_style_tag(**locals_to_params(locals()))
503504

504-
async def expose_function(self, name: str, callback: Callable) -> None:
505-
await self.expose_binding(name, lambda source, *args: callback(*args))
505+
async def expose_function(self, name: str, callback: Callable) -> Disposable:
506+
return await self.expose_binding(name, lambda source, *args: callback(*args))
506507

507508
async def expose_binding(
508509
self, name: str, callback: Callable, handle: bool = None
509-
) -> None:
510+
) -> Disposable:
510511
if name in self._bindings:
511512
raise Error(f'Function "{name}" has been already registered')
512513
if name in self._browser_context._bindings:
513514
raise Error(
514515
f'Function "{name}" has been already registered in the browser context'
515516
)
516517
self._bindings[name] = callback
517-
await self._channel.send(
518-
"exposeBinding",
519-
None,
520-
dict(name=name, needsHandle=handle or False),
518+
return from_channel(
519+
await self._channel.send(
520+
"exposeBinding",
521+
None,
522+
dict(name=name, needsHandle=handle or False),
523+
)
521524
)
522525

523526
async def set_extra_http_headers(self, headers: Dict[str, str]) -> None:
@@ -661,18 +664,20 @@ async def bring_to_front(self) -> None:
661664

662665
async def add_init_script(
663666
self, script: str = None, path: Union[str, Path] = None
664-
) -> None:
667+
) -> Disposable:
665668
if path:
666669
script = add_source_url_to_script(
667670
(await async_readfile(path)).decode(), path
668671
)
669672
if not isinstance(script, str):
670673
raise Error("Either path or script parameter must be specified")
671-
await self._channel.send("addInitScript", None, dict(source=script))
674+
return from_channel(
675+
await self._channel.send("addInitScript", None, dict(source=script))
676+
)
672677

673678
async def route(
674679
self, url: URLMatch, handler: RouteHandlerCallback, times: int = None
675-
) -> None:
680+
) -> DisposableStub:
676681
self._routes.insert(
677682
0,
678683
RouteHandler(
@@ -684,6 +689,7 @@ async def route(
684689
),
685690
)
686691
await self._update_interception_patterns()
692+
return DisposableStub(lambda: self.unroute(url, handler), self)
687693

688694
async def unroute(
689695
self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None

playwright/_impl/_screencast.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from playwright._impl._api_structures import ScreencastFrame
2020
from playwright._impl._artifact import Artifact
2121
from playwright._impl._connection import from_nullable_channel
22+
from playwright._impl._disposable import DisposableStub
2223
from playwright._impl._errors import Error
2324
from playwright._impl._helper import locals_to_params
2425

@@ -63,7 +64,7 @@ async def start(
6364
onFrame: ScreencastFrameCallback = None,
6465
path: Union[str, Path] = None,
6566
quality: int = None,
66-
) -> None:
67+
) -> DisposableStub:
6768
if self._started:
6869
raise Error("Screencast is already started")
6970
self._started = True
@@ -81,6 +82,7 @@ async def start(
8182
if artifact_channel:
8283
self._artifact = from_nullable_channel(artifact_channel)
8384
self._save_path = path
85+
return DisposableStub(lambda: self.stop(), self._page)
8486

8587
async def stop(self) -> None:
8688
self._started = False
@@ -96,18 +98,26 @@ async def show_actions(
9698
duration: float = None,
9799
position: ScreencastPosition = None,
98100
fontSize: int = None,
99-
) -> None:
101+
) -> DisposableStub:
100102
await self._page._channel.send(
101103
"screencastShowActions", None, locals_to_params(locals())
102104
)
105+
return DisposableStub(lambda: self.hide_actions(), self._page)
103106

104107
async def hide_actions(self) -> None:
105108
await self._page._channel.send("screencastHideActions", None)
106109

107-
async def show_overlay(self, html: str, duration: float = None) -> None:
108-
await self._page._channel.send(
110+
async def show_overlay(self, html: str, duration: float = None) -> DisposableStub:
111+
result = await self._page._channel.send_return_as_dict(
109112
"screencastShowOverlay", None, locals_to_params(locals())
110113
)
114+
overlay_id = (result or {}).get("id")
115+
return DisposableStub(
116+
lambda: self._page._channel.send(
117+
"screencastRemoveOverlay", None, {"id": overlay_id}
118+
),
119+
self._page,
120+
)
111121

112122
async def show_chapter(
113123
self,

playwright/_impl/_tracing.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from playwright._impl._api_structures import TracingGroupLocation
1919
from playwright._impl._artifact import Artifact
2020
from playwright._impl._connection import ChannelOwner, from_nullable_channel
21+
from playwright._impl._disposable import DisposableStub
2122
from playwright._impl._helper import locals_to_params
2223

2324

@@ -148,8 +149,11 @@ def _reset_stack_counter(self) -> None:
148149
self._is_tracing = False
149150
self._connection.set_is_tracing(False)
150151

151-
async def group(self, name: str, location: TracingGroupLocation = None) -> None:
152+
async def group(
153+
self, name: str, location: TracingGroupLocation = None
154+
) -> DisposableStub:
152155
await self._channel.send("tracingGroup", None, locals_to_params(locals()))
156+
return DisposableStub(lambda: self.group_end(), self)
153157

154158
async def group_end(self) -> None:
155159
await self._channel.send(

0 commit comments

Comments
 (0)