seleniumbase 4.41.3__py3-none-any.whl → 4.45.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. sbase/steps.py +9 -0
  2. seleniumbase/__version__.py +1 -1
  3. seleniumbase/behave/behave_helper.py +2 -0
  4. seleniumbase/behave/behave_sb.py +21 -8
  5. seleniumbase/common/decorators.py +3 -1
  6. seleniumbase/console_scripts/run.py +1 -0
  7. seleniumbase/console_scripts/sb_caseplans.py +3 -4
  8. seleniumbase/console_scripts/sb_install.py +142 -11
  9. seleniumbase/console_scripts/sb_mkchart.py +1 -2
  10. seleniumbase/console_scripts/sb_mkdir.py +99 -29
  11. seleniumbase/console_scripts/sb_mkfile.py +1 -2
  12. seleniumbase/console_scripts/sb_mkpres.py +1 -2
  13. seleniumbase/console_scripts/sb_mkrec.py +26 -2
  14. seleniumbase/console_scripts/sb_objectify.py +4 -5
  15. seleniumbase/console_scripts/sb_print.py +1 -1
  16. seleniumbase/console_scripts/sb_recorder.py +40 -3
  17. seleniumbase/core/browser_launcher.py +474 -151
  18. seleniumbase/core/detect_b_ver.py +258 -16
  19. seleniumbase/core/log_helper.py +15 -21
  20. seleniumbase/core/mysql.py +1 -1
  21. seleniumbase/core/recorder_helper.py +3 -0
  22. seleniumbase/core/report_helper.py +9 -12
  23. seleniumbase/core/sb_cdp.py +734 -215
  24. seleniumbase/core/sb_driver.py +46 -5
  25. seleniumbase/core/session_helper.py +2 -4
  26. seleniumbase/core/tour_helper.py +1 -2
  27. seleniumbase/drivers/atlas_drivers/__init__.py +0 -0
  28. seleniumbase/drivers/brave_drivers/__init__.py +0 -0
  29. seleniumbase/drivers/chromium_drivers/__init__.py +0 -0
  30. seleniumbase/drivers/comet_drivers/__init__.py +0 -0
  31. seleniumbase/drivers/opera_drivers/__init__.py +0 -0
  32. seleniumbase/fixtures/base_case.py +448 -251
  33. seleniumbase/fixtures/constants.py +36 -9
  34. seleniumbase/fixtures/js_utils.py +77 -18
  35. seleniumbase/fixtures/page_actions.py +41 -13
  36. seleniumbase/fixtures/page_utils.py +19 -12
  37. seleniumbase/fixtures/shared_utils.py +64 -6
  38. seleniumbase/masterqa/master_qa.py +16 -2
  39. seleniumbase/plugins/base_plugin.py +8 -0
  40. seleniumbase/plugins/basic_test_info.py +2 -3
  41. seleniumbase/plugins/driver_manager.py +131 -5
  42. seleniumbase/plugins/page_source.py +2 -3
  43. seleniumbase/plugins/pytest_plugin.py +244 -79
  44. seleniumbase/plugins/sb_manager.py +143 -20
  45. seleniumbase/plugins/selenium_plugin.py +144 -12
  46. seleniumbase/translate/translator.py +2 -3
  47. seleniumbase/undetected/__init__.py +17 -13
  48. seleniumbase/undetected/cdp.py +1 -12
  49. seleniumbase/undetected/cdp_driver/browser.py +330 -129
  50. seleniumbase/undetected/cdp_driver/cdp_util.py +328 -61
  51. seleniumbase/undetected/cdp_driver/config.py +110 -14
  52. seleniumbase/undetected/cdp_driver/connection.py +18 -48
  53. seleniumbase/undetected/cdp_driver/element.py +105 -33
  54. seleniumbase/undetected/cdp_driver/tab.py +414 -39
  55. seleniumbase/utilities/selenium_grid/download_selenium_server.py +1 -1
  56. seleniumbase/utilities/selenium_grid/grid_hub.py +1 -2
  57. seleniumbase/utilities/selenium_grid/grid_node.py +2 -3
  58. seleniumbase/utilities/selenium_ide/convert_ide.py +2 -3
  59. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/METADATA +193 -166
  60. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/RECORD +64 -59
  61. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/licenses/LICENSE +1 -1
  62. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/WHEEL +0 -0
  63. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/entry_points.txt +0 -0
  64. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/top_level.txt +0 -0
@@ -264,16 +264,13 @@ class Reveal:
264
264
 
265
265
 
266
266
  class HighCharts:
267
+ LIB = "https://cdn.jsdelivr.net/npm/highcharts"
267
268
  VER = "10.3.3"
268
- HC_CSS = "https://code.highcharts.com/%s/css/highcharts.css" % VER
269
- HC_JS = "https://code.highcharts.com/%s/highcharts.js" % VER
270
- EXPORTING_JS = "https://code.highcharts.com/%s/modules/exporting.js" % VER
271
- EXPORT_DATA_JS = (
272
- "https://code.highcharts.com/%s/modules/export-data.js" % VER
273
- )
274
- ACCESSIBILITY_JS = (
275
- "https://code.highcharts.com/%s/modules/accessibility.js" % VER
276
- )
269
+ HC_CSS = "%s@%s/css/highcharts.css" % (LIB, VER)
270
+ HC_JS = "%s@%s/highcharts.js" % (LIB, VER)
271
+ EXPORTING_JS = "%s@%s/modules/exporting.js" % (LIB, VER)
272
+ EXPORT_DATA_JS = "%s@%s/modules/export-data.js" % (LIB, VER)
273
+ ACCESSIBILITY_JS = "%s@%s/modules/accessibility.js" % (LIB, VER)
277
274
 
278
275
 
279
276
  class BootstrapTour:
@@ -387,6 +384,20 @@ class ValidBrowsers:
387
384
  "ie",
388
385
  "safari",
389
386
  "remote",
387
+ "opera",
388
+ "brave",
389
+ "comet",
390
+ "atlas",
391
+ ]
392
+
393
+
394
+ class ChromiumSubs:
395
+ # Chromium browsers that still use chromedriver
396
+ chromium_subs = [
397
+ "opera",
398
+ "brave",
399
+ "comet",
400
+ "atlas",
390
401
  ]
391
402
 
392
403
 
@@ -406,7 +417,14 @@ class ValidBinaries:
406
417
  "brave",
407
418
  "opera",
408
419
  "opera-stable",
420
+ "comet",
421
+ "comet-browser",
422
+ "comet-stable",
423
+ "atlas",
424
+ "atlas-browser",
425
+ "atlas-stable",
409
426
  "chrome.exe", # WSL (Windows Subsystem for Linux)
427
+ "chromium.exe", # WSL (Windows Subsystem for Linux)
410
428
  ]
411
429
  valid_edge_binaries_on_linux = [
412
430
  "microsoft-edge",
@@ -423,7 +441,14 @@ class ValidBinaries:
423
441
  "Google Chrome Beta",
424
442
  "Google Chrome Dev",
425
443
  "Brave Browser",
444
+ "Brave",
445
+ "Opera Browser",
426
446
  "Opera",
447
+ "Comet Browser",
448
+ "Comet",
449
+ "ChatGPT Atlas",
450
+ "Atlas Browser",
451
+ "Atlas",
427
452
  ]
428
453
  valid_edge_binaries_on_macos = [
429
454
  "Microsoft Edge",
@@ -434,6 +459,8 @@ class ValidBinaries:
434
459
  "chrome-headless-shell.exe",
435
460
  "brave.exe",
436
461
  "opera.exe",
462
+ "comet.exe",
463
+ "atlas.exe",
437
464
  ]
438
465
  valid_edge_binaries_on_windows = [
439
466
  "msedge.exe",
@@ -29,7 +29,9 @@ def wait_for_ready_state_complete(driver, timeout=settings.LARGE_TIMEOUT):
29
29
  If the timeout is exceeded, the test will still continue
30
30
  because readyState == "interactive" may be good enough.
31
31
  (Previously, tests would fail immediately if exceeding the timeout.)"""
32
- if hasattr(settings, "SKIP_JS_WAITS") and settings.SKIP_JS_WAITS:
32
+ if hasattr(driver, "_swap_driver"):
33
+ return
34
+ if getattr(settings, "SKIP_JS_WAITS", None):
33
35
  return
34
36
  start_ms = time.time() * 1000.0
35
37
  stop_ms = start_ms + (timeout * 1000.0)
@@ -54,18 +56,22 @@ def wait_for_ready_state_complete(driver, timeout=settings.LARGE_TIMEOUT):
54
56
 
55
57
 
56
58
  def execute_async_script(driver, script, timeout=settings.LARGE_TIMEOUT):
57
- driver.set_script_timeout(timeout)
58
- return driver.execute_async_script(script)
59
+ if hasattr(driver, "set_script_timeout"):
60
+ driver.set_script_timeout(timeout)
61
+ if hasattr(driver, "execute_async_script"):
62
+ return driver.execute_async_script(script)
63
+ else:
64
+ return None
59
65
 
60
66
 
61
67
  def wait_for_angularjs(driver, timeout=settings.LARGE_TIMEOUT, **kwargs):
62
- if hasattr(settings, "SKIP_JS_WAITS") and settings.SKIP_JS_WAITS:
68
+ if getattr(settings, "SKIP_JS_WAITS", None):
63
69
  return
64
70
  with suppress(Exception):
65
71
  # This closes pop-up alerts
66
72
  execute_script(driver, "")
67
73
  if (
68
- (hasattr(driver, "_is_using_uc") and driver._is_using_uc)
74
+ getattr(driver, "_is_using_uc", None)
69
75
  or not settings.WAIT_FOR_ANGULARJS
70
76
  ):
71
77
  wait_for_ready_state_complete(driver)
@@ -205,6 +211,19 @@ def activate_jquery(driver):
205
211
  try:
206
212
  execute_script(driver, "jQuery('html');")
207
213
  return
214
+ except TypeError as e:
215
+ if (
216
+ (
217
+ shared_utils.is_cdp_swap_needed(driver)
218
+ or hasattr(driver, "_swap_driver")
219
+ )
220
+ and "cannot unpack non-iterable" in str(e)
221
+ ):
222
+ pass
223
+ else:
224
+ if x == 18:
225
+ add_js_link(driver, jquery_js)
226
+ time.sleep(0.1)
208
227
  except Exception:
209
228
  if x == 18:
210
229
  add_js_link(driver, jquery_js)
@@ -276,6 +295,18 @@ def safe_execute_script(driver, script):
276
295
  This method will load jQuery if it wasn't already loaded."""
277
296
  try:
278
297
  execute_script(driver, script)
298
+ except TypeError as e:
299
+ if (
300
+ (
301
+ shared_utils.is_cdp_swap_needed(driver)
302
+ or hasattr(driver, "_swap_driver")
303
+ )
304
+ and "cannot unpack non-iterable" in str(e)
305
+ ):
306
+ pass
307
+ else:
308
+ activate_jquery(driver) # It's a good thing we can define it here
309
+ execute_script(driver, script)
279
310
  except Exception:
280
311
  # The likely reason this fails is because: "jQuery is not defined"
281
312
  activate_jquery(driver) # It's a good thing we can define it here
@@ -868,7 +899,7 @@ def set_messenger_theme(
868
899
  theme = "future"
869
900
  if location == "default":
870
901
  location = "bottom_right"
871
- if hasattr(sb_config, "mobile_emulator") and sb_config.mobile_emulator:
902
+ if getattr(sb_config, "mobile_emulator", None):
872
903
  location = "top_center"
873
904
  if max_messages == "default":
874
905
  max_messages = "8"
@@ -908,15 +939,16 @@ def set_messenger_theme(
908
939
  % (max_messages, messenger_location, theme)
909
940
  )
910
941
  try:
942
+ time.sleep(0.015)
911
943
  execute_script(driver, msg_style)
912
944
  except Exception:
913
- time.sleep(0.03)
945
+ time.sleep(0.035)
914
946
  activate_messenger(driver)
915
- time.sleep(0.15)
947
+ time.sleep(0.175)
916
948
  with suppress(Exception):
917
949
  execute_script(driver, msg_style)
918
- time.sleep(0.02)
919
- time.sleep(0.05)
950
+ time.sleep(0.035)
951
+ time.sleep(0.055)
920
952
 
921
953
 
922
954
  def post_message(driver, message, msg_dur=None, style="info"):
@@ -937,7 +969,10 @@ def post_message(driver, message, msg_dur=None, style="info"):
937
969
  execute_script(driver, messenger_script)
938
970
  except TypeError as e:
939
971
  if (
940
- shared_utils.is_cdp_swap_needed(driver)
972
+ (
973
+ shared_utils.is_cdp_swap_needed(driver)
974
+ or hasattr(driver, "_swap_driver")
975
+ )
941
976
  and "cannot unpack non-iterable" in str(e)
942
977
  ):
943
978
  pass
@@ -968,7 +1003,7 @@ def post_messenger_success_message(driver, message, msg_dur=None):
968
1003
  with suppress(Exception):
969
1004
  theme = "future"
970
1005
  location = "bottom_right"
971
- if hasattr(sb_config, "mobile_emulator") and sb_config.mobile_emulator:
1006
+ if getattr(sb_config, "mobile_emulator", None):
972
1007
  location = "top_right"
973
1008
  set_messenger_theme(driver, theme=theme, location=location)
974
1009
  post_message(driver, message, msg_dur, style="success")
@@ -1260,10 +1295,16 @@ def scroll_to_element(driver, element):
1260
1295
  return False
1261
1296
  try:
1262
1297
  element_location_x = element.location["x"]
1298
+ except Exception:
1299
+ element_location_x = 0
1300
+ try:
1263
1301
  element_width = element.size["width"]
1302
+ except Exception:
1303
+ element_width = 0
1304
+ try:
1264
1305
  screen_width = driver.get_window_size()["width"]
1265
1306
  except Exception:
1266
- element_location_x = 0
1307
+ screen_width = execute_script(driver, "return window.innerWidth;")
1267
1308
  element_location_y = element_location_y - constants.Scroll.Y_OFFSET
1268
1309
  if element_location_y < 0:
1269
1310
  element_location_y = 0
@@ -1295,18 +1336,36 @@ def slow_scroll_to_element(driver, element, *args, **kwargs):
1295
1336
  element_location_y = None
1296
1337
  try:
1297
1338
  if shared_utils.is_cdp_swap_needed(driver):
1298
- element.get_position().y
1339
+ element_location_y = element.get_position().y
1299
1340
  else:
1300
1341
  element_location_y = element.location["y"]
1301
1342
  except Exception:
1302
- element.location_once_scrolled_into_view
1343
+ if shared_utils.is_cdp_swap_needed(driver):
1344
+ element.scroll_into_view()
1345
+ else:
1346
+ element.location_once_scrolled_into_view
1303
1347
  return
1304
1348
  try:
1305
- element_location_x = element.location["x"]
1306
- element_width = element.size["width"]
1307
- screen_width = driver.get_window_size()["width"]
1349
+ if shared_utils.is_cdp_swap_needed(driver):
1350
+ element_location_x = element.get_position().x
1351
+ else:
1352
+ element_location_x = element.location["x"]
1308
1353
  except Exception:
1309
1354
  element_location_x = 0
1355
+ try:
1356
+ if shared_utils.is_cdp_swap_needed(driver):
1357
+ element_width = element.get_position().width
1358
+ else:
1359
+ element_width = element.size["width"]
1360
+ except Exception:
1361
+ element_width = 0
1362
+ try:
1363
+ if shared_utils.is_cdp_swap_needed(driver):
1364
+ screen_width = driver.cdp.get_window_size()["width"]
1365
+ else:
1366
+ screen_width = driver.get_window_size()["width"]
1367
+ except Exception:
1368
+ screen_width = execute_script(driver, "return window.innerWidth;")
1310
1369
  element_location_y = element_location_y - constants.Scroll.Y_OFFSET
1311
1370
  if element_location_y < 0:
1312
1371
  element_location_y = 0
@@ -17,11 +17,10 @@ By.XPATH # "xpath"
17
17
  By.TAG_NAME # "tag name"
18
18
  By.PARTIAL_LINK_TEXT # "partial link text"
19
19
  """
20
- import codecs
21
- import fasteners
22
20
  import os
23
21
  import time
24
22
  from contextlib import suppress
23
+ from filelock import FileLock
25
24
  from selenium.common.exceptions import ElementNotInteractableException
26
25
  from selenium.common.exceptions import ElementNotVisibleException
27
26
  from selenium.common.exceptions import NoAlertPresentException
@@ -193,6 +192,10 @@ def is_attribute_present(
193
192
  @Returns
194
193
  Boolean (is attribute present)
195
194
  """
195
+ if __is_cdp_swap_needed(driver):
196
+ return driver.cdp.is_attribute_present(
197
+ selector, attribute, value=value
198
+ )
196
199
  _reconnect_if_disconnected(driver)
197
200
  try:
198
201
  element = driver.find_element(by=by, value=selector)
@@ -1108,6 +1111,14 @@ def wait_for_element_absent(
1108
1111
  timeout - the time to wait for elements in seconds
1109
1112
  original_selector - handle pre-converted ":contains(TEXT)" selector
1110
1113
  """
1114
+ if __is_cdp_swap_needed(driver):
1115
+ if page_utils.is_valid_by(by):
1116
+ original_selector = selector
1117
+ elif page_utils.is_valid_by(selector):
1118
+ original_selector = by
1119
+ selector, by = page_utils.recalculate_selector(original_selector, by)
1120
+ driver.cdp.wait_for_element_absent(selector)
1121
+ return True
1111
1122
  _reconnect_if_disconnected(driver)
1112
1123
  start_ms = time.time() * 1000.0
1113
1124
  stop_ms = start_ms + (timeout * 1000.0)
@@ -1156,6 +1167,14 @@ def wait_for_element_not_visible(
1156
1167
  timeout - the time to wait for the element in seconds
1157
1168
  original_selector - handle pre-converted ":contains(TEXT)" selector
1158
1169
  """
1170
+ if __is_cdp_swap_needed(driver):
1171
+ if page_utils.is_valid_by(by):
1172
+ original_selector = selector
1173
+ elif page_utils.is_valid_by(selector):
1174
+ original_selector = by
1175
+ selector, by = page_utils.recalculate_selector(original_selector, by)
1176
+ driver.cdp.wait_for_element_not_visible(selector)
1177
+ return True
1159
1178
  _reconnect_if_disconnected(driver)
1160
1179
  start_ms = time.time() * 1000.0
1161
1180
  stop_ms = start_ms + (timeout * 1000.0)
@@ -1508,10 +1527,10 @@ def save_page_source(driver, name, folder=None):
1508
1527
  page_source = driver.cdp.get_page_source()
1509
1528
  else:
1510
1529
  page_source = driver.page_source
1511
- html_file = codecs.open(html_file_path, "w+", "utf-8")
1512
1530
  rendered_source = log_helper.get_html_source_with_base_href(
1513
1531
  driver, page_source
1514
1532
  )
1533
+ html_file = open(html_file_path, mode="w+", encoding="utf-8")
1515
1534
  html_file.write(rendered_source)
1516
1535
  html_file.close()
1517
1536
 
@@ -1627,14 +1646,8 @@ def switch_to_frame(
1627
1646
 
1628
1647
 
1629
1648
  def __switch_to_window(driver, window_handle, uc_lock=True):
1630
- if (
1631
- hasattr(driver, "_is_using_uc")
1632
- and driver._is_using_uc
1633
- and uc_lock
1634
- ):
1635
- gui_lock = fasteners.InterProcessLock(
1636
- constants.MultiBrowser.PYAUTOGUILOCK
1637
- )
1649
+ if getattr(driver, "_is_using_uc", None) and uc_lock:
1650
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1638
1651
  with gui_lock:
1639
1652
  driver.switch_to.window(window_handle)
1640
1653
  else:
@@ -1717,8 +1730,7 @@ def switch_to_window(
1717
1730
 
1718
1731
  def _reconnect_if_disconnected(driver):
1719
1732
  if (
1720
- hasattr(driver, "_is_using_uc")
1721
- and driver._is_using_uc
1733
+ getattr(driver, "_is_using_uc", None)
1722
1734
  and hasattr(driver, "is_connected")
1723
1735
  and not driver.is_connected()
1724
1736
  ):
@@ -1739,6 +1751,22 @@ def open_url(driver, url):
1739
1751
  if __is_cdp_swap_needed(driver):
1740
1752
  driver.cdp.open(url)
1741
1753
  return
1754
+ elif (
1755
+ getattr(driver, "_is_using_uc", None)
1756
+ # and getattr(driver, "_is_using_auth", None)
1757
+ and not getattr(driver, "_is_using_cdp", None)
1758
+ ):
1759
+ # Auth in UC Mode requires CDP Mode
1760
+ # (and now we're always forcing it)
1761
+ driver.uc_activate_cdp_mode(url)
1762
+ return
1763
+ elif (
1764
+ getattr(driver, "_is_using_uc", None)
1765
+ and getattr(driver, "_is_using_cdp", None)
1766
+ ):
1767
+ driver.disconnect()
1768
+ driver.cdp.open(url)
1769
+ return
1742
1770
  url = str(url).strip() # Remove leading and trailing whitespace
1743
1771
  if not page_utils.looks_like_a_page_url(url):
1744
1772
  if page_utils.is_valid_url("https://" + url):
@@ -1,5 +1,4 @@
1
1
  """This module contains useful utility methods"""
2
- import codecs
3
2
  import fasteners
4
3
  import os
5
4
  import re
@@ -110,10 +109,10 @@ def looks_like_a_page_url(url):
110
109
  navigate to the page if a URL is detected, but will instead call
111
110
  self.get_element(URL_AS_A_SELECTOR) if the input is not a URL."""
112
111
  return url.startswith((
113
- "http:", "https:", "://", "about:", "blob:", "chrome:",
112
+ "http:", "https:", "://", "about:", "blob:", "chrome:", "opera:",
114
113
  "data:", "edge:", "file:", "view-source:", "chrome-search:",
115
114
  "chrome-extension:", "chrome-untrusted:", "isolated-app:",
116
- "chrome-devtools:", "devtools:"
115
+ "chrome-devtools:", "devtools:", "brave:", "comet:", "atlas:"
117
116
  ))
118
117
 
119
118
 
@@ -294,18 +293,20 @@ def _print_unique_links_with_status_codes(page_url, soup):
294
293
  print(link, " -> ", status_code)
295
294
 
296
295
 
297
- def _download_file_to(file_url, destination_folder, new_file_name=None):
296
+ def _download_file_to(
297
+ file_url, destination_folder, new_file_name=None, headers=None
298
+ ):
298
299
  if new_file_name:
299
300
  file_name = new_file_name
300
301
  else:
301
302
  file_name = file_url.split("/")[-1]
302
- r = requests.get(file_url, timeout=5)
303
+ r = requests.get(file_url, headers=headers, timeout=5)
303
304
  file_path = os.path.join(destination_folder, file_name)
304
305
  download_file_lock = fasteners.InterProcessLock(
305
306
  constants.MultiBrowser.DOWNLOAD_FILE_LOCK
306
307
  )
307
308
  with download_file_lock:
308
- with open(file_path, "wb") as code:
309
+ with open(file_path, mode="wb") as code:
309
310
  code.write(r.content)
310
311
 
311
312
 
@@ -314,8 +315,10 @@ def _save_data_as(data, destination_folder, file_name):
314
315
  constants.MultiBrowser.FILE_IO_LOCK
315
316
  )
316
317
  with file_io_lock:
317
- out_file = codecs.open(
318
- os.path.join(destination_folder, file_name), "w+", encoding="utf-8"
318
+ out_file = open(
319
+ os.path.join(destination_folder, file_name),
320
+ mode="w+",
321
+ encoding="utf-8",
319
322
  )
320
323
  out_file.writelines(data)
321
324
  out_file.close()
@@ -328,12 +331,16 @@ def _append_data_to_file(data, destination_folder, file_name):
328
331
  with file_io_lock:
329
332
  existing_data = ""
330
333
  if os.path.exists(os.path.join(destination_folder, file_name)):
331
- with open(os.path.join(destination_folder, file_name), "r") as f:
334
+ with open(
335
+ os.path.join(destination_folder, file_name), mode="r"
336
+ ) as f:
332
337
  existing_data = f.read()
333
338
  if not existing_data.split("\n")[-1] == "":
334
339
  existing_data += "\n"
335
- out_file = codecs.open(
336
- os.path.join(destination_folder, file_name), "w+", encoding="utf-8"
340
+ out_file = open(
341
+ os.path.join(destination_folder, file_name),
342
+ mode="w+",
343
+ encoding="utf-8",
337
344
  )
338
345
  out_file.writelines("%s%s" % (existing_data, data))
339
346
  out_file.close()
@@ -346,7 +353,7 @@ def _get_file_data(folder, file_name):
346
353
  with file_io_lock:
347
354
  if not os.path.exists(os.path.join(folder, file_name)):
348
355
  raise Exception("File not found!")
349
- with open(os.path.join(folder, file_name), "r") as f:
356
+ with open(os.path.join(folder, file_name), mode="r") as f:
350
357
  data = f.read()
351
358
  return data
352
359
 
@@ -7,6 +7,7 @@ import sys
7
7
  import time
8
8
  from contextlib import suppress
9
9
  from seleniumbase import config as sb_config
10
+ from seleniumbase.config import settings
10
11
  from seleniumbase.fixtures import constants
11
12
 
12
13
 
@@ -17,20 +18,79 @@ def pip_install(package, version=None):
17
18
  pip_install_lock = fasteners.InterProcessLock(
18
19
  constants.PipInstall.LOCKFILE
19
20
  )
21
+ upgrade_to_latest = False
22
+ if (
23
+ version
24
+ and ("U" in str(version).upper() or "L" in str(version).upper())
25
+ ):
26
+ # Upgrade to Latest when specified with "U" or "L"
27
+ upgrade_to_latest = True
20
28
  with pip_install_lock:
21
29
  if not version:
22
30
  subprocess.check_call(
23
31
  [sys.executable, "-m", "pip", "install", package]
24
32
  )
25
- else:
33
+ elif not upgrade_to_latest:
26
34
  package_and_version = package + "==" + str(version)
27
35
  subprocess.check_call(
28
36
  [sys.executable, "-m", "pip", "install", package_and_version]
29
37
  )
38
+ else:
39
+ subprocess.check_call(
40
+ [sys.executable, "-m", "pip", "install", "-U", package]
41
+ )
42
+
43
+
44
+ def make_version_list(version_str):
45
+ return [int(i) for i in version_str.split(".") if i.isdigit()]
46
+
47
+
48
+ def make_version_tuple(version_str):
49
+ return tuple(make_version_list(version_str))
50
+
51
+
52
+ def get_mfa_code(totp_key=None):
53
+ """Returns a time-based one-time password based on the
54
+ Google Authenticator algorithm for multi-factor authentication.
55
+ If the "totp_key" is not specified, this method defaults
56
+ to using the one provided in [seleniumbase/config/settings.py].
57
+ Google Authenticator codes expire & change at 30-sec intervals.
58
+ If the fetched password expires in the next 1.2 seconds, waits
59
+ for a new one before returning it (may take up to 1.2 seconds).
60
+ See https://pyotp.readthedocs.io/en/latest/ for details."""
61
+ import pyotp
62
+
63
+ if not totp_key:
64
+ totp_key = settings.TOTP_KEY
65
+ epoch_interval = time.time() / 30.0
66
+ cycle_lifespan = float(epoch_interval) - int(epoch_interval)
67
+ if float(cycle_lifespan) > 0.96:
68
+ # Password expires in the next 1.2 seconds. Wait for a new one.
69
+ for i in range(30):
70
+ time.sleep(0.04)
71
+ epoch_interval = time.time() / 30.0
72
+ cycle_lifespan = float(epoch_interval) - int(epoch_interval)
73
+ if not float(cycle_lifespan) > 0.96:
74
+ # The new password cycle has begun
75
+ break
76
+ totp = pyotp.TOTP(totp_key)
77
+ return str(totp.now())
78
+
79
+
80
+ def is_arm_linux():
81
+ """Returns True if machine is ARM Linux.
82
+ This will be useful once Google adds
83
+ support for ARM Linux ChromeDriver.
84
+ (Raspberry Pi uses ARM architecture.)"""
85
+ return (
86
+ platform.system() == "Linux"
87
+ and platform.machine() == "aarch64"
88
+ )
30
89
 
31
90
 
32
91
  def is_arm_mac():
33
- """(M1 / M2 Macs use the ARM processor)"""
92
+ """Returns True if machine is ARM Mac.
93
+ (Eg. M1 / M2 Macs use ARM processors)"""
34
94
  return (
35
95
  "darwin" in sys.platform
36
96
  and (
@@ -87,8 +147,7 @@ def fix_url_as_needed(url):
87
147
 
88
148
  def reconnect_if_disconnected(driver):
89
149
  if (
90
- hasattr(driver, "_is_using_uc")
91
- and driver._is_using_uc
150
+ getattr(driver, "_is_using_uc", None)
92
151
  and hasattr(driver, "is_connected")
93
152
  and not driver.is_connected()
94
153
  ):
@@ -238,8 +297,7 @@ def __time_limit_exceeded(message):
238
297
 
239
298
  def check_if_time_limit_exceeded():
240
299
  if (
241
- hasattr(sb_config, "time_limit")
242
- and sb_config.time_limit
300
+ getattr(sb_config, "time_limit", None)
243
301
  and not sb_config.recorder_mode
244
302
  ):
245
303
  time_limit = sb_config.time_limit
@@ -11,6 +11,11 @@ from seleniumbase.config import settings
11
11
  from seleniumbase.fixtures import js_utils
12
12
 
13
13
 
14
+ python3_11_or_newer = False
15
+ if sys.version_info >= (3, 11):
16
+ python3_11_or_newer = True
17
+
18
+
14
19
  class MasterQA(BaseCase):
15
20
  def setUp(self):
16
21
  self.check_count = 0
@@ -309,8 +314,17 @@ class MasterQA(BaseCase):
309
314
  if hasattr(sys, "last_traceback") and sys.last_traceback is not None:
310
315
  has_exception = True
311
316
  elif hasattr(self, "_outcome"):
312
- if hasattr(self._outcome, "errors") and self._outcome.errors:
313
- has_exception = True
317
+ if hasattr(self._outcome, "errors"):
318
+ if python3_11_or_newer:
319
+ if (
320
+ self._outcome.errors
321
+ and self._outcome.errors[-1]
322
+ and self._outcome.errors[-1][1]
323
+ ):
324
+ has_exception = True
325
+ else:
326
+ if self._outcome.errors:
327
+ has_exception = True
314
328
  else:
315
329
  has_exception = sys.exc_info()[1] is not None
316
330
  return has_exception
@@ -240,14 +240,22 @@ class Base(Plugin):
240
240
  test.test.test_id = test.id()
241
241
  test.test.is_nosetest = True
242
242
  test.test.environment = self.options.environment
243
+ sb_config.environment = self.options.environment
243
244
  test.test.env = self.options.environment # Add a shortened version
244
245
  test.test.account = self.options.account
246
+ sb_config.account = self.options.account
245
247
  test.test.data = self.options.data
248
+ sb_config.data = self.options.data
246
249
  test.test.var1 = self.options.var1
250
+ sb_config.var1 = self.options.var1
247
251
  test.test.var2 = self.options.var2
252
+ sb_config.var2 = self.options.var2
248
253
  test.test.var3 = self.options.var3
254
+ sb_config.var3 = self.options.var3
249
255
  test.test.variables = variables # Already verified is a dictionary
256
+ sb_config.variables = variables
250
257
  test.test.settings_file = self.options.settings_file
258
+ sb_config.settings_file = self.options.settings_file
251
259
  test.test._final_debug = self.options.final_debug
252
260
  test.test.log_path = self.options.log_path
253
261
  if self.options.archive_downloads:
@@ -1,6 +1,5 @@
1
1
  """Test Info Plugin for SeleniumBase tests that run with pynose / nosetests"""
2
2
  import os
3
- import codecs
4
3
  import time
5
4
  import traceback
6
5
  from nose.plugins import Plugin
@@ -26,7 +25,7 @@ class BasicTestInfo(Plugin):
26
25
  if not os.path.exists(test_logpath):
27
26
  os.makedirs(test_logpath)
28
27
  file_name = "%s/%s" % (test_logpath, self.logfile_name)
29
- basic_info_file = codecs.open(file_name, "w+", "utf-8")
28
+ basic_info_file = open(file_name, mode="w+", encoding="utf-8")
30
29
  self.__log_test_error_data(basic_info_file, test, err, "Error")
31
30
  basic_info_file.close()
32
31
 
@@ -35,7 +34,7 @@ class BasicTestInfo(Plugin):
35
34
  if not os.path.exists(test_logpath):
36
35
  os.makedirs(test_logpath)
37
36
  file_name = "%s/%s" % (test_logpath, self.logfile_name)
38
- basic_info_file = codecs.open(file_name, "w+", "utf-8")
37
+ basic_info_file = open(file_name, mode="w+", encoding="utf-8")
39
38
  self.__log_test_error_data(basic_info_file, test, err, "Error")
40
39
  basic_info_file.close()
41
40