From cc4913c1e940425f3af9dc8e1f57429ae418b886 Mon Sep 17 00:00:00 2001 From: Aleksandr Kotlyar Date: Sun, 17 May 2026 23:35:41 +0300 Subject: [PATCH 1/4] fix(cache): handle file/dir conflict before zip extraction (Ubuntu Opera CI) --- webdriver_manager/core/file_manager.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/webdriver_manager/core/file_manager.py b/webdriver_manager/core/file_manager.py index 8c7abd91..27fbc38b 100644 --- a/webdriver_manager/core/file_manager.py +++ b/webdriver_manager/core/file_manager.py @@ -62,6 +62,7 @@ def unpack_archive(self, archive_file: Archive, target_dir): def __extract_zip(self, archive_file, to_directory): zip_class = (LinuxZipFileWithPermissions if self._os_system_manager.get_os_name() == "linux" else zipfile.ZipFile) archive = zip_class(archive_file.file_path) + self.__remove_file_dir_conflicts(archive.namelist(), to_directory) try: archive.extractall(to_directory) except Exception as e: @@ -88,6 +89,19 @@ def __extract_zip(self, archive_file, to_directory): return sorted(file_names, key=lambda x: x.lower()) return archive.namelist() + def __remove_file_dir_conflicts(self, names, to_directory): + """Remove stale files that conflict with directory entries in archive. + + Example: if cache contains `/operadriver` as a file, and + archive now contains `operadriver/`, extraction would fail with + NotADirectoryError. + """ + top_level_dirs = {name.split("/", 1)[0] for name in names if "/" in name} + for dir_name in top_level_dirs: + path = os.path.join(to_directory, dir_name) + if os.path.isfile(path): + os.remove(path) + def __extract_tar_file(self, archive_file, to_directory): try: tar = tarfile.open(archive_file.file_path, mode="r:gz") From 5e7fa869b108965a0b9c46d5b3678a97510a8b7b Mon Sep 17 00:00:00 2001 From: Aleksandr Kotlyar Date: Sun, 17 May 2026 23:37:51 +0300 Subject: [PATCH 2/4] ci matrix --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 585a7ed4..3159f1cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,10 +53,10 @@ jobs: os: [ windows-latest ] wdm-log: [''] include: - - python-version: '3.12' + - python-version: '3.13' selenium-version: '4.10.0' os: ubuntu-latest - - python-version: '3.12' + - python-version: '3.14' selenium-version: '4.10.0' os: macos-latest wdm-log: '0' From db9f4d5c9945b0ed4a4430b21d191136ddd0eb77 Mon Sep 17 00:00:00 2001 From: Aleksandr Kotlyar Date: Sun, 17 May 2026 23:44:27 +0300 Subject: [PATCH 3/4] fix(opera): handle file cache path in install and avoid listdir on binary path --- tests/test_opera_manager.py | 12 ++++++++++++ webdriver_manager/opera.py | 22 ++++++++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/tests/test_opera_manager.py b/tests/test_opera_manager.py index 72f3cdfe..1516b6b7 100755 --- a/tests/test_opera_manager.py +++ b/tests/test_opera_manager.py @@ -115,3 +115,15 @@ def test_can_get_driver_from_cache(os_type, delete_drivers_dir, opera_release_da OperaDriverManager(os_system_manager=OperationSystemManager(os_type)).install() driver_path = OperaDriverManager(os_system_manager=OperationSystemManager(os_type)).install() assert os.path.exists(driver_path) + + +def test_opera_install_keeps_file_path_without_listdir(monkeypatch, tmp_path): + binary_path = tmp_path / "operadriver" + binary_path.write_text("bin") + + manager = OperaDriverManager() + monkeypatch.setattr(manager, "_get_driver_binary_path", lambda _driver: str(binary_path)) + + resolved_path = manager.install() + + assert resolved_path == str(binary_path) diff --git a/webdriver_manager/opera.py b/webdriver_manager/opera.py index 893c11a6..dc8bb08d 100755 --- a/webdriver_manager/opera.py +++ b/webdriver_manager/opera.py @@ -41,12 +41,22 @@ def __init__( def install(self) -> str: driver_path = self._get_driver_binary_path(self.driver) - if not os.path.isfile(driver_path): - for name in os.listdir(driver_path): - if "sha512_sum" in name: - os.remove(os.path.join(driver_path, name)) - break - driver_path = os.path.join(driver_path, os.listdir(driver_path)[0]) + if os.path.isfile(driver_path): + os.chmod(driver_path, 0o755) + return driver_path + + for name in os.listdir(driver_path): + if "sha512_sum" in name: + os.remove(os.path.join(driver_path, name)) + + candidates = [ + name for name in os.listdir(driver_path) + if os.path.isfile(os.path.join(driver_path, name)) + ] + if not candidates: + raise FileNotFoundError(f"No OperaDriver binary found in {driver_path}") + + driver_path = os.path.join(driver_path, candidates[0]) os.chmod(driver_path, 0o755) return driver_path From 8768aa891d7402f9cec16e2c56d25a5a771e73de Mon Sep 17 00:00:00 2001 From: Aleksandr Kotlyar Date: Sun, 17 May 2026 23:47:12 +0300 Subject: [PATCH 4/4] fix(opera): handle file cache path in install and avoid listdir on binary path --- tests/test_file_manager.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/test_file_manager.py diff --git a/tests/test_file_manager.py b/tests/test_file_manager.py new file mode 100644 index 00000000..f8253b44 --- /dev/null +++ b/tests/test_file_manager.py @@ -0,0 +1,25 @@ +import zipfile + +from webdriver_manager.core.file_manager import FileManager +from webdriver_manager.core.os_manager import OperationSystemManager + + +def test_extract_zip_removes_file_dir_conflict(tmp_path): + target_dir = tmp_path / "target" + target_dir.mkdir() + conflict_path = target_dir / "operadriver" + conflict_path.write_text("stale file") + + zip_path = tmp_path / "driver.zip" + with zipfile.ZipFile(zip_path, "w") as zf: + zf.writestr("operadriver/operadriver", "binary") + + class ArchiveMock: + def __init__(self, file_path): + self.file_path = str(file_path) + + file_manager = FileManager(OperationSystemManager()) + extracted = file_manager.unpack_archive(ArchiveMock(zip_path), str(target_dir)) + + assert "operadriver/operadriver" in extracted + assert (target_dir / "operadriver" / "operadriver").exists()