diff --git a/docs/source/releasenotes.rst b/docs/source/releasenotes.rst index f0af1f4cd6..f98ccb3838 100644 --- a/docs/source/releasenotes.rst +++ b/docs/source/releasenotes.rst @@ -12,6 +12,12 @@ Latest versions `Upcoming release `_ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- Library **RPA.Browser.Selenium** (:issue:`1078`; ``rpaframework-core`` **11.2.0**): + + - Display the used webdriver and browser versions in the logs. + - Reminder: You can disable webdriver-manager SSL checks during downloads by setting + ``WDM_SSL_VERIFY=false`` in the environment. + `Released `_ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/packages/core/poetry.lock b/packages/core/poetry.lock index 1b3967e984..134aecd0f9 100644 --- a/packages/core/poetry.lock +++ b/packages/core/poetry.lock @@ -1059,14 +1059,14 @@ files = [ [[package]] name = "pytest" -version = "7.4.1" +version = "7.4.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.1-py3-none-any.whl", hash = "sha256:460c9a59b14e27c602eb5ece2e47bec99dc5fc5f6513cf924a7d03a578991b1f"}, - {file = "pytest-7.4.1.tar.gz", hash = "sha256:2f2301e797521b23e4d2585a0a3d7b5e50fdddaaf7e7d6773ea26ddb17c213ab"}, + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] @@ -1270,20 +1270,20 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]} [[package]] name = "setuptools" -version = "68.2.0" +version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.2.0-py3-none-any.whl", hash = "sha256:af3d5949030c3f493f550876b2fd1dd5ec66689c4ee5d5344f009746f71fd5a8"}, - {file = "setuptools-68.2.0.tar.gz", hash = "sha256:00478ca80aeebeecb2f288d3206b0de568df5cd2b8fada1209843cc9a8d88a48"}, + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "sniffio" @@ -1383,18 +1383,6 @@ exceptiongroup = "*" trio = ">=0.11" wsproto = ">=0.14" -[[package]] -name = "truststore" -version = "0.7.0" -description = "Verify certificates using native system trust stores" -category = "main" -optional = false -python-versions = ">= 3.10" -files = [ - {file = "truststore-0.7.0-py3-none-any.whl", hash = "sha256:a4b410a365cafec4c2b386b30df53d89a6a4466cc229c7026e41c2d847f94fe9"}, - {file = "truststore-0.7.0.tar.gz", hash = "sha256:72e784507a624375434381e4bad3eff8614bc8c845a7f5ae16a25a2624d0683f"}, -] - [[package]] name = "typing-extensions" version = "4.7.1" @@ -1554,4 +1542,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "2ea20303075220e77df931843e45f19c7515c839b52cb2f7fc1aa9703cf663c6" +content-hash = "487b0e4a43d2e60ed50a1e1bb3614fea3e2ce5e5e2050f2cc5f04980b8bdb086" diff --git a/packages/core/pyproject.toml b/packages/core/pyproject.toml index 2fe8a51b79..1a5e0d1dcf 100644 --- a/packages/core/pyproject.toml +++ b/packages/core/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "rpaframework-core" -version = "11.1.1" +version = "11.2.0" description = "Core utilities used by RPA Framework" authors = ["RPA Framework "] license = "Apache-2.0" @@ -36,7 +36,6 @@ pywin32 = { version = ">=300,<304", platform = "win32", python = "!=3.8.1" } uiautomation = { version = "^2.0.15", platform = "win32" } pillow = "^9.1.1" packaging = "^23.1" -truststore = { version = "^0.7.0", python = ">=3.10.12" } [tool.poetry.group.dev.dependencies] black = "^22.3.0" diff --git a/packages/core/src/RPA/core/webdriver.py b/packages/core/src/RPA/core/webdriver.py index 38c1459786..d90ed5619d 100644 --- a/packages/core/src/RPA/core/webdriver.py +++ b/packages/core/src/RPA/core/webdriver.py @@ -17,9 +17,20 @@ from webdriver_manager.core.driver_cache import ( DriverCacheManager as _DriverCacheManager, ) +from webdriver_manager.core.file_manager import FileManager from webdriver_manager.core.logger import log from webdriver_manager.core.manager import DriverManager -from webdriver_manager.core.os_manager import ChromeType, OperationSystemManager +from webdriver_manager.core.os_manager import ( + ChromeType, + OSType, + OperationSystemManager as _OperationSystemManager, + PATTERN as _PATTERN, +) +from webdriver_manager.core.utils import ( + linux_browser_apps_to_cmd, + read_version_from_cmd, + windows_browser_apps_to_cmd, +) from webdriver_manager.drivers.chrome import ChromeDriver as _ChromeDriver from webdriver_manager.firefox import GeckoDriverManager from webdriver_manager.microsoft import ( @@ -31,11 +42,88 @@ from RPA.core.robocorp import robocorp_home -# FIXME(cmin764; 6 Sep 2023): Remove this once the following issue is solved: -# https://github.com/SergeyPirogov/webdriver_manager/issues/618 +class BrowserType: + """Constants for the browser types. (expands the internal one)""" + + MSIE = "msie" + FIREFOX = "firefox" + + +PATTERN = _PATTERN.copy() +PATTERN[BrowserType.MSIE] = r"\d+\.\d+\.\d+\.\d+" + + +class OperationSystemManager(_OperationSystemManager): + """Custom manager for browser version retrieval which works with explicit paths.""" + + @staticmethod + def _get_browser_version(browser_type: str, paths: List[str]) -> Optional[str]: + common_cmds = { + OSType.LINUX: linux_browser_apps_to_cmd(*paths), + OSType.MAC: f"{paths[0]} --version", + OSType.WIN: windows_browser_apps_to_cmd( + *( + f"(Get-Item -Path '{path}').VersionInfo.FileVersion" + for path in paths + ) + ), + } + cmd_mapping = { + ChromeType.GOOGLE: common_cmds, + ChromeType.CHROMIUM: common_cmds, + ChromeType.MSEDGE: common_cmds, + BrowserType.FIREFOX: common_cmds, + BrowserType.MSIE: common_cmds, + } + try: + cmd_mapping = cmd_mapping[browser_type][ + OperationSystemManager.get_os_name() + ] + pattern = PATTERN[browser_type] + version = read_version_from_cmd(cmd_mapping, pattern) + return version + # pylint: disable=broad-except + except Exception as exc: + LOGGER.warning( + "Can't read %r browser version due to: %s", browser_type, exc + ) + return None + + def get_browser_version_from_os( + self, browser_type: Optional[str] = None + ) -> Optional[str]: + if browser_type != BrowserType.MSIE: + return super().get_browser_version_from_os(browser_type) + + # Add support for IE version retrieval. + # NOTE(cmin764; 15 Sep 2023): This got slightly different due to posted issue: + # https://github.com/SergeyPirogov/webdriver_manager/issues/625 + program_files = os.getenv("PROGRAMFILES", r"C:\Program Files") + paths = [ + rf"{program_files}\Internet Explorer\iexplore.exe", + rf"{program_files} (x86)\Internet Explorer\iexplore.exe", + ] + return self._get_browser_version(BrowserType.MSIE, paths=paths) + + def get_browser_version( + self, browser_type: str, path: Optional[str] = None + ) -> Optional[str]: + if path: + return self._get_browser_version(browser_type, paths=[path]) + + return self.get_browser_version_from_os(browser_type) + + class DriverCacheManager(_DriverCacheManager): """Fixes caching when retrieving an existing already downloaded webdriver.""" + def __init__(self, *args, file_manager: Optional[FileManager] = None, **kwargs): + super().__init__(*args, **kwargs) + self._os_system_manager = OperationSystemManager() + self._file_manager = file_manager or FileManager(self._os_system_manager) + + # FIXME(cmin764; 6 Sep 2023): Remove this once the following issue is solved: + # https://github.com/SergeyPirogov/webdriver_manager/issues/618 # pylint: disable=unused-private-member def __get_metadata_key(self, *args, **kwargs) -> str: # pylint: disable=super-with-arguments @@ -351,16 +439,21 @@ def _is_chromium() -> bool: return not is_browser(ChromeType.GOOGLE) and is_browser(ChromeType.CHROMIUM) -def _to_manager(browser: str, *, root: Path) -> DriverManager: +def _get_browser_lower(browser: str) -> str: browser = browser.strip() browser_lower = browser.lower() - manager_factory = AVAILABLE_DRIVERS.get(browser_lower) - if not manager_factory: + if browser_lower not in AVAILABLE_DRIVERS: raise ValueError( f"Unsupported browser {browser!r} for webdriver download!" f" (choose from: {', '.join(SUPPORTED_BROWSERS.values())})" ) + return browser_lower + + +def _to_manager(browser: str, *, root: Path) -> DriverManager: + browser_lower = _get_browser_lower(browser) + manager_factory = AVAILABLE_DRIVERS[browser_lower] if manager_factory == ChromeDriverManager and _is_chromium(): manager_factory = functools.partial( manager_factory, chrome_type=ChromeType.CHROMIUM @@ -373,7 +466,9 @@ def _to_manager(browser: str, *, root: Path) -> DriverManager: LOGGER.warning("Can't set an internal webdriver source for %r!", browser) cache_manager = DriverCacheManager(root_dir=str(root)) - manager = manager_factory(cache_manager=cache_manager) + manager = manager_factory( + cache_manager=cache_manager, os_system_manager=_OPS_MANAGER + ) driver = manager.driver cache = getattr(functools, "cache", functools.lru_cache(maxsize=None)) driver.get_latest_release_version = cache(driver.get_latest_release_version) @@ -397,3 +492,27 @@ def download(browser: str, root: Path = DRIVER_ROOT) -> str: _set_executable(path) LOGGER.info("Downloaded webdriver to: %s", path) return path + + +def get_browser_version(browser: str, path: Optional[str] = None) -> Optional[str]: + """Returns the detected browser version from OS in the absence of a given `path`.""" + browser_lower = _get_browser_lower(browser) + chrome_type = ChromeType.CHROMIUM if _is_chromium() else ChromeType.GOOGLE + browser_types = { + # NOTE(cmin764; 12 Sep 2023): There's no upstream support on getting the + # automatically detected browser version from the OS for IE, Safari and Opera. + # But we introduce one here for IE only. + "chrome": chrome_type, + "firefox": BrowserType.FIREFOX, + "gecko": BrowserType.FIREFOX, + "mozilla": BrowserType.FIREFOX, + "edge": ChromeType.MSEDGE, + "chromiumedge": ChromeType.MSEDGE, + "ie": BrowserType.MSIE, + } + browser_type = browser_types.get(browser_lower) + if not browser_type: + LOGGER.warning("Can't determine browser version for %r!", browser) + return None + + return _OPS_MANAGER.get_browser_version(browser_type, path=path) diff --git a/packages/core/tests/python/test_webdriver.py b/packages/core/tests/python/test_webdriver.py index 2f75036e37..49a0e6e11f 100644 --- a/packages/core/tests/python/test_webdriver.py +++ b/packages/core/tests/python/test_webdriver.py @@ -1,4 +1,7 @@ +import platform +import re import unittest.mock as mock +from pathlib import Path import pytest from webdriver_manager.core.os_manager import ChromeType @@ -13,7 +16,7 @@ def disable_caching(): with mock.patch( "webdriver_manager.core.driver_cache.DriverCacheManager.find_driver", new=mock.Mock(return_value=None), - ): + ), mock.patch("RPA.core.webdriver.suppress_logging"): yield @@ -48,3 +51,35 @@ def test_edge_download(): def test_ie_download(): path = webdriver.download("Ie", root=RESULTS_DIR) assert "IEDriverServer.exe" in path + + +@pytest.mark.parametrize("browser", ["Chrome", "Firefox", "Edge", "Ie"]) +def test_get_browser_version(browser): + version = webdriver.get_browser_version(browser) + print(f"{browser}: {version}") + + +@pytest.mark.skipif( + platform.system() != "Windows", reason="requires Windows with IE installed" +) +@pytest.mark.parametrize( + "path", [None, r"C:\Program Files\Internet Explorer\iexplore.exe"] +) +def test_get_ie_version(path): + version = webdriver.get_browser_version("Ie", path=path) + assert re.match(r"\d+(\.\d+){3}$", version) # 4 atoms in the version + + +@pytest.mark.skipif( + platform.system() != "Darwin", reason="requires Mac with Chrome installed" +) +def test_get_chrome_version_path_mac(): + path = ( + Path("/Applications") + / r"Google\ Chrome.app" + / "Contents" + / "MacOS" + / r"Google\ Chrome" + ) + version = webdriver.get_browser_version("Chrome", path=str(path)) + assert re.match(r"\d+(\.\d+){2,3}$", version) # 3-4 atoms in the version