Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(browser): Enhance Browser Module with Type Safety and Improve Documentation #1329

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
install:
pip install -r requirements/doc.txt

.PHONY: install
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ Getting Started

* `Installation <https://splinter.readthedocs.io/en/latest/install/install.html>`_

or

::

git clone https://github.com/cobrateam/splinter
make install

* `Tutorial <https://splinter.readthedocs.io/en/latest/tutorial.html>`_


Expand Down
241 changes: 142 additions & 99 deletions splinter/browser.py
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)