diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index b5ad483ffc9..5e643c84f3b 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -5,7 +5,7 @@ regex>=2024.11.6 pymdown-extensions>=10.14.3 pipdeptree>=2.26.0 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.11 +mkdocs-material==9.6.12 mkdocs-exclude-search==0.6.6 mkdocs-simple-hooks==0.1.5 mkdocs-material-extensions==1.3.1 diff --git a/requirements.txt b/requirements.txt index 1bb76e8f39b..c54a1a4b879 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ 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" wheel>=0.45.1 @@ -61,7 +61,7 @@ pytest-xdist==3.6.1 parameterized==0.9.0 behave==1.2.6 soupsieve==2.6 -beautifulsoup4==4.13.3 +beautifulsoup4==4.13.4 pyotp==2.9.0 python-xlib==0.33;platform_system=="Linux" markdown-it-py==3.0.0 diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 192e56ca704..5dd0bd649fc 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.37.2" +__version__ = "4.37.3" diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 3e55e5eb425..7e1c538fea2 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -162,6 +162,7 @@ def __initialize_variables(self): 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 = {} @@ -15676,19 +15677,24 @@ def __get_test_id(self): 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__, diff --git a/seleniumbase/plugins/sb_manager.py b/seleniumbase/plugins/sb_manager.py index 0ed56d603b0..17111ab84e0 100644 --- a/seleniumbase/plugins/sb_manager.py +++ b/seleniumbase/plugins/sb_manager.py @@ -23,7 +23,7 @@ ######################################### """ -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,7 @@ def SB( "%s%s%s%s%s" % (c1, left_space, end_text, right_space, cr) ) + gc.collect() if test and test_name and not test_passed and raise_test_failure: raise exception elif ( diff --git a/seleniumbase/undetected/__init__.py b/seleniumbase/undetected/__init__.py index 04a562d77ed..365ffc5ba32 100644 --- a/seleniumbase/undetected/__init__.py +++ b/seleniumbase/undetected/__init__.py @@ -145,9 +145,14 @@ def __init__( 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 @@ -462,9 +467,7 @@ def reconnect(self, timeout=0.1): 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) @@ -559,10 +562,18 @@ def quit(self): 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() try: self.service.send_remote_shutdown_command() except TypeError: @@ -570,10 +581,12 @@ def quit(self): finally: with suppress(Exception): self.service._terminate_process() - with suppress(Exception): - if self.reactor and isinstance(self.reactor, Reactor): - logger.debug("Shutting down Reactor") + if 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") diff --git a/seleniumbase/undetected/cdp.py b/seleniumbase/undetected/cdp.py index 38bbe2862cd..65a8f1d86a9 100644 --- a/seleniumbase/undetected/cdp.py +++ b/seleniumbase/undetected/cdp.py @@ -53,28 +53,58 @@ def __init__(self, options): 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 @@ def get(self, uri): 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 @@ def post(self, uri, data=None): 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): diff --git a/setup.py b/setup.py index b03518ad7b7..5a8ddedfe19 100755 --- a/setup.py +++ b/setup.py @@ -148,7 +148,7 @@ 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"', 'wheel>=0.45.1', @@ -210,7 +210,7 @@ 'parameterized==0.9.0', "behave==1.2.6", 'soupsieve==2.6', - "beautifulsoup4==4.13.3", + "beautifulsoup4==4.13.4", 'pyotp==2.9.0', 'python-xlib==0.33;platform_system=="Linux"', 'markdown-it-py==3.0.0', @@ -261,7 +261,7 @@ # (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 @@ # (An optional library for image-processing.) "pillow": [ 'Pillow>=10.4.0;python_version<"3.9"', - 'Pillow>=11.2.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.)