-
Notifications
You must be signed in to change notification settings - Fork 512
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(browser): Enhance Browser Module with Type Safety and Setup …
…Documentation
- Loading branch information
Showing
3 changed files
with
153 additions
and
99 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
install: | ||
pip install -r requirements/doc.txt | ||
|
||
.PHONY: install |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,130 +1,173 @@ | ||
# Copyright 2012 splinter authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style | ||
# license that can be found in the LICENSE file. | ||
|
||
import logging | ||
from http.client import HTTPException | ||
from typing import Dict | ||
from typing import Tuple | ||
from typing import Type | ||
from typing import Union | ||
|
||
from typing import Dict, Tuple, Type, Union, Optional, Any | ||
from urllib3.exceptions import MaxRetryError | ||
|
||
from splinter.driver import DriverAPI | ||
from splinter.exceptions import DriverNotFoundError | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
# Define base exceptions tuple | ||
driver_exceptions: Tuple[Type[Exception], ...] = (IOError, HTTPException, MaxRetryError) | ||
|
||
# Safely add WebDriverException if available | ||
try: | ||
from selenium.common.exceptions import WebDriverException | ||
|
||
driver_exceptions += (WebDriverException,) | ||
except ImportError as e: | ||
logger.debug(f"Import Warning: {e}") | ||
|
||
|
||
_DRIVERS: Dict[str, Union[None, Type[DriverAPI]]] = { | ||
"chrome": None, | ||
"edge": None, | ||
"firefox": None, | ||
"remote": None, | ||
"django": None, | ||
"flask": None, | ||
"zope.testbrowser": None, | ||
} | ||
|
||
try: | ||
from splinter.driver.webdriver.chrome import WebDriver as ChromeWebDriver | ||
from splinter.driver.webdriver.firefox import WebDriver as FirefoxWebDriver | ||
from splinter.driver.webdriver.remote import WebDriver as RemoteWebDriver | ||
|
||
_DRIVERS["chrome"] = ChromeWebDriver | ||
_DRIVERS["firefox"] = FirefoxWebDriver | ||
_DRIVERS["remote"] = RemoteWebDriver | ||
except ImportError as e: | ||
logger.debug(f"Import Warning: {e}") | ||
|
||
try: | ||
from splinter.driver.webdriver.edge import WebDriver as EdgeWebDriver | ||
|
||
_DRIVERS["edge"] = EdgeWebDriver | ||
except ImportError as e: | ||
logger.debug(f"Import Warning: {e}") | ||
|
||
|
||
try: | ||
from splinter.driver.zopetestbrowser import ZopeTestBrowser | ||
|
||
_DRIVERS["zope.testbrowser"] = ZopeTestBrowser | ||
except ImportError as e: | ||
logger.debug(f"Import Warning: {e}") | ||
|
||
try: | ||
import django # noqa | ||
from splinter.driver.djangoclient import DjangoClient | ||
|
||
_DRIVERS["django"] = DjangoClient | ||
driver_exceptions = driver_exceptions + (WebDriverException,) | ||
except ImportError as e: | ||
logger.debug(f"Import Warning: {e}") | ||
|
||
try: | ||
import flask # noqa | ||
from splinter.driver.flaskclient import FlaskClient | ||
|
||
_DRIVERS["flask"] = FlaskClient | ||
except ImportError as e: | ||
logger.debug(f"Import Warning: {e}") | ||
logger.debug("Selenium WebDriverException not available: %s", str(e)) | ||
|
||
# Type alias for driver types | ||
DriverType = Union[None, Type[DriverAPI]] | ||
|
||
class DriverRegistry: | ||
"""Registry for managing browser drivers.""" | ||
|
||
_drivers: Dict[str, DriverType] = { | ||
"chrome": None, | ||
"edge": None, | ||
"firefox": None, | ||
"remote": None, | ||
"django": None, | ||
"flask": None, | ||
"zope.testbrowser": None, | ||
} | ||
|
||
@classmethod | ||
def register_driver(cls, name: str, driver: DriverType) -> None: | ||
"""Register a new driver.""" | ||
cls._drivers[name] = driver | ||
|
||
@classmethod | ||
def get_driver(cls, name: str) -> DriverType: | ||
"""Get a registered driver.""" | ||
return cls._drivers.get(name) | ||
|
||
# Register WebDriver implementations | ||
def _register_webdrivers() -> None: | ||
try: | ||
from splinter.driver.webdriver.chrome import WebDriver as ChromeWebDriver | ||
from splinter.driver.webdriver.firefox import WebDriver as FirefoxWebDriver | ||
from splinter.driver.webdriver.remote import WebDriver as RemoteWebDriver | ||
|
||
DriverRegistry.register_driver("chrome", ChromeWebDriver) | ||
DriverRegistry.register_driver("firefox", FirefoxWebDriver) | ||
DriverRegistry.register_driver("remote", RemoteWebDriver) | ||
except ImportError as e: | ||
logger.debug("WebDriver import failed: %s", str(e)) | ||
|
||
try: | ||
from splinter.driver.webdriver.edge import WebDriver as EdgeWebDriver | ||
DriverRegistry.register_driver("edge", EdgeWebDriver) | ||
except ImportError as e: | ||
logger.debug("Edge WebDriver import failed: %s", str(e)) | ||
|
||
def get_driver(driver, retry_count: int = 3, config=None, *args, **kwargs): | ||
"""Try to instantiate the driver. | ||
# Register other drivers | ||
def _register_other_drivers() -> None: | ||
try: | ||
from splinter.driver.zopetestbrowser import ZopeTestBrowser | ||
DriverRegistry.register_driver("zope.testbrowser", ZopeTestBrowser) | ||
except ImportError as e: | ||
logger.debug("Zope TestBrowser import failed: %s", str(e)) | ||
|
||
Common selenium errors are caught and a retry attempt occurs. | ||
This can mitigate issues running on Remote WebDriver. | ||
try: | ||
import django # noqa | ||
from splinter.driver.djangoclient import DjangoClient | ||
DriverRegistry.register_driver("django", DjangoClient) | ||
except ImportError as e: | ||
logger.debug("Django client import failed: %s", str(e)) | ||
|
||
try: | ||
import flask # noqa | ||
from splinter.driver.flaskclient import FlaskClient | ||
DriverRegistry.register_driver("flask", FlaskClient) | ||
except ImportError as e: | ||
logger.debug("Flask client import failed: %s", str(e)) | ||
|
||
# Initialize drivers | ||
_register_webdrivers() | ||
_register_other_drivers() | ||
|
||
def get_driver( | ||
driver: Type[DriverAPI], | ||
retry_count: int, | ||
config: Optional[Dict[str, Any]] = None, | ||
*args: Any, | ||
**kwargs: Any | ||
) -> DriverAPI: | ||
""" | ||
err = None | ||
|
||
for _ in range(retry_count): | ||
Try to instantiate the driver with retry mechanism. | ||
Args: | ||
driver: Driver class to instantiate | ||
retry_count: Number of retry attempts | ||
config: Optional configuration dictionary | ||
*args: Positional arguments for driver initialization | ||
**kwargs: Keyword arguments for driver initialization | ||
Returns: | ||
Instantiated driver | ||
Raises: | ||
Exception: Last caught exception if all retries fail | ||
""" | ||
if retry_count < 1: | ||
raise ValueError("retry_count must be positive") | ||
|
||
last_error = None | ||
for attempt in range(retry_count): | ||
try: | ||
return driver(config=config, *args, **kwargs) | ||
except driver_exceptions as e: | ||
err = e | ||
|
||
raise err | ||
|
||
|
||
def Browser( # NOQA: N802 | ||
last_error = e | ||
logger.warning( | ||
"Driver instantiation failed (attempt %d/%d): %s", | ||
attempt + 1, | ||
retry_count, | ||
str(e) | ||
) | ||
|
||
if last_error: | ||
raise last_error | ||
raise RuntimeError("Unknown error during driver instantiation") | ||
|
||
def Browser( | ||
driver_name: str = "firefox", | ||
retry_count: int = 3, | ||
config=None, | ||
*args, | ||
**kwargs, | ||
): | ||
"""Get a new driver instance. | ||
Extra arguments will be sent to the driver instance. | ||
If there is no driver registered with the provided ``driver_name``, this | ||
function will raise a :class:`splinter.exceptions.DriverNotFoundError` | ||
exception. | ||
Arguments: | ||
driver_name (str): Name of the driver to use. | ||
retry_count (int): Number of times to try and instantiate the driver. | ||
config: Optional[Dict[str, Any]] = None, | ||
*args: Any, | ||
**kwargs: Any | ||
) -> DriverAPI: | ||
""" | ||
Get a new driver instance. | ||
Args: | ||
driver_name: Name of the driver to use | ||
retry_count: Number of times to try instantiating the driver | ||
config: Optional configuration dictionary | ||
*args: Additional positional arguments for the driver | ||
**kwargs: Additional keyword arguments for the driver | ||
Returns: | ||
Driver instance | ||
Raises: | ||
DriverNotFoundError: If the requested driver is not available | ||
ValueError: If retry_count is less than 1 | ||
""" | ||
if retry_count < 1: | ||
raise ValueError("retry_count must be positive") | ||
|
||
try: | ||
driver = _DRIVERS[driver_name] | ||
except KeyError as err: | ||
raise DriverNotFoundError(f"{driver_name} is not a recognized driver.") from err | ||
|
||
driver = DriverRegistry.get_driver(driver_name.lower()) | ||
|
||
if driver is None: | ||
raise DriverNotFoundError(f"Driver for {driver_name} was not found.") | ||
|
||
return get_driver(driver, retry_count=retry_count, config=config, *args, **kwargs) | ||
raise DriverNotFoundError( | ||
f"Driver '{driver_name}' is not available. " | ||
"Please ensure the required dependencies are installed." | ||
) | ||
|
||
return get_driver(driver, retry_count, config, *args, **kwargs) |