initial commit

This commit is contained in:
2026-06-25 21:29:21 +00:00
commit 0d0a7456de
2738 changed files with 542622 additions and 0 deletions
@@ -0,0 +1,32 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import importlib
_LAZY_SUBMODULES = ["options", "service", "webdriver"]
def __getattr__(name):
if name in _LAZY_SUBMODULES:
module = importlib.import_module(f".{name}", __name__)
globals()[name] = module
return module
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def __dir__():
return sorted(_LAZY_SUBMODULES)
@@ -0,0 +1,26 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Type stub with lazy import mapping from __init__.py.
This stub file is necessary for type checkers and IDEs to automatically have
visibility into lazy modules since they are not imported immediately at runtime.
"""
from . import options, service, webdriver
__all__ = ["options", "service", "webdriver"]
@@ -0,0 +1,339 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from enum import Enum
from typing import Any
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.common.options import ArgOptions
class ElementScrollBehavior(Enum):
TOP = 0
BOTTOM = 1
class _IeOptionsDescriptor:
"""_IeOptionsDescriptor is an implementation of Descriptor Protocol.
Any look-up or assignment to the below attributes in `Options` class will be intercepted
by `__get__` and `__set__` method respectively.
- `browser_attach_timeout`
- `element_scroll_behavior`
- `ensure_clean_session`
- `file_upload_dialog_timeout`
- `force_create_process_api`
- `force_shell_windows_api`
- `full_page_screenshot`
- `ignore_protected_mode_settings`
- `ignore_zoom_level`
- `initial_browser_url`
- `native_events`
- `persistent_hover`
- `require_window_focus`
- `use_per_process_proxy`
- `use_legacy_file_upload_dialog_handling`
- `attach_to_edge_chrome`
- `edge_executable_path`
When an attribute lookup happens:
Example:
`self. browser_attach_timeout`
`__get__` method does a dictionary look up in the dictionary `_options` in `Options` class
and returns the value of key `browserAttachTimeout`
When an attribute assignment happens:
Example:
`self.browser_attach_timeout` = 30
`__set__` method sets/updates the value of the key `browserAttachTimeout` in `_options`
dictionary in `Options` class.
"""
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __get__(self, obj, cls):
return obj._options.get(self.name)
def __set__(self, obj, value) -> None:
if not isinstance(value, self.expected_type):
raise ValueError(f"{self.name} should be of type {self.expected_type.__name__}")
if self.name == "elementScrollBehavior" and value not in [
ElementScrollBehavior.TOP,
ElementScrollBehavior.BOTTOM,
]:
raise ValueError("Element Scroll Behavior out of range.")
obj._options[self.name] = value
class Options(ArgOptions):
KEY = "se:ieOptions"
SWITCHES = "ie.browserCommandLineSwitches"
BROWSER_ATTACH_TIMEOUT = "browserAttachTimeout"
ELEMENT_SCROLL_BEHAVIOR = "elementScrollBehavior"
ENSURE_CLEAN_SESSION = "ie.ensureCleanSession"
FILE_UPLOAD_DIALOG_TIMEOUT = "ie.fileUploadDialogTimeout"
FORCE_CREATE_PROCESS_API = "ie.forceCreateProcessApi"
FORCE_SHELL_WINDOWS_API = "ie.forceShellWindowsApi"
FULL_PAGE_SCREENSHOT = "ie.enableFullPageScreenshot"
IGNORE_PROTECTED_MODE_SETTINGS = "ignoreProtectedModeSettings"
IGNORE_ZOOM_LEVEL = "ignoreZoomSetting"
INITIAL_BROWSER_URL = "initialBrowserUrl"
NATIVE_EVENTS = "nativeEvents"
PERSISTENT_HOVER = "enablePersistentHover"
REQUIRE_WINDOW_FOCUS = "requireWindowFocus"
USE_PER_PROCESS_PROXY = "ie.usePerProcessProxy"
USE_LEGACY_FILE_UPLOAD_DIALOG_HANDLING = "ie.useLegacyFileUploadDialogHandling"
ATTACH_TO_EDGE_CHROME = "ie.edgechromium"
EDGE_EXECUTABLE_PATH = "ie.edgepath"
IGNORE_PROCESS_MATCH = "ie.ignoreprocessmatch"
# Creating descriptor objects for each of the above IE options
browser_attach_timeout = _IeOptionsDescriptor(BROWSER_ATTACH_TIMEOUT, int)
"""Gets and Sets `browser_attach_timeout`.
Usage:
- Get: `self.browser_attach_timeout`
- Set: `self.browser_attach_timeout = value`
Args:
value: int - Timeout in milliseconds.
"""
element_scroll_behavior = _IeOptionsDescriptor(ELEMENT_SCROLL_BEHAVIOR, Enum)
"""Gets and Sets `element_scroll_behavior`.
Usage:
- Get: `self.element_scroll_behavior`
- Set: `self.element_scroll_behavior = value`
Args:
value: int - Either 0 (Top) or 1 (Bottom).
"""
ensure_clean_session = _IeOptionsDescriptor(ENSURE_CLEAN_SESSION, bool)
"""Gets and Sets `ensure_clean_session`.
Usage:
- Get: `self.ensure_clean_session`
- Set: `self.ensure_clean_session = value`
Args:
value: bool
"""
file_upload_dialog_timeout = _IeOptionsDescriptor(FILE_UPLOAD_DIALOG_TIMEOUT, int)
"""Gets and Sets `file_upload_dialog_timeout`.
Usage:
- Get: `self.file_upload_dialog_timeout`
- Set: `self.file_upload_dialog_timeout = value`
Args:
value: int - Timeout in milliseconds.
"""
force_create_process_api = _IeOptionsDescriptor(FORCE_CREATE_PROCESS_API, bool)
"""Gets and Sets `force_create_process_api`.
Usage:
- Get: `self.force_create_process_api`
- Set: `self.force_create_process_api = value`
Args:
value: bool
"""
force_shell_windows_api = _IeOptionsDescriptor(FORCE_SHELL_WINDOWS_API, bool)
"""Gets and Sets `force_shell_windows_api`.
Usage:
- Get: `self.force_shell_windows_api`
- Set: `self.force_shell_windows_api = value`
Args:
value: bool
"""
full_page_screenshot = _IeOptionsDescriptor(FULL_PAGE_SCREENSHOT, bool)
"""Gets and Sets `full_page_screenshot`.
Usage:
- Get: `self.full_page_screenshot`
- Set: `self.full_page_screenshot = value`
Args:
value: bool
"""
ignore_protected_mode_settings = _IeOptionsDescriptor(IGNORE_PROTECTED_MODE_SETTINGS, bool)
"""Gets and Sets `ignore_protected_mode_settings`.
Usage:
- Get: `self.ignore_protected_mode_settings`
- Set: `self.ignore_protected_mode_settings = value`
Args:
value: bool
"""
ignore_zoom_level = _IeOptionsDescriptor(IGNORE_ZOOM_LEVEL, bool)
"""Gets and Sets `ignore_zoom_level`.
Usage:
- Get: `self.ignore_zoom_level`
- Set: `self.ignore_zoom_level = value`
Args:
value: bool
"""
initial_browser_url = _IeOptionsDescriptor(INITIAL_BROWSER_URL, str)
"""Gets and Sets `initial_browser_url`.
Usage:
- Get: `self.initial_browser_url`
- Set: `self.initial_browser_url = value`
Args:
value: str
"""
native_events = _IeOptionsDescriptor(NATIVE_EVENTS, bool)
"""Gets and Sets `native_events`.
Usage:
- Get: `self.native_events`
- Set: `self.native_events = value`
Args:
value: bool
"""
persistent_hover = _IeOptionsDescriptor(PERSISTENT_HOVER, bool)
"""Gets and Sets `persistent_hover`.
Usage:
- Get: `self.persistent_hover`
- Set: `self.persistent_hover = value`
Args:
value: bool
"""
require_window_focus = _IeOptionsDescriptor(REQUIRE_WINDOW_FOCUS, bool)
"""Gets and Sets `require_window_focus`.
Usage:
- Get: `self.require_window_focus`
- Set: `self.require_window_focus = value`
Args:
value: bool
"""
use_per_process_proxy = _IeOptionsDescriptor(USE_PER_PROCESS_PROXY, bool)
"""Gets and Sets `use_per_process_proxy`.
Usage:
- Get: `self.use_per_process_proxy`
- Set: `self.use_per_process_proxy = value`
Args:
value: bool
"""
use_legacy_file_upload_dialog_handling = _IeOptionsDescriptor(USE_LEGACY_FILE_UPLOAD_DIALOG_HANDLING, bool)
"""Gets and Sets `use_legacy_file_upload_dialog_handling`.
Usage:
- Get: `self.use_legacy_file_upload_dialog_handling`
- Set: `self.use_legacy_file_upload_dialog_handling = value`
Args:
value: bool
"""
attach_to_edge_chrome = _IeOptionsDescriptor(ATTACH_TO_EDGE_CHROME, bool)
"""Gets and Sets `attach_to_edge_chrome`.
Usage:
- Get: `self.attach_to_edge_chrome`
- Set: `self.attach_to_edge_chrome = value`
Args:
value: bool
"""
edge_executable_path = _IeOptionsDescriptor(EDGE_EXECUTABLE_PATH, str)
"""Gets and Sets `edge_executable_path`.
Usage:
- Get: `self.edge_executable_path`
- Set: `self.edge_executable_path = value`
Args:
value: str
"""
def __init__(self) -> None:
super().__init__()
self._options: dict[str, Any] = {}
self._additional: dict[str, Any] = {}
@property
def options(self) -> dict:
"""Returns a dictionary of browser options."""
return self._options
@property
def additional_options(self) -> dict:
"""Returns the additional options."""
return self._additional
def add_additional_option(self, name: str, value) -> None:
"""Adds an additional option not yet added as a safe option for IE.
Args:
name: name of the option to add
value: value of the option to add
"""
self._additional[name] = value
def to_capabilities(self) -> dict:
"""Marshals the IE options to the correct object."""
caps = self._caps
opts = self._options.copy()
if self._arguments:
opts[self.SWITCHES] = " ".join(self._arguments)
if self._additional:
opts.update(self._additional)
if opts:
caps[Options.KEY] = opts
return caps
@property
def default_capabilities(self) -> dict:
return DesiredCapabilities.INTERNETEXPLORER.copy()
@@ -0,0 +1,96 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import logging
import os
import sys
from collections.abc import Sequence
from typing import IO, Any
from selenium.webdriver.common import service
class Service(service.Service):
"""Service class responsible for starting and stopping of `IEDriver`.
Args:
executable_path: (Optional) Install path of the executable.
port: (Optional) Port for the service to run on, defaults to 0 where the operating system will decide.
host: (Optional) IP address the service port is bound
service_args: (Optional) Sequence of args to be passed to the subprocess when launching the executable.
log_level: (Optional) Level of logging of service, may be "FATAL", "ERROR", "WARN", "INFO", "DEBUG",
"TRACE". Default is "FATAL".
log_output: (Optional) int representation of STDOUT/DEVNULL, any IO instance or String path to file.
driver_path_env_key: (Optional) Environment variable to use to get the path to the driver executable.
**kwargs: Additional keyword arguments to pass to the parent Service class.
"""
def __init__(
self,
executable_path: str | None = None,
port: int = 0,
host: str | None = None,
service_args: Sequence[str] | None = None,
log_level: str | None = None,
log_output: int | str | IO[Any] | None = None,
driver_path_env_key: str | None = None,
**kwargs,
) -> None:
self._service_args = list(service_args or [])
driver_path_env_key = driver_path_env_key or "SE_IEDRIVER"
if host:
self._service_args.append(f"--host={host}")
if log_level:
self._service_args.append(f"--log-level={log_level}")
if os.environ.get("SE_DEBUG"):
has_arg_conflicts = any(x in arg for arg in self._service_args for x in ("log-level", "log-file"))
has_output_conflict = log_output is not None
if has_arg_conflicts or has_output_conflict:
logging.getLogger(__name__).warning(
"Environment Variable `SE_DEBUG` is set; "
"forcing IEDriver log level to DEBUG and overriding configured log level/output."
)
if has_arg_conflicts:
self._service_args = [
arg for arg in self._service_args if not any(x in arg for x in ("log-level", "log-file"))
]
self._service_args.append("--log-level=DEBUG")
log_output = sys.stderr
super().__init__(
executable_path=executable_path,
port=port,
log_output=log_output,
driver_path_env_key=driver_path_env_key,
**kwargs,
)
def command_line_args(self) -> list[str]:
return [f"--port={self.port}"] + self._service_args
@property
def service_args(self) -> Sequence[str]:
"""Returns the sequence of service arguments."""
return self._service_args
@service_args.setter
def service_args(self, value: Sequence[str]):
if isinstance(value, str) or not isinstance(value, Sequence):
raise TypeError("service_args must be a sequence")
self._service_args = list(value)
@@ -0,0 +1,60 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.common.driver_finder import DriverFinder
from selenium.webdriver.common.webdriver import LocalWebDriver
from selenium.webdriver.ie.options import Options
from selenium.webdriver.ie.service import Service
from selenium.webdriver.remote.client_config import ClientConfig
from selenium.webdriver.remote.remote_connection import RemoteConnection
class WebDriver(LocalWebDriver):
"""Control the IEServerDriver and drive Internet Explorer."""
def __init__(
self,
options: Options | None = None,
service: Service | None = None,
keep_alive: bool = True,
) -> None:
"""Creates a new instance of the Ie driver.
Starts the service and then creates new instance of Ie driver.
Args:
options: Instance of Options.
service: Service object for handling the browser driver if you need to pass extra details.
keep_alive: Whether to configure RemoteConnection to use HTTP keep-alive.
"""
self.service = service if service else Service()
self.options = options if options else Options()
self.service.path = self.service.env_path() or DriverFinder(self.service, self.options).get_driver_path()
self.service.start()
client_config = ClientConfig(remote_server_addr=self.service.service_url, keep_alive=keep_alive, timeout=120)
executor = RemoteConnection(
ignore_proxy=self.options._ignore_local_proxy,
client_config=client_config,
)
try:
super().__init__(command_executor=executor, options=self.options)
except Exception:
self.quit()
raise