Compare commits

..

No commits in common. "master" and "v4.36.4" have entirely different histories.

28 changed files with 131 additions and 544 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/discord/727927627830001734?color=7289DA&label=Discord&logo=discord&logoColor=white"/></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/badge/join-discord-infomational" alt="Join the SeleniumBase chat on Discord"/></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/discord/727927627830001734?color=7289DA&label=Discord&logo=discord&logoColor=white"/></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://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,7 +286,6 @@ 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"
@ -301,7 +300,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-title"]'
'[data-automation-id="product-price"] + span'
)
if description and description.text not in unique_item_text:
unique_item_text.append(description.text)
@ -420,7 +419,7 @@ sb.cdp.js_dumps(obj_name)
sb.cdp.maximize()
sb.cdp.minimize()
sb.cdp.medimize()
sb.cdp.set_window_rect(x, y, width, height)
sb.cdp.set_window_rect()
sb.cdp.reset_window_size()
sb.cdp.open_new_window(url=None, switch_to=True)
sb.cdp.switch_to_window(window)
@ -466,9 +465,8 @@ 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, 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_drag_drop_points(x1, y1, x2, y2)
sb.cdp.gui_drag_and_drop(drag_selector, drop_selector)
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,15 +16,6 @@ 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

@ -1,16 +0,0 @@
"""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,7 +3,6 @@ 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,20 +2,12 @@ from seleniumbase import SB
with SB(uc=True, incognito=True, test=True) as sb:
sb.activate_cdp_mode("https://pixelscan.net/")
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.sleep(3)
sb.remove_elements(".bg-bannerBg") # Remove the top banner
sb.remove_elements("pxlscn-ad1") # Remove the 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",
timeout=20,
)
sb.assert_text(not_masking_text, "pxlscn-fingerprint-masking")
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,25 +8,28 @@ 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"
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.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)
sb.sleep(1.5)
sb.cdp.remove_elements("#lngtd-top-sticky")
sb.sleep(1.5)
name = sb.cdp.get_text("h1")
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]
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(" ", " ")
print("********** SocialBlade Stats for %s: **********" % name)
print(">>> (Link: %s) <<<" % link)
print(sb.get_text('[class*="grid lg:hidden"]'))
print("* YouTube Subscribers: %s" % subscribers)
print("* YouTube Video Views: %s" % video_views)
print("********** SocialBlade Ranks: **********")
print(sb.get_text('[class*="gap-3 flex-1"]'))
for row in rankings.split("\n"):
if len(row.strip()) > 8:
print("--> " + row.strip())
for i in range(17):
sb.cdp.scroll_down(6)
sb.sleep(0.1)

View File

@ -5,7 +5,6 @@ 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

@ -1,34 +0,0 @@
"""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,7 +5,6 @@ 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"
@ -20,7 +19,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-title"]'
'[data-automation-id="product-price"] + span'
)
if description and description.text not in unique_item_text:
unique_item_text.append(description.text)

View File

@ -521,7 +521,6 @@ 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"
@ -536,7 +535,7 @@ class UCPresentationClass(BaseCase):
for item in items:
if required_text in item.text:
description = item.querySelector(
'[data-automation-id="product-title"]'
'[data-automation-id="product-price"] + span'
)
if (
description

View File

@ -1,4 +1,3 @@
from contextlib import suppress
from seleniumbase import BaseCase
BaseCase.main(__name__, __file__)
@ -8,54 +7,6 @@ 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(
@ -293,9 +244,4 @@ 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(2)
driver.sleep(3)
pprint(driver.get_log("performance"))
finally:
driver.quit()

View File

@ -1,18 +1,12 @@
from seleniumbase import SB
with SB(uc=True, incognito=True, test=True) as sb:
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.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.remove_elements("jdiv") # Remove chat widgets
no_automation_detected = "No automation framework detected"
sb.assert_text(
no_automation_detected,
"pxlscn-bot-detection",
timeout=20,
)
sb.assert_text(no_automation_detected, "pxlscn-bot-detection")
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.1
pipdeptree>=2.26.0
python-dateutil>=2.8.2
Markdown==3.8
Markdown==3.7
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.12
mkdocs-material==9.6.9
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>=25.0
packaging>=24.2
setuptools~=70.2;python_version<"3.10"
setuptools>=79.0.1;python_version>="3.10"
setuptools>=78.1.0;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.2.0
mycdp>=1.1.1
pynose>=1.5.4
platformdirs>=4.3.6;python_version<"3.9"
platformdirs>=4.3.7;python_version>="3.9"
typing-extensions>=4.13.2
typing-extensions>=4.13.0
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.5.0;python_version>="3.10"
urllib3>=1.26.20,<2.4.0;python_version>="3.10"
requests==2.32.3
sniffio==1.3.1
h11==0.16.0
h11==0.14.0
outcome==1.3.0.post0
trio==0.27.0;python_version<"3.9"
trio==0.30.0;python_version>="3.9"
trio==0.29.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.31.0;python_version>="3.9"
selenium==4.30.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.7
beautifulsoup4==4.13.4
soupsieve==2.6
beautifulsoup4==4.13.3
pyotp==2.9.0
python-xlib==0.33;platform_system=="Linux"
markdown-it-py==3.0.0
mdurl==0.1.2
rich>=14.0.0,<15
rich==13.9.4
# --- 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.8.0;python_version>="3.9"
coverage>=7.7.1;python_version>="3.9"
pytest-cov>=5.0.0;python_version<"3.9"
pytest-cov>=6.1.1;python_version>="3.9"
pytest-cov>=6.0.0;python_version>="3.9"
flake8==5.0.4;python_version<"3.9"
flake8==7.2.0;python_version>="3.9"
flake8==7.1.2;python_version>="3.9"
mccabe==0.7.0
pyflakes==2.5.0;python_version<"3.9"
pyflakes==3.3.2;python_version>="3.9"
pyflakes==3.2.0;python_version>="3.9"
pycodestyle==2.9.1;python_version<"3.9"
pycodestyle==2.13.0;python_version>="3.9"
pycodestyle==2.12.1;python_version>="3.9"

View File

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

View File

@ -682,7 +682,6 @@ 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 # noqa
global already_uploaded_files
already_uploaded_files.extend(files)

View File

@ -1227,23 +1227,15 @@ class CDPMethods():
if not timeout:
timeout = settings.SMALL_TIMEOUT
selector = self.__convert_to_css_if_xpath(selector)
element = self.select(selector, timeout=timeout)
self.select(selector, timeout=timeout)
self.__add_light_pause()
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))
)
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):
@ -1622,8 +1614,6 @@ 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
)
@ -1663,8 +1653,6 @@ 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)
@ -1673,14 +1661,6 @@ 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,7 +162,6 @@ 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 = {}
@ -4872,13 +4871,6 @@ 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)
@ -15677,24 +15669,19 @@ 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(os.sep)[-1]
test_base = stack_base.split(", in ")[0]
stack_base = traceback.format_stack()[0].split(", in ")[0]
test_base = stack_base.split(", in ")[0].split(os.sep)[-1]
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, suppress
from contextlib import contextmanager
@contextmanager # Usage: -> ``with SB() as sb:``
@ -258,7 +258,6 @@ def SB(
time_limit (float): SECONDS (Safely fail tests that exceed the time limit)
"""
import colorama
import gc
import os
import sys
import time
@ -1232,15 +1231,6 @@ 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:
@ -1367,13 +1357,6 @@ 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,14 +145,9 @@ 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:
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!")
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!")
except Exception:
# Use port 9222, which outputs to chrome://inspect/#devices
special_port_free = True
@ -402,8 +397,7 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
def add_cdp_listener(self, event_name, callback):
if (
hasattr(self, "reactor")
and self.reactor
self.reactor
and self.reactor is not None
and isinstance(self.reactor, Reactor)
):
@ -412,11 +406,7 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
return False
def clear_cdp_listeners(self):
if (
hasattr(self, "reactor")
and self.reactor
and isinstance(self.reactor, Reactor)
):
if self.reactor and isinstance(self.reactor, Reactor):
self.reactor.handlers.clear()
def window_new(self, url=None):
@ -451,13 +441,7 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
with suppress(Exception):
if self.service.is_connectable():
self.stop_client()
try:
self.service.send_remote_shutdown_command()
except TypeError:
pass
finally:
with suppress(Exception):
self.service._terminate_process()
self.service.stop()
if isinstance(timeout, str):
if timeout.lower() == "breakpoint":
breakpoint() # To continue:
@ -472,7 +456,9 @@ 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)
@ -480,13 +466,7 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
self.close()
if self.service.is_connectable():
self.stop_client()
try:
self.service.send_remote_shutdown_command()
except TypeError:
pass
finally:
with suppress(Exception):
self.service._terminate_process()
self.service.stop()
self.service.start()
self.start_session()
time.sleep(0.003)
@ -502,13 +482,7 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
if self.service.is_connectable():
self.stop_client()
time.sleep(0.003)
try:
self.service.send_remote_shutdown_command()
except TypeError:
pass
finally:
with suppress(Exception):
self.service._terminate_process()
self.service.stop()
self._is_connected = False
def connect(self):
@ -533,13 +507,7 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
self.close()
if self.service.is_connectable():
self.stop_client()
try:
self.service.send_remote_shutdown_command()
except TypeError:
pass
finally:
with suppress(Exception):
self.service._terminate_process()
self.service.stop()
self.service.start()
self.start_session()
time.sleep(0.003)
@ -567,35 +535,15 @@ 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):
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.stop_client()
self.service.stop()
with suppress(Exception):
if self.reactor and isinstance(self.reactor, Reactor):
logger.debug("Shutting down Reactor")
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,58 +53,28 @@ class CDP:
self._session = requests.Session()
self._last_resp = None
self._last_json = None
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"]
resp = self.get(self.endpoints.json)
self.sessionId = resp[0]["id"]
self.wsurl = resp[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
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()
return self.post(self.endpoints["activate"].format(id=id))
def tab_list(self):
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]
retval = self.get(self.endpoints["list"])
return [PageElement(o) for o in retval]
def tab_new(self, 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()
return self.post(self.endpoints["new"].format(url=url))
def tab_close_last_opened(self):
sessions = self.tab_list()
opentabs = [s for s in sessions if s["type"] == "page"]
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()
return self.post(self.endpoints["close"].format(id=opentabs[-1]["id"]))
async def send(self, method, params):
pip_find_lock = fasteners.InterProcessLock(
@ -131,19 +101,14 @@ class CDP:
from urllib.parse import unquote
uri = unquote(uri, errors="strict")
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
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
def post(self, uri, data=None):
from urllib.parse import unquote
@ -151,18 +116,12 @@ class CDP:
uri = unquote(uri, errors="strict")
if not data:
data = {}
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
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
@property
def last_json(self):

View File

@ -287,63 +287,10 @@ 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"))
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
await connection.set_locale(sb_config._cdp_locale)
frame_id, loader_id, *_ = await connection.send(
cdp.page.navigate(url)
)

View File

@ -10,10 +10,8 @@ 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
@ -27,10 +25,7 @@ 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")
@ -173,19 +168,6 @@ 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,
@ -249,7 +231,6 @@ 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
@ -257,10 +238,7 @@ 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:
@ -318,13 +296,6 @@ 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,
@ -358,26 +329,6 @@ 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
@ -455,13 +406,7 @@ async def create_from_driver(driver) -> Browser:
browser = await start(conf)
browser._process_pid = driver.browser_pid
# Stop chromedriver binary
try:
driver.service.send_remote_shutdown_command()
except TypeError:
pass
finally:
with suppress(Exception):
driver.service._terminate_process()
driver.service.stop()
driver.browser_pid = -1
driver.user_data_dir = None
return browser

View File

@ -19,7 +19,6 @@ 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
@ -271,7 +270,6 @@ 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:
@ -292,6 +290,10 @@ 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:
@ -299,9 +301,6 @@ 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
)
@ -348,37 +347,7 @@ 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.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,
))
await self.send(cdp.network.set_user_agent_override("", locale))
def __getattr__(self, item):
""":meta private:"""
@ -446,7 +415,6 @@ 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:
@ -579,7 +547,8 @@ class Listener:
except asyncio.TimeoutError:
self.idle.set()
# Pause for a moment.
await asyncio.sleep(self.time_before_considered_idle / 10)
# await asyncio.sleep(self.time_before_considered_idle / 10)
await asyncio.sleep(0.015)
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.2.0'")
os.system("python -m pip install 'flake8==7.1.2'")
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>=25.0',
'packaging>=24.2',
'setuptools~=70.2;python_version<"3.10"', # Newer ones had issues
'setuptools>=79.0.1;python_version>="3.10"',
'setuptools>=78.1.0;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.2.0",
"mycdp>=1.1.1",
"pynose>=1.5.4",
'platformdirs>=4.3.6;python_version<"3.9"',
'platformdirs>=4.3.7;python_version>="3.9"',
'typing-extensions>=4.13.2',
'typing-extensions>=4.13.0',
"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.5.0;python_version>="3.10"',
'urllib3>=1.26.20,<2.4.0;python_version>="3.10"',
'requests==2.32.3',
'sniffio==1.3.1',
'h11==0.16.0',
'h11==0.14.0',
'outcome==1.3.0.post0',
'trio==0.27.0;python_version<"3.9"',
'trio==0.30.0;python_version>="3.9"',
'trio==0.29.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.31.0;python_version>="3.9"',
'selenium==4.30.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.7',
"beautifulsoup4==4.13.4",
'soupsieve==2.6',
"beautifulsoup4==4.13.3",
'pyotp==2.9.0',
'python-xlib==0.33;platform_system=="Linux"',
'markdown-it-py==3.0.0',
'mdurl==0.1.2',
'rich>=14.0.0,<15',
'rich==13.9.4',
],
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.8.0;python_version>="3.9"',
'coverage>=7.7.1;python_version>="3.9"',
'pytest-cov>=5.0.0;python_version<"3.9"',
'pytest-cov>=6.1.1;python_version>="3.9"',
'pytest-cov>=6.0.0;python_version>="3.9"',
],
# pip install -e .[flake8]
# Usage: flake8
"flake8": [
'flake8==5.0.4;python_version<"3.9"',
'flake8==7.2.0;python_version>="3.9"',
'flake8==7.1.2;python_version>="3.9"',
"mccabe==0.7.0",
'pyflakes==2.5.0;python_version<"3.9"',
'pyflakes==3.3.2;python_version>="3.9"',
'pyflakes==3.2.0;python_version>="3.9"',
'pycodestyle==2.9.1;python_version<"3.9"',
'pycodestyle==2.13.0;python_version>="3.9"',
'pycodestyle==2.12.1;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==20250416;python_version>="3.9"',
'pdfminer.six==20250327;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.2.1;python_version>="3.9"',
'Pillow>=11.1.0;python_version>="3.9"',
],
# pip install -e .[pip-system-certs]
# (If you see [SSL: CERTIFICATE_VERIFY_FAILED], then get this.)