Skip to content

Commit 377c3d2

Browse files
authored
Merge pull request #4319 from seleniumbase/selenium-and-cdp-updates
Selenium and CDP updates
2 parents bcc06c7 + 7d0f569 commit 377c3d2

File tree

11 files changed

+473
-26
lines changed

11 files changed

+473
-26
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from playwright.sync_api import sync_playwright
2+
from seleniumbase import SB
3+
4+
with SB(uc=True, locale="en") as sb:
5+
sb.activate_cdp_mode()
6+
endpoint_url = sb.cdp.get_endpoint_url()
7+
8+
with sync_playwright() as p:
9+
browser = p.chromium.connect_over_cdp(endpoint_url)
10+
page = browser.contexts[0].pages[0]
11+
page.goto("https://www.browserscan.net/bot-detection")
12+
page.wait_for_timeout(1000)
13+
sb.cdp.flash("Test Results", duration=4)
14+
page.wait_for_timeout(1000)
15+
sb.assert_element('strong:contains("Normal")')
16+
sb.cdp.flash('strong:contains("Normal")', duration=4, pause=4)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from playwright.sync_api import sync_playwright
2+
from seleniumbase import sb_cdp
3+
4+
sb = sb_cdp.Chrome(locale="en")
5+
endpoint_url = sb.get_endpoint_url()
6+
7+
with sync_playwright() as p:
8+
browser = p.chromium.connect_over_cdp(endpoint_url)
9+
page = browser.contexts[0].pages[0]
10+
page.goto("https://www.browserscan.net/bot-detection")
11+
page.wait_for_timeout(1000)
12+
sb.flash("Test Results", duration=4)
13+
page.wait_for_timeout(1000)
14+
sb.assert_element('strong:contains("Normal")')
15+
sb.flash('strong:contains("Normal")', duration=4, pause=4)

examples/cdp_mode/raw_muse.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""(Bypasses Friendly Captcha)"""
2+
from seleniumbase import SB
3+
4+
with SB(uc=True, test=True, guest=True) as sb:
5+
url = "https://muse.jhu.edu/verify"
6+
sb.activate_cdp_mode(url)
7+
sb.sleep(1.5)
8+
sb.solve_captcha()
9+
sb.sleep(4)
10+
sb.assert_element('#search_input')
11+
sb.sleep(3)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""(Bypasses Friendly Captcha)"""
2+
import asyncio
3+
from seleniumbase import cdp_driver
4+
5+
6+
async def main():
7+
url = "https://muse.jhu.edu/verify"
8+
driver = await cdp_driver.start_async(guest=True)
9+
page = await driver.get(url)
10+
await page.sleep(2.5)
11+
await page.solve_captcha()
12+
await page.sleep(4)
13+
await page.find('#search_input')
14+
await page.sleep(3)
15+
16+
if __name__ == "__main__":
17+
loop = asyncio.new_event_loop()
18+
loop.run_until_complete(main())

mkdocs_build/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
regex>=2026.3.32
55
pymdown-extensions>=10.21.2
6-
pipdeptree>=2.34.0
6+
pipdeptree>=2.35.1
77
python-dateutil>=2.8.2
88
Markdown==3.10.2
99
click==8.3.1

requirements.txt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ fasteners>=0.20
1414
mycdp>=1.3.7
1515
pynose>=1.5.5
1616
platformdirs~=4.4.0;python_version<"3.10"
17-
platformdirs>=4.9.4;python_version>="3.10"
17+
platformdirs>=4.9.6;python_version>="3.10"
1818
typing-extensions>=4.15.0
1919
sbvirtualdisplay>=1.4.0
2020
MarkupSafe>=3.0.3
@@ -29,7 +29,7 @@ pyreadline3>=3.5.4;platform_system=="Windows"
2929
tabcompleter>=1.4.0
3030
pdbp>=1.8.2
3131
idna>=3.11
32-
charset-normalizer>=3.4.6,<4
32+
charset-normalizer>=3.4.7,<4
3333
urllib3>=1.26.20,<2;python_version<"3.10"
3434
urllib3>=1.26.20,<3;python_version>="3.10"
3535
requests~=2.32.5;python_version<"3.10"
@@ -44,18 +44,17 @@ wsproto==1.2.0;python_version<"3.10"
4444
wsproto~=1.3.2;python_version>="3.10"
4545
websocket-client~=1.9.0
4646
selenium==4.32.0;python_version<"3.10"
47-
selenium==4.41.0;python_version>="3.10"
47+
selenium==4.43.0;python_version>="3.10"
4848
cssselect==1.3.0;python_version<"3.10"
4949
cssselect>=1.4.0,<2;python_version>="3.10"
50-
nest-asyncio==1.6.0
5150
sortedcontainers==2.4.0
5251
execnet==2.1.1;python_version<"3.10"
5352
execnet==2.1.2;python_version>="3.10"
5453
iniconfig==2.1.0;python_version<"3.10"
5554
iniconfig==2.3.0;python_version>="3.10"
5655
pluggy==1.6.0
5756
pytest==8.4.2;python_version<"3.11"
58-
pytest==9.0.2;python_version>="3.11"
57+
pytest==9.0.3;python_version>="3.11"
5958
pytest-html==4.0.2
6059
pytest-metadata==3.1.1
6160
pytest-ordering==0.6

seleniumbase/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# seleniumbase package
2-
__version__ = "4.47.9"
2+
__version__ = "4.48.0"

seleniumbase/core/nest_asyncio.py

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
"""Patch asyncio to allow nested event loops."""
2+
import asyncio
3+
import asyncio.events as events
4+
import os
5+
import sys
6+
import threading
7+
from contextlib import contextmanager, suppress
8+
from heapq import heappop
9+
10+
_run_close_loop = True
11+
12+
13+
class PatchedNestAsyncio:
14+
pass
15+
16+
17+
def apply(loop=None, *, run_close_loop=False, error_on_mispatched=False):
18+
global _run_close_loop
19+
_patch_asyncio(error_on_mispatched=error_on_mispatched)
20+
_patch_policy()
21+
_patch_tornado()
22+
loop = loop or _get_event_loop()
23+
if loop is not None:
24+
_patch_loop(loop)
25+
_run_close_loop &= run_close_loop
26+
27+
28+
if sys.version_info < (3, 14, 0):
29+
def _get_event_loop():
30+
return asyncio.get_event_loop()
31+
else:
32+
def _get_event_loop():
33+
try:
34+
return asyncio.get_event_loop()
35+
except RuntimeError:
36+
return None
37+
38+
39+
if sys.version_info < (3, 12, 0):
40+
def run(main, *, debug=False):
41+
loop = asyncio.get_event_loop()
42+
loop.set_debug(debug)
43+
task = asyncio.ensure_future(main)
44+
try:
45+
return loop.run_until_complete(task)
46+
finally:
47+
if not task.done():
48+
task.cancel()
49+
with suppress(asyncio.CancelledError):
50+
loop.run_until_complete(task)
51+
else:
52+
def run(main, *, debug=False, loop_factory=None):
53+
new_event_loop = False
54+
set_event_loop = None
55+
try:
56+
loop = asyncio.get_running_loop()
57+
except RuntimeError:
58+
if not _run_close_loop:
59+
loop = _get_event_loop()
60+
if loop is None:
61+
if loop_factory is None:
62+
loop_factory = asyncio.new_event_loop
63+
loop = loop_factory()
64+
asyncio.set_event_loop(loop)
65+
else:
66+
if loop_factory is None:
67+
loop = asyncio.new_event_loop()
68+
set_event_loop = _get_event_loop()
69+
asyncio.set_event_loop(loop)
70+
else:
71+
loop = loop_factory()
72+
new_event_loop = True
73+
_patch_loop(loop)
74+
75+
loop.set_debug(debug)
76+
task = asyncio.ensure_future(main, loop=loop)
77+
try:
78+
return loop.run_until_complete(task)
79+
finally:
80+
if not task.done():
81+
task.cancel()
82+
with suppress(asyncio.CancelledError):
83+
loop.run_until_complete(task)
84+
if set_event_loop:
85+
asyncio.set_event_loop(set_event_loop)
86+
if new_event_loop:
87+
# Avoid ResourceWarning: unclosed event loop
88+
loop.close()
89+
90+
91+
def _patch_asyncio(*, error_on_mispatched=False):
92+
"""Patch asyncio module to use pure Python tasks and futures."""
93+
94+
def _get_event_loop(stacklevel=3):
95+
loop = events._get_running_loop()
96+
if loop is None:
97+
if sys.version_info < (3, 14, 0):
98+
policy = events.get_event_loop_policy()
99+
else:
100+
policy = events._get_event_loop_policy()
101+
loop = policy.get_event_loop()
102+
return loop
103+
104+
if hasattr(asyncio, "_nest_patched"):
105+
if not hasattr(asyncio, "_nest_asyncio"):
106+
if error_on_mispatched:
107+
raise RuntimeError("asyncio was already patched!")
108+
elif sys.version_info >= (3, 12, 0):
109+
import warnings
110+
warnings.warn("asyncio was already patched!")
111+
return
112+
113+
asyncio.tasks.Task = asyncio.tasks._PyTask
114+
asyncio.Task = asyncio.tasks._CTask = asyncio.tasks.Task
115+
asyncio.Future = asyncio.futures._CFuture = asyncio.futures.Future = (
116+
asyncio.futures._PyFuture
117+
)
118+
asyncio.get_event_loop = _get_event_loop
119+
events._get_event_loop = events.get_event_loop = asyncio.get_event_loop
120+
asyncio.run = run
121+
asyncio._nest_patched = True
122+
asyncio._nest_asyncio = PatchedNestAsyncio()
123+
124+
125+
def _patch_policy():
126+
"""Patch the policy to always return a patched loop."""
127+
128+
def get_event_loop(self):
129+
if self._local._loop is None:
130+
loop = self.new_event_loop()
131+
_patch_loop(loop)
132+
self.set_event_loop(loop)
133+
return self._local._loop
134+
135+
if sys.version_info < (3, 14, 0):
136+
policy = events.get_event_loop_policy()
137+
else:
138+
policy = events._get_event_loop_policy()
139+
policy.__class__.get_event_loop = get_event_loop
140+
141+
142+
def _patch_loop(loop):
143+
"""Patch loop to make it reentrant."""
144+
145+
def run_forever(self):
146+
with manage_run(self), manage_asyncgens(self):
147+
while True:
148+
self._run_once()
149+
if self._stopping:
150+
break
151+
self._stopping = False
152+
153+
def run_until_complete(self, future):
154+
with manage_run(self):
155+
f = asyncio.ensure_future(future, loop=self)
156+
if f is not future:
157+
f._log_destroy_pending = False
158+
while not f.done():
159+
self._run_once()
160+
if self._stopping:
161+
break
162+
if not f.done():
163+
raise RuntimeError("Loop stopped before Future completed!")
164+
return f.result()
165+
166+
def _run_once(self):
167+
"""Simplified re-implementation of asyncio's _run_once."""
168+
ready = self._ready
169+
scheduled = self._scheduled
170+
while scheduled and scheduled[0]._cancelled:
171+
heappop(scheduled)
172+
timeout = (
173+
0
174+
if ready or self._stopping
175+
else min(max(scheduled[0]._when - self.time(), 0), 86400)
176+
if scheduled
177+
else None
178+
)
179+
event_list = self._selector.select(timeout)
180+
self._process_events(event_list)
181+
end_time = self.time() + self._clock_resolution
182+
while scheduled and scheduled[0]._when < end_time:
183+
handle = heappop(scheduled)
184+
ready.append(handle)
185+
for _ in range(len(ready)):
186+
if not ready:
187+
break
188+
handle = ready.popleft()
189+
if not handle._cancelled:
190+
if sys.version_info < (3, 14, 0):
191+
curr_task = curr_tasks.pop(self, None)
192+
else:
193+
try:
194+
curr_task = asyncio.tasks._swap_current_task(
195+
self, None
196+
)
197+
except KeyError:
198+
curr_task = None
199+
try:
200+
handle._run()
201+
finally:
202+
if curr_task is not None:
203+
if sys.version_info < (3, 14, 0):
204+
curr_tasks[self] = curr_task
205+
else:
206+
asyncio.tasks._swap_current_task(self, curr_task)
207+
handle = None
208+
209+
@contextmanager
210+
def manage_run(self):
211+
self._check_closed()
212+
old_thread_id = self._thread_id
213+
old_running_loop = events._get_running_loop()
214+
try:
215+
self._thread_id = threading.get_ident()
216+
events._set_running_loop(self)
217+
self._num_runs_pending += 1
218+
if self._is_proactorloop:
219+
if self._self_reading_future is None:
220+
self.call_soon(self._loop_self_reading)
221+
yield
222+
finally:
223+
self._thread_id = old_thread_id
224+
events._set_running_loop(old_running_loop)
225+
self._num_runs_pending -= 1
226+
if self._is_proactorloop:
227+
if (
228+
self._num_runs_pending == 0
229+
and self._self_reading_future is not None
230+
):
231+
ov = self._self_reading_future._ov
232+
self._self_reading_future.cancel()
233+
if ov is not None:
234+
self._proactor._unregister(ov)
235+
self._self_reading_future = None
236+
237+
@contextmanager
238+
def manage_asyncgens(self):
239+
old_agen_hooks = sys.get_asyncgen_hooks()
240+
try:
241+
self._set_coroutine_origin_tracking(self._debug)
242+
if self._asyncgens is not None:
243+
sys.set_asyncgen_hooks(
244+
firstiter=self._asyncgen_firstiter_hook,
245+
finalizer=self._asyncgen_finalizer_hook,
246+
)
247+
yield
248+
finally:
249+
self._set_coroutine_origin_tracking(False)
250+
if self._asyncgens is not None:
251+
sys.set_asyncgen_hooks(*old_agen_hooks)
252+
253+
def _check_running(self):
254+
"""Do not throw exception if loop is already running."""
255+
pass
256+
257+
if hasattr(loop, "_nest_patched"):
258+
return
259+
if not isinstance(loop, asyncio.BaseEventLoop):
260+
raise ValueError("Can't patch loop of type %s" % type(loop))
261+
cls = loop.__class__
262+
cls.run_forever = run_forever
263+
cls.run_until_complete = run_until_complete
264+
cls._run_once = _run_once
265+
cls._check_running = _check_running
266+
cls._num_runs_pending = 1 if loop.is_running() else 0
267+
cls._is_proactorloop = os.name == "nt" and issubclass(
268+
cls, asyncio.ProactorEventLoop
269+
)
270+
curr_tasks = asyncio.tasks._current_tasks
271+
cls._nest_patched = True
272+
cls._nest_asyncio = PatchedNestAsyncio()
273+
274+
275+
def _patch_tornado():
276+
"""If tornado is imported before nest_asyncio,
277+
make tornado aware of the pure-Python asyncio Future."""
278+
if "tornado" in sys.modules:
279+
import tornado.concurrent as tc # type: ignore
280+
tc.Future = asyncio.Future
281+
if asyncio.Future not in tc.FUTURES:
282+
tc.FUTURES += (asyncio.Future,)

0 commit comments

Comments
 (0)