Compare commits

..

46 Commits

Author SHA1 Message Date
Michael Mintz 54e76c8fd3
Merge pull request #3700 from seleniumbase/add-cdp-options
Add CDP Options
2025-04-25 02:38:48 -04:00
Michael Mintz 5ae12a0570 Version 4.37.7 2025-04-25 02:34:14 -04:00
Michael Mintz 0dd8e9291e Refresh Python dependencies 2025-04-25 02:34:02 -04:00
Michael Mintz 5984207cc9 Add an example of changing the timezone with CDP 2025-04-25 02:33:45 -04:00
Michael Mintz 64943b2870 Add an example of changing geolocation with CDP 2025-04-25 02:33:06 -04:00
Michael Mintz 3dbb7d162f Update a ReadMe 2025-04-25 02:30:34 -04:00
Michael Mintz 420991034b Add more CDP options 2025-04-25 02:29:55 -04:00
Michael Mintz 94b3fa6e9b
Merge pull request #3693 from seleniumbase/improve-cdp-multithreading
Improve CDP Multithreading
2025-04-23 13:13:25 -04:00
Michael Mintz 2d5c810376 Version 4.37.6 2025-04-23 13:08:40 -04:00
Michael Mintz ee8b82df82 Update examples 2025-04-23 13:08:26 -04:00
Michael Mintz c500466ecb Improve CDP multithreading 2025-04-23 13:08:02 -04:00
Michael Mintz 395e6a024f
Merge pull request #3690 from seleniumbase/fix-asyncio-issue
Fix `asyncio` issue
2025-04-22 18:21:28 -04:00
Michael Mintz 60561238c5 Version 4.37.5 2025-04-22 18:17:06 -04:00
Michael Mintz 1cc6681706 Refresh Python dependencies 2025-04-22 18:16:53 -04:00
Michael Mintz fb271054d1 Fix `asyncio` issue that appears when using multiple drivers 2025-04-22 18:16:02 -04:00
Michael Mintz 0fa098042e
Merge pull request #3686 from seleniumbase/fix-cdp-mode-memory-leak
Fix CDP Mode memory leak
2025-04-20 11:21:22 -04:00
Michael Mintz 9cc66b9146 Refresh Python dependencies 2025-04-20 11:17:57 -04:00
Michael Mintz 51cefe6e58 Version 4.37.4 2025-04-20 11:11:31 -04:00
Michael Mintz 2040014f0d Fix a memory leak in CDP Mode 2025-04-20 11:11:18 -04:00
Michael Mintz 238ad3d233
Merge pull request #3684 from seleniumbase/mostly-memory-improvements
Mostly memory improvements
2025-04-19 11:53:19 -04:00
Michael Mintz 5b30240d73 Version 4.37.3 2025-04-19 11:49:21 -04:00
Michael Mintz 7798808d8c Refresh Python dependencies 2025-04-19 11:49:09 -04:00
Michael Mintz 4e9fbd65bf Reduce memory usage 2025-04-19 11:48:00 -04:00
Michael Mintz b9a89f7a26
Merge pull request #3668 from seleniumbase/gui-click-and-hold
Add `sb.cdp.gui_click_and_hold(selector, timeframe)`
2025-04-10 18:58:17 -04:00
Michael Mintz 4fa959b718 Version 4.37.2 2025-04-10 18:54:10 -04:00
Michael Mintz c80b256e34 Refresh Python dependencies 2025-04-10 18:53:59 -04:00
Michael Mintz dba82c0bda Add `sb.cdp.gui_click_and_hold(selector, timeframe)` 2025-04-10 18:53:26 -04:00
Michael Mintz 990701d9ee
Merge pull request #3655 from seleniumbase/fix-cdp-mode-issues
Fix CDP Mode issues
2025-04-07 16:30:39 -04:00
Michael Mintz 79e38de0b1 Version 4.37.1 2025-04-07 16:26:45 -04:00
Michael Mintz a68f33ce0d Fix CDP Mode issues 2025-04-07 16:26:33 -04:00
Michael Mintz 004f22ffbd Update an example 2025-04-07 16:21:53 -04:00
Michael Mintz 94fd008a2a
Merge pull request #3653 from seleniumbase/selenium-update
Selenium update
2025-04-06 20:54:55 -04:00
Michael Mintz 0f3b0e9fe5 Version 4.37.0 2025-04-06 20:50:27 -04:00
Michael Mintz 74b57669ff Refresh Python dependencies 2025-04-06 20:50:04 -04:00
Michael Mintz 9c96a8ca83
Merge pull request #3649 from seleniumbase/refresh-dependencies
Refresh dependencies
2025-04-03 23:42:06 -04:00
Michael Mintz ce43535a53 Version 4.36.5 2025-04-03 23:38:07 -04:00
Michael Mintz 0da7d0fbca Refresh Python dependencies 2025-04-03 23:37:49 -04:00
Michael Mintz 9c48161ef1 Update CDP ReadMe 2025-04-03 23:30:27 -04:00
Michael Mintz 30815fe879 Refresh mkdocs dependencies 2025-04-03 23:28:47 -04:00
Michael Mintz d18bbf1c0d Update comments 2025-04-03 23:28:02 -04:00
Michael Mintz 05b8385994 Update flake8 2025-04-03 23:27:12 -04:00
Michael Mintz e5558c4c8a Update the ReadMe 2025-03-31 14:14:12 -04:00
Michael Mintz f30325e575 Update the ReadMe 2025-03-31 12:08:02 -04:00
Michael Mintz 808b252f47 Update a presentation 2025-03-31 12:07:52 -04:00
Michael Mintz ab9d898d56 Update timing 2025-03-31 12:07:34 -04:00
Michael Mintz a2817abcd8 Update an example 2025-03-29 11:45:50 -04:00
28 changed files with 544 additions and 131 deletions

View File

@ -13,7 +13,7 @@
<p align="center" class="hero__title"><b>All-in-one Browser Automation Framework:<br />Web Crawling / Testing / Scraping / Stealth</b></p>
<p align="center"><a href="https://pypi.python.org/pypi/seleniumbase" target="_blank"><img src="https://img.shields.io/pypi/v/seleniumbase.svg?color=3399EE" alt="PyPI version" /></a> <a href="https://github.com/seleniumbase/SeleniumBase/releases" target="_blank"><img src="https://img.shields.io/github/v/release/seleniumbase/SeleniumBase.svg?color=22AAEE" alt="GitHub version" /></a> <a href="https://seleniumbase.io"><img src="https://img.shields.io/badge/docs-seleniumbase.io-11BBAA.svg" alt="SeleniumBase Docs" /></a> <a href="https://github.com/seleniumbase/SeleniumBase/actions" target="_blank"><img src="https://github.com/seleniumbase/SeleniumBase/workflows/CI%20build/badge.svg" alt="SeleniumBase GitHub Actions" /></a> <a href="https://discord.gg/EdhQTn3EyE" target="_blank"><img src="https://img.shields.io/badge/join-discord-infomational" alt="Join the SeleniumBase chat on Discord"/></a></p>
<p align="center"><a href="https://pypi.python.org/pypi/seleniumbase" target="_blank"><img src="https://img.shields.io/pypi/v/seleniumbase.svg?color=3399EE" alt="PyPI version" /></a> <a href="https://github.com/seleniumbase/SeleniumBase/releases" target="_blank"><img src="https://img.shields.io/github/v/release/seleniumbase/SeleniumBase.svg?color=22AAEE" alt="GitHub version" /></a> <a href="https://seleniumbase.io"><img src="https://img.shields.io/badge/docs-seleniumbase.io-11BBAA.svg" alt="SeleniumBase Docs" /></a> <a href="https://github.com/seleniumbase/SeleniumBase/actions" target="_blank"><img src="https://github.com/seleniumbase/SeleniumBase/workflows/CI%20build/badge.svg" alt="SeleniumBase GitHub Actions" /></a> <a href="https://discord.gg/EdhQTn3EyE" target="_blank"><img src="https://img.shields.io/discord/727927627830001734?color=7289DA&label=Discord&logo=discord&logoColor=white"/></a></p>
<p align="center">
<a href="#python_installation">🚀 Start</a> |
@ -1391,6 +1391,6 @@ pytest --reruns=1 --reruns-delay=1
<div><a href="https://github.com/seleniumbase/SeleniumBase/"><img src="https://seleniumbase.github.io/cdn/img/super_logo_sb3.png" title="SeleniumBase" width="274" /></a></div>
<div><a href="https://seleniumbase.io"><img src="https://img.shields.io/badge/docs-seleniumbase.io-11BBAA.svg" alt="SeleniumBase Docs" /></a> <a href="https://github.com/seleniumbase/SeleniumBase/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-22BBCC.svg" title="SeleniumBase" /></a></div>
<div><a href="https://github.com/seleniumbase/SeleniumBase"><img src="https://img.shields.io/badge/tested%20with-SeleniumBase-04C38E.svg" alt="Tested with SeleniumBase" /></a> <a href="https://github.com/seleniumbase/SeleniumBase/stargazers"><img src="https://img.shields.io/github/stars/seleniumbase/seleniumbase.svg?color=19A57B" title="Stargazers" /></a></div>
<div><a href="https://hellogithub.com/repository/c6be2d0f1969448697683d11a4ff915e" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=c6be2d0f1969448697683d11a4ff915e&claim_uid=xcrm4p9j3d6JCO5&theme=small" alt="FeaturedHelloGitHub" /></a> <a href="https://discord.gg/EdhQTn3EyE" target="_blank"><img src="https://img.shields.io/badge/join-discord-infomational" alt="Join the SeleniumBase chat on Discord"/></a> <a href="https://gitter.im/seleniumbase/SeleniumBase" target="_blank"><img src="https://img.shields.io/gitter/room/seleniumbase/SeleniumBase.svg" alt="Gitter chat"/></a></div>
<div><a href="https://hellogithub.com/repository/c6be2d0f1969448697683d11a4ff915e" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=c6be2d0f1969448697683d11a4ff915e&claim_uid=xcrm4p9j3d6JCO5&theme=small" alt="FeaturedHelloGitHub" /></a> <a href="https://discord.gg/EdhQTn3EyE" target="_blank"><img src="https://img.shields.io/discord/727927627830001734?color=7289DA&label=Discord&logo=discord&logoColor=white"/></a></div>
<div><a href="https://pepy.tech/projects/seleniumbase?timeRange=threeMonths&category=version&includeCIDownloads=true&granularity=daily&viewType=line&versions=*" target="_blank"><img src="https://static.pepy.tech/badge/seleniumbase" alt="SeleniumBase PyPI downloads" /></a> <img src="https://views.whatilearened.today/views/github/seleniumbase/SeleniumBase.svg" width="98px" height="20px" alt="Views" /></div>
<div align="left"></div>

View File

@ -286,6 +286,7 @@ with SB(uc=True, test=True, ad_block=True) as sb:
sb.activate_cdp_mode(url)
sb.sleep(2.5)
sb.cdp.click_if_visible('[data-automation-id*="close-mark"]')
sb.sleep(0.3)
sb.cdp.mouse_click('input[aria-label="Search"]')
sb.sleep(1.2)
search = "Settlers of Catan Board Game"
@ -300,7 +301,7 @@ with SB(uc=True, test=True, ad_block=True) as sb:
for item in items:
if required_text in item.text:
description = item.querySelector(
'[data-automation-id="product-price"] + span'
'[data-automation-id="product-title"]'
)
if description and description.text not in unique_item_text:
unique_item_text.append(description.text)
@ -419,7 +420,7 @@ sb.cdp.js_dumps(obj_name)
sb.cdp.maximize()
sb.cdp.minimize()
sb.cdp.medimize()
sb.cdp.set_window_rect()
sb.cdp.set_window_rect(x, y, width, height)
sb.cdp.reset_window_size()
sb.cdp.open_new_window(url=None, switch_to=True)
sb.cdp.switch_to_window(window)
@ -465,8 +466,9 @@ sb.cdp.gui_press_keys(keys)
sb.cdp.gui_write(text)
sb.cdp.gui_click_x_y(x, y)
sb.cdp.gui_click_element(selector)
sb.cdp.gui_drag_drop_points(x1, y1, x2, y2)
sb.cdp.gui_drag_and_drop(drag_selector, drop_selector)
sb.cdp.gui_drag_drop_points(x1, y1, x2, y2, timeframe=0.35)
sb.cdp.gui_drag_and_drop(drag_selector, drop_selector, timeframe=0.35)
sb.cdp.gui_click_and_hold(selector, timeframe=0.35)
sb.cdp.gui_hover_x_y(x, y)
sb.cdp.gui_hover_element(selector)
sb.cdp.gui_hover_and_click(hover_selector, click_selector)

View File

@ -16,6 +16,15 @@ with SB(uc=True, test=True, locale="en") as sb:
sb.sleep(1)
sb.cdp.gui_press_keys("\b" * 10 + formatted_date + "\n")
sb.sleep(1)
days_ahead = (4 - today.weekday() + 8) % 14
following_saturday = today + datetime.timedelta(days=days_ahead)
formatted_date = following_saturday.strftime("%m/%d/%Y")
sb.cdp.gui_click_element(
'[data-att="end-date-toggler"] [aria-describedby*="date-input"]'
)
sb.sleep(1)
sb.cdp.gui_press_keys("\b" * 10 + formatted_date + "\n")
sb.sleep(1)
sb.cdp.click('button[data-att="done"]')
sb.sleep(1)
sb.cdp.click('button[data-att="search"]')

View File

@ -0,0 +1,16 @@
"""Geolocation example using CDP Mode without WebDriver"""
from seleniumbase import decorators
from seleniumbase import sb_cdp
@decorators.print_runtime("Geolocation CDP Example")
def main():
url = "https://www.openstreetmap.org/"
sb = sb_cdp.Chrome(url, geoloc=(48.87645, 2.26340))
sb.click("span.geolocate")
sb.assert_url_contains("48.876450/2.263400")
sb.sleep(5)
if __name__ == "__main__":
main()

View File

@ -3,6 +3,7 @@ from seleniumbase import SB
with SB(uc=True, test=True, ad_block=True) as sb:
url = "https://www.glassdoor.com/Reviews/index.htm"
sb.activate_cdp_mode(url)
sb.sleep(2)
sb.uc_gui_click_captcha()
sb.highlight('[data-test="global-nav-glassdoor-logo"]')
sb.highlight('[data-test="site-header-companies"]')

View File

@ -2,12 +2,20 @@ from seleniumbase import SB
with SB(uc=True, incognito=True, test=True) as sb:
sb.activate_cdp_mode("https://pixelscan.net/")
sb.sleep(3)
sb.remove_elements(".bg-bannerBg") # Remove the top banner
sb.remove_elements("pxlscn-ad1") # Remove the ad banner
sb.sleep(2)
sb.click('button[class*="startButton"]')
sb.sleep(6)
sb.remove_elements(".bg-bannerBg") # Remove top banner
sb.remove_elements("pxlscn-ad1") # Remove an ad banner
sb.remove_elements("pxlscn-ad2") # Remove an ad banner
sb.remove_elements("jdiv") # Remove chat widgets
sb.sleep(14)
not_masking_text = "You are not masking your fingerprint"
sb.assert_text(not_masking_text, "pxlscn-fingerprint-masking")
sb.assert_text(
not_masking_text,
"pxlscn-fingerprint-masking",
timeout=20,
)
no_automation_detected = "No automation framework detected"
sb.assert_text(no_automation_detected, "pxlscn-bot-detection")
consistent_selector = 'div.bg-consistentBg [alt="Good"]'

View File

@ -8,28 +8,25 @@ with SB(uc=True, test=True, ad_block=True, pls="none") as sb:
sb.uc_gui_click_captcha()
sb.sleep(0.5)
channel_name = "michaelmintz"
sb.cdp.press_keys('input[name="query"]', channel_name)
sb.cdp.click('form[action*="/search"] button')
sb.sleep(2)
sb.cdp.click('a[title="%s"] h2' % channel_name)
channel_title = "Michael Mintz"
sb.cdp.press_keys('input[placeholder*="Search"]', channel_name)
sb.sleep(1.5)
sb.cdp.click('a:contains("%s")' % channel_title)
sb.sleep(2)
sb.cdp.remove_elements("#lngtd-top-sticky")
sb.sleep(1.5)
name = sb.cdp.get_text("h1")
link = sb.cdp.get_attribute("#YouTubeUserTopInfoBlockTop h4 a", "href")
subscribers = sb.cdp.get_text("#youtube-stats-header-subs")
video_views = sb.cdp.get_text("#youtube-stats-header-views")
rankings = sb.cdp.get_text(
'#socialblade-user-content [style*="border-bottom"]'
).replace("\xa0", "").replace(" ", " ").replace(" ", " ")
source = sb.get_page_source()
base = "https://www.youtube.com/c/"
base2 = 'href="/youtube/c/'
start = source.find(base2) + len(base2)
end = source.find('"', start)
link = base + source[start:end]
print("********** SocialBlade Stats for %s: **********" % name)
print(">>> (Link: %s) <<<" % link)
print("* YouTube Subscribers: %s" % subscribers)
print("* YouTube Video Views: %s" % video_views)
print(sb.get_text('[class*="grid lg:hidden"]'))
print("********** SocialBlade Ranks: **********")
for row in rankings.split("\n"):
if len(row.strip()) > 8:
print("--> " + row.strip())
print(sb.get_text('[class*="gap-3 flex-1"]'))
for i in range(17):
sb.cdp.scroll_down(6)
sb.sleep(0.1)

View File

@ -5,6 +5,7 @@ with SB(uc=True, test=True, locale="en", ad_block=True) as sb:
url = "https://architectureofcities.com/roman-theaters"
sb.activate_cdp_mode(url)
sb.cdp.click_if_visible("#cn-close-notice")
sb.cdp.click_if_visible('span:contains("Continue")')
sb.sleep(1)
print("*** " + sb.cdp.get_text("h1") + " ***")
for item in sb.cdp.find_elements("h3"):

View File

@ -0,0 +1,34 @@
"""Timezone example using CDP Mode without WebDriver"""
import mycdp
from seleniumbase import decorators
from seleniumbase import sb_cdp
async def request_paused_handler(event, tab):
r = event.request
is_image = ".png" in r.url or ".jpg" in r.url or ".gif" in r.url
if not is_image: # Let the data through
tab.feed_cdp(mycdp.fetch.continue_request(request_id=event.request_id))
else: # Block the data (images)
TIMED_OUT = mycdp.network.ErrorReason.TIMED_OUT
tab.feed_cdp(mycdp.fetch.fail_request(event.request_id, TIMED_OUT))
@decorators.print_runtime("Timezone CDP Example")
def main():
url = "https://www.randymajors.org/what-time-zone-am-i-in"
sb = sb_cdp.Chrome(
url,
ad_block=True,
lang="bn",
tzone="Asia/Kolkata",
geoloc=(26.855323, 80.937710)
)
sb.add_handler(mycdp.fetch.RequestPaused, request_paused_handler)
sb.remove_elements("#right-sidebar")
sb.remove_elements('[id*="Footer"]')
sb.sleep(6)
if __name__ == "__main__":
main()

View File

@ -5,6 +5,7 @@ with SB(uc=True, test=True, ad_block=True) as sb:
sb.activate_cdp_mode(url)
sb.sleep(2.5)
sb.cdp.click_if_visible('[data-automation-id*="close-mark"]')
sb.sleep(0.3)
sb.cdp.mouse_click('input[aria-label="Search"]')
sb.sleep(1.2)
search = "Settlers of Catan Board Game"
@ -19,7 +20,7 @@ with SB(uc=True, test=True, ad_block=True) as sb:
for item in items:
if required_text in item.text:
description = item.querySelector(
'[data-automation-id="product-price"] + span'
'[data-automation-id="product-title"]'
)
if description and description.text not in unique_item_text:
unique_item_text.append(description.text)

View File

@ -521,6 +521,7 @@ class UCPresentationClass(BaseCase):
sb.activate_cdp_mode(url)
sb.sleep(2.5)
sb.cdp.click_if_visible('[data-automation-id*="close-mark"]')
sb.sleep(0.3)
sb.cdp.mouse_click('input[aria-label="Search"]')
sb.sleep(1.2)
search = "Settlers of Catan Board Game"
@ -535,7 +536,7 @@ class UCPresentationClass(BaseCase):
for item in items:
if required_text in item.text:
description = item.querySelector(
'[data-automation-id="product-price"] + span'
'[data-automation-id="product-title"]'
)
if (
description

View File

@ -1,3 +1,4 @@
from contextlib import suppress
from seleniumbase import BaseCase
BaseCase.main(__name__, __file__)
@ -7,6 +8,54 @@ class UCPresentationClass(BaseCase):
self.open("data:,")
self.set_window_position(4, 40)
self._output_file_saves = False
self.create_presentation(theme="serif", transition="none")
self.add_slide("<h2>Press SPACE to continue!</h2>\n")
self.add_slide(
"<h3><b>Before we begin</b></h3><hr />"
"<p><b>(Here's the GitHub page)</b></p>",
image="https://seleniumbase.io/other/sbase_qr_code.png",
)
self.begin_presentation(filename="uc_presentation.html")
with suppress(Exception):
self.open("https://www.bostoncodecamp.com/CC38/info")
self.create_tour(theme="hopscotch")
self.add_tour_step(
"<h2>Good Afternoon and Welcome!</h2>", 'h1.wow'
)
self.add_tour_step(
"<h4>PSA: Visit our sponsors later.</h4>",
'[href*="/Sponsors"]',
)
self.add_tour_step(
"<h4>Let's check out the schedule...</h4>",
'[href*="/Schedule/SessionGrid"]'
)
self.play_tour()
with suppress(Exception):
self.open(
"https://www.bostoncodecamp.com/CC38/Schedule/SessionGrid"
)
self.highlight("h2", loops=8)
if self.is_element_visible('[data-sessionid="869465"]'):
self.highlight(
'div[data-sessionid="869465"]', loops=10, scroll=False
)
self.create_tour(theme="driverjs")
self.add_tour_step(
"<h2>Here we are</h2>", '[data-sessionid="869465"]'
)
self.play_tour()
self.click('a[onclick*="869465"]')
self.create_tour(theme="hopscotch")
self.add_tour_step(
"<h2>What to expect</h2>",
"div.sz-modal-session",
alignment="left",
)
self.play_tour()
self.create_presentation(theme="serif", transition="none")
self.add_slide("<h2>Press SPACE to begin!</h2>\n")
self.add_slide(
@ -244,4 +293,9 @@ class UCPresentationClass(BaseCase):
'<img src="https://seleniumbase.io/other/hackers_at_comp.jpg"'
' width="70%">'
)
self.add_slide(
"<h3><b>Live Demo Time!</b></h3><hr />"
"<h3>(Let's head over to GitHub...)</h3>",
image="https://seleniumbase.io/other/sbase_qr_code.png",
)
self.begin_presentation(filename="uc_presentation.html")

View File

@ -6,7 +6,7 @@ try:
url = "seleniumbase.io/apps/turnstile"
driver.uc_open_with_reconnect(url, 2)
driver.uc_gui_handle_captcha()
driver.sleep(3)
driver.sleep(2)
pprint(driver.get_log("performance"))
finally:
driver.quit()

View File

@ -1,12 +1,18 @@
from seleniumbase import SB
with SB(uc=True, incognito=True, test=True) as sb:
sb.driver.uc_open_with_reconnect("https://pixelscan.net/", 7)
sb.remove_elements(".bg-bannerBg") # Remove the top banner
sb.remove_elements("pxlscn-ad1") # Remove the ad banner
sb.driver.uc_open_with_reconnect("https://pixelscan.net/", 2)
sb.uc_click('button[class*="startButton"]', reconnect_time=20)
sb.remove_elements(".bg-bannerBg") # Remove top banner
sb.remove_elements("pxlscn-ad1") # Remove an ad banner
sb.remove_elements("pxlscn-ad2") # Remove an ad banner
sb.remove_elements("jdiv") # Remove chat widgets
no_automation_detected = "No automation framework detected"
sb.assert_text(no_automation_detected, "pxlscn-bot-detection")
sb.assert_text(
no_automation_detected,
"pxlscn-bot-detection",
timeout=20,
)
not_masking_text = "You are not masking your fingerprint"
sb.assert_text(not_masking_text, "pxlscn-fingerprint-masking")
consistent_selector = 'div.bg-consistentBg [alt="Good"]'

View File

@ -3,9 +3,9 @@
regex>=2024.11.6
pymdown-extensions>=10.14.3
pipdeptree>=2.26.0
pipdeptree>=2.26.1
python-dateutil>=2.8.2
Markdown==3.7
Markdown==3.8
click==8.1.8
ghp-import==2.1.0
watchdog==6.0.0
@ -14,7 +14,7 @@ pathspec==0.12.1
Babel==2.17.0
paginate==0.5.7
mkdocs==1.6.1
mkdocs-material==9.6.9
mkdocs-material==9.6.12
mkdocs-exclude-search==0.6.6
mkdocs-simple-hooks==0.1.5
mkdocs-material-extensions==1.3.1

View File

@ -1,7 +1,7 @@
pip>=25.0.1
packaging>=24.2
packaging>=25.0
setuptools~=70.2;python_version<"3.10"
setuptools>=78.1.0;python_version>="3.10"
setuptools>=79.0.1;python_version>="3.10"
wheel>=0.45.1
attrs>=25.3.0
certifi>=2025.1.31
@ -11,11 +11,11 @@ websockets>=15.0.1;python_version>="3.9"
filelock~=3.16.1;python_version<"3.9"
filelock>=3.18.0;python_version>="3.9"
fasteners>=0.19
mycdp>=1.1.1
mycdp>=1.2.0
pynose>=1.5.4
platformdirs>=4.3.6;python_version<"3.9"
platformdirs>=4.3.7;python_version>="3.9"
typing-extensions>=4.13.0
typing-extensions>=4.13.2
sbvirtualdisplay>=1.4.0
MarkupSafe==2.1.5;python_version<"3.9"
MarkupSafe>=3.0.2;python_version>="3.9"
@ -33,18 +33,18 @@ idna==3.10
chardet==5.2.0
charset-normalizer==3.4.1
urllib3>=1.26.20,<2;python_version<"3.10"
urllib3>=1.26.20,<2.4.0;python_version>="3.10"
urllib3>=1.26.20,<2.5.0;python_version>="3.10"
requests==2.32.3
sniffio==1.3.1
h11==0.14.0
h11==0.16.0
outcome==1.3.0.post0
trio==0.27.0;python_version<"3.9"
trio==0.29.0;python_version>="3.9"
trio==0.30.0;python_version>="3.9"
trio-websocket==0.12.2
wsproto==1.2.0
websocket-client==1.8.0
selenium==4.27.1;python_version<"3.9"
selenium==4.30.0;python_version>="3.9"
selenium==4.31.0;python_version>="3.9"
cssselect==1.2.0;python_version<"3.9"
cssselect==1.3.0;python_version>="3.9"
sortedcontainers==2.4.0
@ -60,25 +60,25 @@ pytest-rerunfailures==15.0;python_version>="3.9"
pytest-xdist==3.6.1
parameterized==0.9.0
behave==1.2.6
soupsieve==2.6
beautifulsoup4==4.13.3
soupsieve==2.7
beautifulsoup4==4.13.4
pyotp==2.9.0
python-xlib==0.33;platform_system=="Linux"
markdown-it-py==3.0.0
mdurl==0.1.2
rich==13.9.4
rich>=14.0.0,<15
# --- Testing Requirements --- #
# ("pip install -r requirements.txt" also installs this, but "pip install -e ." won't.)
coverage>=7.6.1;python_version<"3.9"
coverage>=7.7.1;python_version>="3.9"
coverage>=7.8.0;python_version>="3.9"
pytest-cov>=5.0.0;python_version<"3.9"
pytest-cov>=6.0.0;python_version>="3.9"
pytest-cov>=6.1.1;python_version>="3.9"
flake8==5.0.4;python_version<"3.9"
flake8==7.1.2;python_version>="3.9"
flake8==7.2.0;python_version>="3.9"
mccabe==0.7.0
pyflakes==2.5.0;python_version<"3.9"
pyflakes==3.2.0;python_version>="3.9"
pyflakes==3.3.2;python_version>="3.9"
pycodestyle==2.9.1;python_version<"3.9"
pycodestyle==2.12.1;python_version>="3.9"
pycodestyle==2.13.0;python_version>="3.9"

View File

@ -1,2 +1,2 @@
# seleniumbase package
__version__ = "4.36.4"
__version__ = "4.37.7"

View File

@ -682,6 +682,7 @@ def uc_open_with_cdp_mode(driver, url=None):
cdp.gui_click_element = CDPM.gui_click_element
cdp.gui_drag_drop_points = CDPM.gui_drag_drop_points
cdp.gui_drag_and_drop = CDPM.gui_drag_and_drop
cdp.gui_click_and_hold = CDPM.gui_click_and_hold
cdp.gui_hover_x_y = CDPM.gui_hover_x_y
cdp.gui_hover_element = CDPM.gui_hover_element
cdp.gui_hover_and_click = CDPM.gui_hover_and_click

View File

@ -92,5 +92,5 @@ class S3LoggingBucket(object):
"""Keep a record of all file names that have been uploaded.
Upload log files related to each test after its execution.
Once done, use already_uploaded_files to create an index file."""
global already_uploaded_files
global already_uploaded_files # noqa
already_uploaded_files.extend(files)

View File

@ -1227,15 +1227,23 @@ class CDPMethods():
if not timeout:
timeout = settings.SMALL_TIMEOUT
selector = self.__convert_to_css_if_xpath(selector)
self.select(selector, timeout=timeout)
element = self.select(selector, timeout=timeout)
self.__add_light_pause()
coordinates = self.loop.run_until_complete(
self.page.js_dumps(
"""document.querySelector"""
"""('%s').getBoundingClientRect()"""
% js_utils.escape_quotes_if_needed(re.escape(selector))
coordinates = None
if ":contains(" in selector:
position = element.get_position()
x = position.x
y = position.y
width = position.width
height = position.height
coordinates = {"x": x, "y": y, "width": width, "height": height}
else:
coordinates = self.loop.run_until_complete(
self.page.js_dumps(
"""document.querySelector('%s').getBoundingClientRect()"""
% js_utils.escape_quotes_if_needed(re.escape(selector))
)
)
)
return coordinates
def get_element_size(self, selector, timeout=None):
@ -1614,6 +1622,8 @@ class CDPMethods():
pyautogui.dragTo(x2, y2, button="left", duration=timeframe)
def gui_drag_drop_points(self, x1, y1, x2, y2, timeframe=0.35):
"""Use PyAutoGUI to drag-and-drop from one point to another.
Can simulate click-and-hold when using the same point twice."""
gui_lock = fasteners.InterProcessLock(
constants.MultiBrowser.PYAUTOGUILOCK
)
@ -1653,6 +1663,8 @@ class CDPMethods():
self.loop.run_until_complete(self.page.wait())
def gui_drag_and_drop(self, drag_selector, drop_selector, timeframe=0.35):
"""Use PyAutoGUI to drag-and-drop from one selector to another.
Can simulate click-and-hold when using the same selector twice."""
self.__slow_mode_pause_if_set()
self.bring_active_window_to_front()
x1, y1 = self.get_gui_element_center(drag_selector)
@ -1661,6 +1673,14 @@ class CDPMethods():
self.__add_light_pause()
self.gui_drag_drop_points(x1, y1, x2, y2, timeframe=timeframe)
def gui_click_and_hold(self, selector, timeframe=0.35):
"""Use PyAutoGUI to click-and-hold a selector."""
self.__slow_mode_pause_if_set()
self.bring_active_window_to_front()
x, y = self.get_gui_element_center(selector)
self.__add_light_pause()
self.gui_drag_drop_points(x, y, x, y, timeframe=timeframe)
def __gui_hover_x_y(self, x, y, timeframe=0.25, uc_lock=False):
self.__install_pyautogui_if_missing()
import pyautogui

View File

@ -162,6 +162,7 @@ class BaseCase(unittest.TestCase):
self.__jqc_default_theme = None
self.__jqc_default_color = None
self.__jqc_default_width = None
self.__saved_id = None
# Requires self._* instead of self.__* for external class use
self._language = "English"
self._presentation_slides = {}
@ -4871,6 +4872,13 @@ class BaseCase(unittest.TestCase):
def activate_cdp_mode(self, url=None):
if hasattr(self.driver, "_is_using_uc") and self.driver._is_using_uc:
if self.__is_cdp_swap_needed():
return # CDP Mode is already active
if not self.is_connected():
self.driver.connect()
current_url = self.get_current_url()
if not current_url.startswith(("about", "data", "chrome")):
self.get_new_driver(undetectable=True)
self.driver.uc_open_with_cdp_mode(url)
else:
self.get_new_driver(undetectable=True)
@ -15669,19 +15677,24 @@ class BaseCase(unittest.TestCase):
test_id = "%s.%s" % (file_name, scenario_name)
return test_id
elif hasattr(self, "is_context_manager") and self.is_context_manager:
if hasattr(self, "_manager_saved_id"):
self.__saved_id = self._manager_saved_id
if self.__saved_id:
return self.__saved_id
filename = self.__class__.__module__.split(".")[-1] + ".py"
methodname = self._testMethodName
context_id = None
if filename == "base_case.py" or methodname == "runTest":
import traceback
stack_base = traceback.format_stack()[0].split(", in ")[0]
test_base = stack_base.split(", in ")[0].split(os.sep)[-1]
stack_base = traceback.format_stack()[0].split(os.sep)[-1]
test_base = stack_base.split(", in ")[0]
if hasattr(self, "cm_filename") and self.cm_filename:
filename = self.cm_filename
else:
filename = test_base.split('"')[0]
methodname = ".line_" + test_base.split(", line ")[-1]
context_id = filename.split(".")[0] + methodname
self.__saved_id = context_id
return context_id
test_id = "%s.%s.%s" % (
self.__class__.__module__,

View File

@ -23,7 +23,7 @@ with SB(uc=True) as sb: # Many args! Eg. SB(browser="edge")
#########################################
"""
from contextlib import contextmanager
from contextlib import contextmanager, suppress
@contextmanager # Usage: -> ``with SB() as sb:``
@ -258,6 +258,7 @@ def SB(
time_limit (float): SECONDS (Safely fail tests that exceed the time limit)
"""
import colorama
import gc
import os
import sys
import time
@ -1231,6 +1232,15 @@ def SB(
sb.cap_file = sb_config.cap_file
sb.cap_string = sb_config.cap_string
sb._has_failure = False # This may change
with suppress(Exception):
stack_base = traceback.format_stack()[0].split(os.sep)[-1]
test_base = stack_base.split(", in ")[0]
filename = test_base.split('"')[0]
methodname = ".line_" + test_base.split(", line ")[-1]
context_id = filename.split(".")[0] + methodname
sb._manager_saved_id = context_id
if hasattr(sb_config, "headless_active"):
sb.headless_active = sb_config.headless_active
else:
@ -1357,6 +1367,13 @@ def SB(
"%s%s%s%s%s"
% (c1, left_space, end_text, right_space, cr)
)
if hasattr(sb_config, "_cdp_aclose"):
import asyncio
with suppress(Exception):
loop = asyncio.get_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(sb_config._cdp_aclose())
gc.collect()
if test and test_name and not test_passed and raise_test_failure:
raise exception
elif (

View File

@ -145,9 +145,14 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
debug_port = 9222
special_port_free = False # If the port isn't free, don't use 9222
try:
res = requests.get("http://127.0.0.1:9222", timeout=1)
if res.status_code != 200:
raise Exception("The port is free! It will be used!")
with requests.Session() as session:
res = session.get(
"http://127.0.0.1:9222",
headers={"Connection": "close"},
timeout=2,
)
if res.status_code != 200:
raise Exception("The port is free! It will be used!")
except Exception:
# Use port 9222, which outputs to chrome://inspect/#devices
special_port_free = True
@ -397,7 +402,8 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
def add_cdp_listener(self, event_name, callback):
if (
self.reactor
hasattr(self, "reactor")
and self.reactor
and self.reactor is not None
and isinstance(self.reactor, Reactor)
):
@ -406,7 +412,11 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
return False
def clear_cdp_listeners(self):
if self.reactor and isinstance(self.reactor, Reactor):
if (
hasattr(self, "reactor")
and self.reactor
and isinstance(self.reactor, Reactor)
):
self.reactor.handlers.clear()
def window_new(self, url=None):
@ -441,7 +451,13 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
with suppress(Exception):
if self.service.is_connectable():
self.stop_client()
self.service.stop()
try:
self.service.send_remote_shutdown_command()
except TypeError:
pass
finally:
with suppress(Exception):
self.service._terminate_process()
if isinstance(timeout, str):
if timeout.lower() == "breakpoint":
breakpoint() # To continue:
@ -456,9 +472,7 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
with suppress(Exception):
for window_handle in self.window_handles:
self.switch_to.window(window_handle)
if self.current_url.startswith(
"chrome-extension://"
):
if self.current_url.startswith("chrome-extension://"):
# https://issues.chromium.org/issues/396611138
# (Remove the Linux conditional when resolved)
# (So that close() is always called)
@ -466,7 +480,13 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
self.close()
if self.service.is_connectable():
self.stop_client()
self.service.stop()
try:
self.service.send_remote_shutdown_command()
except TypeError:
pass
finally:
with suppress(Exception):
self.service._terminate_process()
self.service.start()
self.start_session()
time.sleep(0.003)
@ -482,7 +502,13 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
if self.service.is_connectable():
self.stop_client()
time.sleep(0.003)
self.service.stop()
try:
self.service.send_remote_shutdown_command()
except TypeError:
pass
finally:
with suppress(Exception):
self.service._terminate_process()
self._is_connected = False
def connect(self):
@ -507,7 +533,13 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
self.close()
if self.service.is_connectable():
self.stop_client()
self.service.stop()
try:
self.service.send_remote_shutdown_command()
except TypeError:
pass
finally:
with suppress(Exception):
self.service._terminate_process()
self.service.start()
self.start_session()
time.sleep(0.003)
@ -535,15 +567,35 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
logger.debug(e, exc_info=True)
except Exception:
pass
with suppress(Exception):
self.stop_client()
with suppress(Exception):
if hasattr(self, "command_executor") and self.command_executor:
self.command_executor.close()
# Remove instance reference to allow garbage collection
Chrome._instances.discard(self)
if hasattr(self, "service") and getattr(self.service, "process", None):
logger.debug("Stopping webdriver service")
with suppress(Exception):
self.stop_client()
self.service.stop()
with suppress(Exception):
if self.reactor and isinstance(self.reactor, Reactor):
logger.debug("Shutting down Reactor")
try:
self.service.send_remote_shutdown_command()
except TypeError:
pass
finally:
with suppress(Exception):
self.service._terminate_process()
if (
hasattr(self, "reactor")
and self.reactor
and hasattr(self.reactor, "event")
):
logger.debug("Shutting down Reactor")
with suppress(Exception):
self.reactor.event.set()
self.reactor.join(timeout=2)
self.reactor = None
if (
hasattr(self, "keep_user_data_dir")
and hasattr(self, "user_data_dir")

View File

@ -53,28 +53,58 @@ class CDP:
self._session = requests.Session()
self._last_resp = None
self._last_json = None
resp = self.get(self.endpoints.json)
self.sessionId = resp[0]["id"]
self.wsurl = resp[0]["webSocketDebuggerUrl"]
with requests.Session() as session:
resp = session.get(
self.server_addr + self.endpoints.json,
headers={"Connection": "close"},
timeout=2,
)
self.sessionId = resp.json()[0]["id"]
self.wsurl = resp.json()[0]["webSocketDebuggerUrl"]
def tab_activate(self, id=None):
if not id:
active_tab = self.tab_list()[0]
id = active_tab.id
self.wsurl = active_tab.webSocketDebuggerUrl
return self.post(self.endpoints["activate"].format(id=id))
with requests.Session() as session:
resp = session.post(
self.server_addr + self.endpoints["activate"].format(id=id),
headers={"Connection": "close"},
timeout=2,
)
return resp.json()
def tab_list(self):
retval = self.get(self.endpoints["list"])
return [PageElement(o) for o in retval]
with requests.Session() as session:
resp = session.get(
self.server_addr + self.endpoints["list"],
headers={"Connection": "close"},
timeout=2,
)
retval = resp.json()
return [PageElement(o) for o in retval]
def tab_new(self, url):
return self.post(self.endpoints["new"].format(url=url))
with requests.Session() as session:
resp = session.post(
self.server_addr + self.endpoints["new"].format(url=url),
headers={"Connection": "close"},
timeout=2,
)
return resp.json()
def tab_close_last_opened(self):
sessions = self.tab_list()
opentabs = [s for s in sessions if s["type"] == "page"]
return self.post(self.endpoints["close"].format(id=opentabs[-1]["id"]))
with requests.Session() as session:
endp_close = self.endpoints["close"]
resp = session.post(
self.server_addr + endp_close.format(id=opentabs[-1]["id"]),
headers={"Connection": "close"},
timeout=2,
)
return resp.json()
async def send(self, method, params):
pip_find_lock = fasteners.InterProcessLock(
@ -101,14 +131,19 @@ class CDP:
from urllib.parse import unquote
uri = unquote(uri, errors="strict")
resp = self._session.get(self.server_addr + uri)
try:
self._last_resp = resp
self._last_json = resp.json()
except Exception:
return
else:
return self._last_json
with requests.Session() as session:
resp = session.get(
self.server_addr + uri,
headers={"Connection": "close"},
timeout=2,
)
try:
self._last_resp = resp
self._last_json = resp.json()
except Exception:
return
else:
return self._last_json
def post(self, uri, data=None):
from urllib.parse import unquote
@ -116,12 +151,18 @@ class CDP:
uri = unquote(uri, errors="strict")
if not data:
data = {}
resp = self._session.post(self.server_addr + uri, json=data)
try:
self._last_resp = resp
self._last_json = resp.json()
except Exception:
return self._last_resp
with requests.Session() as session:
resp = session.post(
self.server_addr + uri,
json=data,
headers={"Connection": "close"},
timeout=2,
)
try:
self._last_resp = resp
self._last_json = resp.json()
except Exception:
return self._last_resp
@property
def last_json(self):

View File

@ -287,10 +287,63 @@ class Browser:
connection: tab.Tab = next(
filter(lambda item: item.type_ == "page", self.targets)
)
# Use the tab to navigate to new url
if hasattr(sb_config, "_cdp_locale") and sb_config._cdp_locale:
await connection.send(cdp.page.navigate("about:blank"))
await connection.set_locale(sb_config._cdp_locale)
if (
hasattr(sb_config, "_cdp_user_agent")
and sb_config._cdp_user_agent
):
pass
elif (
hasattr(sb_config, "_cdp_platform")
and sb_config._cdp_platform
):
pass
else:
await connection.set_locale(sb_config._cdp_locale)
if hasattr(sb_config, "_cdp_timezone") and sb_config._cdp_timezone:
await connection.send(cdp.page.navigate("about:blank"))
await connection.set_timezone(sb_config._cdp_timezone)
if (
hasattr(sb_config, "_cdp_user_agent")
and sb_config._cdp_user_agent
):
await connection.send(cdp.page.navigate("about:blank"))
if hasattr(sb_config, "_cdp_locale") and sb_config._cdp_locale:
_cdp_platform = None
if (
hasattr(sb_config, "_cdp_platform")
and sb_config._cdp_platform
):
_cdp_platform = sb_config._cdp_platform
await connection.set_user_agent(
sb_config._cdp_user_agent,
sb_config._cdp_locale,
_cdp_platform,
)
else:
await connection.set_user_agent(sb_config._cdp_user_agent)
elif (
hasattr(sb_config, "_cdp_platform") and sb_config._cdp_platform
):
await connection.send(cdp.page.navigate("about:blank"))
if hasattr(sb_config, "_cdp_locale") and sb_config._cdp_locale:
_cdp_platform = sb_config._cdp_platform
await connection.set_user_agent(
accept_language=sb_config._cdp_locale,
platform=_cdp_platform,
)
else:
await connection.set_user_agent(
platform=sb_config._cdp_platform
)
if (
hasattr(sb_config, "_cdp_geolocation")
and sb_config._cdp_geolocation
):
await connection.send(cdp.page.navigate("about:blank"))
await connection.set_geolocation(sb_config._cdp_geolocation)
# Use the tab to navigate to new url
frame_id, loader_id, *_ = await connection.send(
cdp.page.navigate(url)
)

View File

@ -10,8 +10,10 @@ import types
import typing
from contextlib import suppress
from seleniumbase import config as sb_config
from seleniumbase import extensions
from seleniumbase.config import settings
from seleniumbase.core import detect_b_ver
from seleniumbase.core import download_helper
from seleniumbase.core import proxy_helper
from seleniumbase.fixtures import constants
from seleniumbase.fixtures import shared_utils
@ -25,7 +27,10 @@ import mycdp as cdp
logger = logging.getLogger(__name__)
IS_LINUX = shared_utils.is_linux()
DOWNLOADS_FOLDER = download_helper.get_downloads_folder()
PROXY_DIR_LOCK = proxy_helper.PROXY_DIR_LOCK
EXTENSIONS_DIR = os.path.dirname(os.path.realpath(extensions.__file__))
AD_BLOCK_ZIP_PATH = os.path.join(EXTENSIONS_DIR, "ad_block.zip")
T = typing.TypeVar("T")
@ -168,6 +173,19 @@ def __add_chrome_ext_dir(extension_dir, dir_path):
return extension_dir
def __unzip_to_new_folder(zip_file, folder):
proxy_dir_lock = fasteners.InterProcessLock(PROXY_DIR_LOCK)
with proxy_dir_lock:
with suppress(Exception):
shared_utils.make_writable(PROXY_DIR_LOCK)
if not os.path.exists(folder):
import zipfile
zip_ref = zipfile.ZipFile(zip_file, "r")
os.makedirs(folder)
zip_ref.extractall(folder)
zip_ref.close()
def __add_chrome_proxy_extension(
extension_dir,
proxy_string,
@ -231,6 +249,7 @@ async def start(
browser_executable_path: Optional[PathLike] = None,
browser_args: Optional[List[str]] = None,
xvfb_metrics: Optional[List[str]] = None, # "Width,Height" for Linux
ad_block: Optional[bool] = False,
sandbox: Optional[bool] = True,
lang: Optional[str] = None, # Set the Language Locale Code
host: Optional[str] = None, # Chrome remote-debugging-host
@ -238,7 +257,10 @@ async def start(
xvfb: Optional[int] = None, # Use a special virtual display on Linux
headed: Optional[bool] = None, # Override default Xvfb mode on Linux
expert: Optional[bool] = None, # Open up closed Shadow-root elements
agent: Optional[str] = None, # Set the user-agent string
proxy: Optional[str] = None, # "host:port" or "user:pass@host:port"
tzone: Optional[str] = None, # Eg "America/New_York", "Asia/Kolkata"
geoloc: Optional[list | tuple] = None, # Eg (48.87645, 2.26340)
extension_dir: Optional[str] = None, # Chrome extension directory
**kwargs: Optional[dict],
) -> Browser:
@ -296,6 +318,13 @@ async def start(
proxy_user,
proxy_pass,
)
if ad_block:
incognito = False
guest = False
ad_block_zip = AD_BLOCK_ZIP_PATH
ad_block_dir = os.path.join(DOWNLOADS_FOLDER, "ad_block")
__unzip_to_new_folder(ad_block_zip, ad_block_dir)
extension_dir = __add_chrome_ext_dir(extension_dir, ad_block_dir)
if not config:
config = Config(
user_data_dir,
@ -329,6 +358,26 @@ async def start(
sb_config._cdp_locale = kwargs["locale_code"]
else:
sb_config._cdp_locale = None
if tzone:
sb_config._cdp_timezone = tzone
elif "timezone" in kwargs:
sb_config._cdp_timezone = kwargs["timezone"]
else:
sb_config._cdp_timezone = None
if geoloc:
sb_config._cdp_geolocation = geoloc
elif "geolocation" in kwargs:
sb_config._cdp_geolocation = kwargs["geolocation"]
else:
sb_config._cdp_geolocation = None
if agent:
sb_config._cdp_user_agent = agent
elif "user_agent" in kwargs:
sb_config._cdp_user_agent = kwargs["user_agent"]
else:
sb_config._cdp_user_agent = None
if "platform" in kwargs:
sb_config._cdp_platform = kwargs["platform"]
return driver
@ -406,7 +455,13 @@ async def create_from_driver(driver) -> Browser:
browser = await start(conf)
browser._process_pid = driver.browser_pid
# Stop chromedriver binary
driver.service.stop()
try:
driver.service.send_remote_shutdown_command()
except TypeError:
pass
finally:
with suppress(Exception):
driver.service._terminate_process()
driver.browser_pid = -1
driver.user_data_dir = None
return browser

View File

@ -19,6 +19,7 @@ from typing import (
)
import websockets
from websockets.protocol import State
from seleniumbase import config as sb_config
from . import cdp_util as util
import mycdp as cdp
import mycdp.network
@ -270,6 +271,7 @@ class Connection(metaclass=CantTouchThis):
max_size=MAX_SIZE,
)
self.listener = Listener(self)
sb_config._cdp_aclose = self.aclose
except (Exception,) as e:
logger.debug("Exception during opening of websocket: %s", e)
if self.listener:
@ -290,10 +292,6 @@ class Connection(metaclass=CantTouchThis):
Closes the websocket connection. Shouldn't be called manually by users.
"""
if self.websocket and self.websocket.state is not State.CLOSED:
if self.listener and self.listener.running:
self.listener.cancel()
self.enabled_domains.clear()
await asyncio.sleep(0.015)
try:
await self.websocket.close()
except Exception:
@ -301,6 +299,9 @@ class Connection(metaclass=CantTouchThis):
"\n❌ Error closing websocket connection to %s",
self.websocket_url
)
if self.listener and self.listener.running:
self.listener.cancel()
self.enabled_domains.clear()
logger.debug(
"\n❌ Closed websocket connection to %s", self.websocket_url
)
@ -347,7 +348,37 @@ class Connection(metaclass=CantTouchThis):
async def set_locale(self, locale: Optional[str] = None):
"""Sets the Language Locale code via set_user_agent_override."""
await self.send(cdp.network.set_user_agent_override("", locale))
await self.set_user_agent(user_agent="", accept_language=locale)
async def set_timezone(self, timezone: Optional[str] = None):
"""Sets the Timezone via set_timezone_override."""
await self.send(cdp.emulation.set_timezone_override(timezone))
async def set_user_agent(
self,
user_agent: Optional[str] = "",
accept_language: Optional[str] = None,
platform: Optional[str] = None, # navigator.platform
):
"""Sets the User Agent via set_user_agent_override."""
if not user_agent:
user_agent = ""
await self.send(cdp.network.set_user_agent_override(
user_agent=user_agent,
accept_language=accept_language,
platform=platform,
))
async def set_geolocation(self, geolocation: Optional[tuple] = None):
"""Sets the User Agent via set_geolocation_override."""
await self.send(cdp.browser.grant_permissions(
permissions=["geolocation"],
))
await self.send(cdp.emulation.set_geolocation_override(
latitude=geolocation[0],
longitude=geolocation[1],
accuracy=100,
))
def __getattr__(self, item):
""":meta private:"""
@ -415,6 +446,7 @@ class Connection(metaclass=CantTouchThis):
if not _is_update:
await self._register_handlers()
await self.websocket.send(tx.message)
sb_config._cdp_aclose = self.aclose
try:
return await tx
except ProtocolException as e:
@ -547,8 +579,7 @@ class Listener:
except asyncio.TimeoutError:
self.idle.set()
# Pause for a moment.
# await asyncio.sleep(self.time_before_considered_idle / 10)
await asyncio.sleep(0.015)
await asyncio.sleep(self.time_before_considered_idle / 10)
continue
except (Exception,) as e:
logger.debug(

View File

@ -34,7 +34,7 @@ if sys.argv[-1] == "publish":
print("\nERROR! Publishing to PyPI requires Python>=3.9")
sys.exit()
print("\n*** Checking code health with flake8:\n")
os.system("python -m pip install 'flake8==7.1.2'")
os.system("python -m pip install 'flake8==7.2.0'")
flake8_status = os.system("flake8 --exclude=recordings,temp")
if flake8_status != 0:
print("\nERROR! Fix flake8 issues before publishing to PyPI!\n")
@ -148,9 +148,9 @@ setup(
python_requires=">=3.8",
install_requires=[
'pip>=25.0.1',
'packaging>=24.2',
'packaging>=25.0',
'setuptools~=70.2;python_version<"3.10"', # Newer ones had issues
'setuptools>=78.1.0;python_version>="3.10"',
'setuptools>=79.0.1;python_version>="3.10"',
'wheel>=0.45.1',
'attrs>=25.3.0',
"certifi>=2025.1.31",
@ -160,11 +160,11 @@ setup(
'filelock~=3.16.1;python_version<"3.9"',
'filelock>=3.18.0;python_version>="3.9"',
'fasteners>=0.19',
"mycdp>=1.1.1",
"mycdp>=1.2.0",
"pynose>=1.5.4",
'platformdirs>=4.3.6;python_version<"3.9"',
'platformdirs>=4.3.7;python_version>="3.9"',
'typing-extensions>=4.13.0',
'typing-extensions>=4.13.2',
"sbvirtualdisplay>=1.4.0",
'MarkupSafe==2.1.5;python_version<"3.9"',
'MarkupSafe>=3.0.2;python_version>="3.9"',
@ -182,18 +182,18 @@ setup(
'chardet==5.2.0',
'charset-normalizer==3.4.1',
'urllib3>=1.26.20,<2;python_version<"3.10"',
'urllib3>=1.26.20,<2.4.0;python_version>="3.10"',
'urllib3>=1.26.20,<2.5.0;python_version>="3.10"',
'requests==2.32.3',
'sniffio==1.3.1',
'h11==0.14.0',
'h11==0.16.0',
'outcome==1.3.0.post0',
'trio==0.27.0;python_version<"3.9"',
'trio==0.29.0;python_version>="3.9"',
'trio==0.30.0;python_version>="3.9"',
'trio-websocket==0.12.2',
'wsproto==1.2.0',
'websocket-client==1.8.0',
'selenium==4.27.1;python_version<"3.9"',
'selenium==4.30.0;python_version>="3.9"',
'selenium==4.31.0;python_version>="3.9"',
'cssselect==1.2.0;python_version<"3.9"',
'cssselect==1.3.0;python_version>="3.9"',
"sortedcontainers==2.4.0",
@ -209,13 +209,13 @@ setup(
'pytest-xdist==3.6.1',
'parameterized==0.9.0',
"behave==1.2.6",
'soupsieve==2.6',
"beautifulsoup4==4.13.3",
'soupsieve==2.7',
"beautifulsoup4==4.13.4",
'pyotp==2.9.0',
'python-xlib==0.33;platform_system=="Linux"',
'markdown-it-py==3.0.0',
'mdurl==0.1.2',
'rich==13.9.4',
'rich>=14.0.0,<15',
],
extras_require={
# pip install -e .[allure]
@ -230,20 +230,20 @@ setup(
# Usage: coverage run -m pytest; coverage html; coverage report
"coverage": [
'coverage>=7.6.1;python_version<"3.9"',
'coverage>=7.7.1;python_version>="3.9"',
'coverage>=7.8.0;python_version>="3.9"',
'pytest-cov>=5.0.0;python_version<"3.9"',
'pytest-cov>=6.0.0;python_version>="3.9"',
'pytest-cov>=6.1.1;python_version>="3.9"',
],
# pip install -e .[flake8]
# Usage: flake8
"flake8": [
'flake8==5.0.4;python_version<"3.9"',
'flake8==7.1.2;python_version>="3.9"',
'flake8==7.2.0;python_version>="3.9"',
"mccabe==0.7.0",
'pyflakes==2.5.0;python_version<"3.9"',
'pyflakes==3.2.0;python_version>="3.9"',
'pyflakes==3.3.2;python_version>="3.9"',
'pycodestyle==2.9.1;python_version<"3.9"',
'pycodestyle==2.12.1;python_version>="3.9"',
'pycodestyle==2.13.0;python_version>="3.9"',
],
# pip install -e .[ipdb]
# (Not needed for debugging anymore. SeleniumBase now includes "pdbp".)
@ -261,7 +261,7 @@ setup(
# (An optional library for parsing PDF files.)
"pdfminer": [
'pdfminer.six==20250324;python_version<"3.9"',
'pdfminer.six==20250327;python_version>="3.9"',
'pdfminer.six==20250416;python_version>="3.9"',
'cryptography==39.0.2;python_version<"3.9"',
'cryptography==44.0.2;python_version>="3.9"',
'cffi==1.17.1',
@ -271,7 +271,7 @@ setup(
# (An optional library for image-processing.)
"pillow": [
'Pillow>=10.4.0;python_version<"3.9"',
'Pillow>=11.1.0;python_version>="3.9"',
'Pillow>=11.2.1;python_version>="3.9"',
],
# pip install -e .[pip-system-certs]
# (If you see [SSL: CERTIFICATE_VERIFY_FAILED], then get this.)