From 5d6aaa10517f36a9e89ffb559fe78c693d80135c Mon Sep 17 00:00:00 2001 From: "Caleb P. Burns" Date: Wed, 8 Dec 2021 17:51:48 -0500 Subject: [PATCH 01/53] Fix "NoReturn" type-hinnts. The [NoReturn](https://docs.python.org/3/library/typing.html#typing.NoReturn) type-hint in Python indicates the function never returns (e.g., it always raises an exception). These methods are incorrectly marked as `NoReturn` when they in fact return no value (`None`), and should by hinted using `None`. --- py/selenium/webdriver/common/options.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/py/selenium/webdriver/common/options.py b/py/selenium/webdriver/common/options.py index e940a4645a3b1..dd5e893bbb6ce 100644 --- a/py/selenium/webdriver/common/options.py +++ b/py/selenium/webdriver/common/options.py @@ -16,7 +16,6 @@ # under the License. from abc import ABCMeta, abstractmethod -from typing import NoReturn from selenium.webdriver.common.proxy import Proxy from selenium.common.exceptions import InvalidArgumentException @@ -48,7 +47,7 @@ def browser_version(self) -> str: return self._caps["browserVersion"] @browser_version.setter - def browser_version(self, version: str) -> NoReturn: + def browser_version(self, version: str) -> None: """ Requires the major version of the browser to match provided value: https://w3c.github.io/webdriver/#dfn-browser-version @@ -65,7 +64,7 @@ def platform_name(self) -> str: return self._caps["platformName"] @platform_name.setter - def platform_name(self, platform: str) -> NoReturn: + def platform_name(self, platform: str) -> None: """ Requires the platform to match the provided value: https://w3c.github.io/webdriver/#dfn-platform-name @@ -81,7 +80,7 @@ def page_load_strategy(self) -> str: return self._caps["pageLoadStrategy"] @page_load_strategy.setter - def page_load_strategy(self, strategy: str) -> NoReturn: + def page_load_strategy(self, strategy: str) -> None: """ Determines the point at which a navigation command is returned: https://w3c.github.io/webdriver/#dfn-table-of-page-load-strategies @@ -101,7 +100,7 @@ def unhandled_prompt_behavior(self) -> str: return self._caps["unhandledPromptBehavior"] @unhandled_prompt_behavior.setter - def unhandled_prompt_behavior(self, behavior: str) -> NoReturn: + def unhandled_prompt_behavior(self, behavior: str) -> None: """ How the driver should respond when an alert is present and the command sent is not handling the alert: https://w3c.github.io/webdriver/#dfn-table-of-page-load-strategies @@ -122,7 +121,7 @@ def timeouts(self) -> dict: return self._caps["timeouts"] @timeouts.setter - def timeouts(self, timeouts: dict) -> NoReturn: + def timeouts(self, timeouts: dict) -> None: """ How long the driver should wait for actions to complete before returning an error https://w3c.github.io/webdriver/#timeouts @@ -159,7 +158,7 @@ def accept_insecure_certs(self) -> bool: return self._caps.get('acceptInsecureCerts') @accept_insecure_certs.setter - def accept_insecure_certs(self, value: bool) -> NoReturn: + def accept_insecure_certs(self, value: bool) -> None: """ Whether untrusted and self-signed TLS certificates are implicitly trusted: https://w3c.github.io/webdriver/#dfn-insecure-tls-certificates From b4623ab4b35a4a4500a391e0604c3d6a54cbb5a1 Mon Sep 17 00:00:00 2001 From: colons Date: Tue, 21 Dec 2021 10:20:29 +0000 Subject: [PATCH 02/53] Reflect how find_elements returns a list, not just one WebElement. --- py/selenium/webdriver/remote/webdriver.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 7cee657fb5324..9036260a29418 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -472,7 +472,7 @@ def find_element_by_id(self, id_) -> WebElement: ) return self.find_element(by=By.ID, value=id_) - def find_elements_by_id(self, id_) -> WebElement: + def find_elements_by_id(self, id_) -> List[WebElement]: """ Finds multiple elements by id. @@ -520,7 +520,7 @@ def find_element_by_xpath(self, xpath) -> WebElement: ) return self.find_element(by=By.XPATH, value=xpath) - def find_elements_by_xpath(self, xpath) -> WebElement: + def find_elements_by_xpath(self, xpath) -> List[WebElement]: """ Finds multiple elements by xpath. @@ -568,7 +568,7 @@ def find_element_by_link_text(self, link_text) -> WebElement: ) return self.find_element(by=By.LINK_TEXT, value=link_text) - def find_elements_by_link_text(self, text) -> WebElement: + def find_elements_by_link_text(self, text) -> List[WebElement]: """ Finds elements by link text. @@ -616,7 +616,7 @@ def find_element_by_partial_link_text(self, link_text) -> WebElement: ) return self.find_element(by=By.PARTIAL_LINK_TEXT, value=link_text) - def find_elements_by_partial_link_text(self, link_text) -> WebElement: + def find_elements_by_partial_link_text(self, link_text) -> List[WebElement]: """ Finds elements by a partial match of their link text. @@ -664,7 +664,7 @@ def find_element_by_name(self, name) -> WebElement: ) return self.find_element(by=By.NAME, value=name) - def find_elements_by_name(self, name) -> WebElement: + def find_elements_by_name(self, name) -> List[WebElement]: """ Finds elements by name. @@ -712,7 +712,7 @@ def find_element_by_tag_name(self, name) -> WebElement: ) return self.find_element(by=By.TAG_NAME, value=name) - def find_elements_by_tag_name(self, name) -> WebElement: + def find_elements_by_tag_name(self, name) -> List[WebElement]: """ Finds elements by tag name. @@ -760,7 +760,7 @@ def find_element_by_class_name(self, name) -> WebElement: ) return self.find_element(by=By.CLASS_NAME, value=name) - def find_elements_by_class_name(self, name) -> WebElement: + def find_elements_by_class_name(self, name) -> List[WebElement]: """ Finds elements by class name. @@ -808,7 +808,7 @@ def find_element_by_css_selector(self, css_selector) -> WebElement: ) return self.find_element(by=By.CSS_SELECTOR, value=css_selector) - def find_elements_by_css_selector(self, css_selector) -> WebElement: + def find_elements_by_css_selector(self, css_selector) -> List[WebElement]: """ Finds elements by css selector. From 622c6aa5a48b627263ed2d4875c55cfef7c00453 Mon Sep 17 00:00:00 2001 From: colons Date: Tue, 21 Dec 2021 11:15:54 +0000 Subject: [PATCH 03/53] Reduce mypy's output to only include errors in the annotations. --- py/mypy.ini | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/py/mypy.ini b/py/mypy.ini index ae0d60cc721eb..16a7349a274b7 100644 --- a/py/mypy.ini +++ b/py/mypy.ini @@ -1,16 +1,3 @@ [mypy] files = selenium -ignore_missing_imports = False -warn_unused_configs = True -disallow_subclassing_any = True -disallow_any_generics = True -disallow_untyped_calls = True -disallow_untyped_defs = True -disallow_incomplete_defs = True -check_untyped_defs = True -disallow_untyped_decorators = True -no_implicit_optional = True -warn_redundant_casts = True -warn_unused_ignores = True -warn_return_any = True -warn_unreachable = True +ignore_missing_imports = True From 293e3116995e583943ccd758ead30e6f4345adf5 Mon Sep 17 00:00:00 2001 From: colons Date: Tue, 21 Dec 2021 11:30:14 +0000 Subject: [PATCH 04/53] Fix some more incorrect `NoReturn` usage. This is an extension of what was done in SeleniumHQ/selenium#10120. --- py/selenium/webdriver/chromium/options.py | 14 +++++++------- py/selenium/webdriver/chromium/webdriver.py | 9 ++++----- py/selenium/webdriver/firefox/webdriver.py | 7 +++---- py/selenium/webdriver/ie/webdriver.py | 3 +-- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/py/selenium/webdriver/chromium/options.py b/py/selenium/webdriver/chromium/options.py index fe906cbe8620c..d118a16eaa77a 100644 --- a/py/selenium/webdriver/chromium/options.py +++ b/py/selenium/webdriver/chromium/options.py @@ -18,7 +18,7 @@ import base64 import os import warnings -from typing import List, NoReturn, Union +from typing import List, Union from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.common.options import ArgOptions @@ -43,7 +43,7 @@ def binary_location(self) -> str: return self._binary_location @binary_location.setter - def binary_location(self, value: str) -> NoReturn: + def binary_location(self, value: str) -> None: """ Allows you to set where the chromium binary lives :Args: @@ -59,7 +59,7 @@ def debugger_address(self: str) -> str: return self._debugger_address @debugger_address.setter - def debugger_address(self, value: str) -> NoReturn: + def debugger_address(self, value: str) -> None: """ Allows you to set the address of the remote devtools instance that the ChromeDriver instance will try to connect to during an @@ -85,7 +85,7 @@ def extensions(self) -> List[str]: file_.close() return encoded_extensions + self._extensions - def add_extension(self, extension: str) -> NoReturn: + def add_extension(self, extension: str) -> None: """ Adds the path to the extension to a list that will be used to extract it to the ChromeDriver @@ -102,7 +102,7 @@ def add_extension(self, extension: str) -> NoReturn: else: raise ValueError("argument can not be null") - def add_encoded_extension(self, extension: str) -> NoReturn: + def add_encoded_extension(self, extension: str) -> None: """ Adds Base64 encoded string with extension data to a list that will be used to extract it to the ChromeDriver @@ -122,7 +122,7 @@ def experimental_options(self) -> dict: """ return self._experimental_options - def add_experimental_option(self, name: str, value: Union[str, int, dict, List[str]]) -> NoReturn: + def add_experimental_option(self, name: str, value: Union[str, int, dict, List[str]]) -> None: """ Adds an experimental option which is passed to chromium. @@ -142,7 +142,7 @@ def headless(self) -> bool: return '--headless' in self._arguments @headless.setter - def headless(self, value: bool) -> NoReturn: + def headless(self, value: bool) -> None: """ Sets the headless argument :Args: diff --git a/py/selenium/webdriver/chromium/webdriver.py b/py/selenium/webdriver/chromium/webdriver.py index 4534110dd360c..114ecf82cdcb3 100644 --- a/py/selenium/webdriver/chromium/webdriver.py +++ b/py/selenium/webdriver/chromium/webdriver.py @@ -15,7 +15,6 @@ # specific language governing permissions and limitations # under the License. -from typing import NoReturn from selenium.webdriver.common.options import BaseOptions from selenium.webdriver.common.service import Service from selenium.webdriver.edge.options import Options as EdgeOptions @@ -117,7 +116,7 @@ def get_network_conditions(self): """ return self.execute("getNetworkConditions")['value'] - def set_network_conditions(self, **network_conditions) -> NoReturn: + def set_network_conditions(self, **network_conditions) -> None: """ Sets Chromium network emulation settings. @@ -139,13 +138,13 @@ def set_network_conditions(self, **network_conditions) -> NoReturn: 'network_conditions': network_conditions }) - def delete_network_conditions(self) -> NoReturn: + def delete_network_conditions(self) -> None: """ Resets Chromium network emulation settings. """ self.execute("deleteNetworkConditions") - def set_permissions(self, name: str, value: str) -> NoReturn: + def set_permissions(self, name: str, value: str) -> None: """ Sets Applicable Permission. @@ -217,7 +216,7 @@ def stop_casting(self, sink_name: str) -> str: """ return self.execute('stopCasting', {'sinkName': sink_name}) - def quit(self) -> NoReturn: + def quit(self) -> None: """ Closes the browser and shuts down the ChromiumDriver executable that is started when starting the ChromiumDriver diff --git a/py/selenium/webdriver/firefox/webdriver.py b/py/selenium/webdriver/firefox/webdriver.py index b0aae190b2c92..2c0fce0850818 100644 --- a/py/selenium/webdriver/firefox/webdriver.py +++ b/py/selenium/webdriver/firefox/webdriver.py @@ -19,7 +19,6 @@ from shutil import rmtree import warnings from contextlib import contextmanager -from typing import NoReturn from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver @@ -184,7 +183,7 @@ def __init__(self, firefox_profile=None, firefox_binary=None, self._is_remote = False - def quit(self) -> NoReturn: + def quit(self) -> None: """Quits the driver and close every associated window.""" try: RemoteWebDriver.quit(self) @@ -208,7 +207,7 @@ def firefox_profile(self): # Extension commands: - def set_context(self, context) -> NoReturn: + def set_context(self, context) -> None: self.execute("SET_CONTEXT", {"context": context}) @contextmanager @@ -252,7 +251,7 @@ def install_addon(self, path, temporary=None) -> str: payload["temporary"] = temporary return self.execute("INSTALL_ADDON", payload)["value"] - def uninstall_addon(self, identifier) -> NoReturn: + def uninstall_addon(self, identifier) -> None: """ Uninstalls Firefox addon using its identifier. diff --git a/py/selenium/webdriver/ie/webdriver.py b/py/selenium/webdriver/ie/webdriver.py index 44c5c33dbd3fd..75f06325e3428 100644 --- a/py/selenium/webdriver/ie/webdriver.py +++ b/py/selenium/webdriver/ie/webdriver.py @@ -15,7 +15,6 @@ # specific language governing permissions and limitations # under the License. -from typing import NoReturn import warnings from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver @@ -115,7 +114,7 @@ def __init__(self, executable_path=DEFAULT_EXECUTABLE_PATH, capabilities=None, keep_alive=keep_alive) self._is_remote = False - def quit(self) -> NoReturn: + def quit(self) -> None: RemoteWebDriver.quit(self) self.iedriver.stop() From 98aab559d43093cbbea8bc982361e1a28c3d0cac Mon Sep 17 00:00:00 2001 From: colons Date: Tue, 21 Dec 2021 11:43:54 +0000 Subject: [PATCH 05/53] Placate mypy about an attribute missing from ChromeOptions. --- py/selenium/webdriver/common/options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/common/options.py b/py/selenium/webdriver/common/options.py index dd5e893bbb6ce..75b0337fb6e2f 100644 --- a/py/selenium/webdriver/common/options.py +++ b/py/selenium/webdriver/common/options.py @@ -25,6 +25,8 @@ class BaseOptions(metaclass=ABCMeta): Base class for individual browser options """ + _ignore_local_proxy: bool = False + def __init__(self): super(BaseOptions, self).__init__() self._caps = self.default_capabilities @@ -224,11 +226,9 @@ def default_capabilities(self): class ArgOptions(BaseOptions): - def __init__(self): super(ArgOptions, self).__init__() self._arguments = [] - self._ignore_local_proxy = False @property def arguments(self): From 9d766bcfe07961de378b5d8ed427fd0f64f69fa1 Mon Sep 17 00:00:00 2001 From: colons Date: Wed, 22 Dec 2021 16:48:50 +0000 Subject: [PATCH 06/53] Create an `ExecuteResponse` type to use for .execute() return values. --- py/selenium/webdriver/chromium/webdriver.py | 8 ++++---- py/selenium/webdriver/remote/webdriver.py | 20 +++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/py/selenium/webdriver/chromium/webdriver.py b/py/selenium/webdriver/chromium/webdriver.py index 114ecf82cdcb3..6ef683888762f 100644 --- a/py/selenium/webdriver/chromium/webdriver.py +++ b/py/selenium/webdriver/chromium/webdriver.py @@ -22,7 +22,7 @@ import warnings from selenium.webdriver.chromium.remote_connection import ChromiumRemoteConnection -from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from selenium.webdriver.remote.webdriver import ExecuteResponse, WebDriver as RemoteWebDriver DEFAULT_PORT = 0 DEFAULT_SERVICE_LOG_PATH = None @@ -189,7 +189,7 @@ def get_issue_message(self): """ return self.execute('getIssueMessage')['value'] - def set_sink_to_use(self, sink_name: str) -> str: + def set_sink_to_use(self, sink_name: str) -> ExecuteResponse: """ Sets a specific sink, using its name, as a Cast session receiver target. @@ -198,7 +198,7 @@ def set_sink_to_use(self, sink_name: str) -> str: """ return self.execute('setSinkToUse', {'sinkName': sink_name}) - def start_tab_mirroring(self, sink_name: str) -> str: + def start_tab_mirroring(self, sink_name: str) -> ExecuteResponse: """ Starts a tab mirroring session on a specific receiver target. @@ -207,7 +207,7 @@ def start_tab_mirroring(self, sink_name: str) -> str: """ return self.execute('startTabMirroring', {'sinkName': sink_name}) - def stop_casting(self, sink_name: str) -> str: + def stop_casting(self, sink_name: str) -> ExecuteResponse: """ Stops the existing Cast session on a specific receiver target. diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 9036260a29418..f5f55ad33c280 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -23,7 +23,7 @@ import pkgutil import sys -from typing import Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, TypedDict, Union import warnings @@ -132,9 +132,9 @@ def get_remote_connection(capabilities, command_executor, keep_alive, ignore_loc return handler(command_executor, keep_alive=keep_alive, ignore_proxy=ignore_local_proxy) -def create_matches(options: List[BaseOptions]) -> Dict: - capabilities = {"capabilities": {}} - opts = [] +def create_matches(options: List[BaseOptions]) -> Dict[str, Dict[str, Any]]: + capabilities: Dict[str, Dict[str, Any]] = {"capabilities": {}} + opts: List[dict] = [] for opt in options: opts.append(opt.to_capabilities()) opts_size = len(opts) @@ -156,9 +156,9 @@ def create_matches(options: List[BaseOptions]) -> Dict: for k, v in samesies.items(): always[k] = v - for i in opts: + for o in opts: for k in always.keys(): - del i[k] + del o[k] capabilities["capabilities"]["alwaysMatch"] = always capabilities["capabilities"]["firstMatch"] = opts @@ -166,6 +166,12 @@ def create_matches(options: List[BaseOptions]) -> Dict: return capabilities +class ExecuteResponse(TypedDict): + success: int + value: Any + sessionId: str + + class BaseWebDriver(metaclass=ABCMeta): """ Abstract Base Class for all Webdriver subtypes. @@ -402,7 +408,7 @@ def _unwrap_value(self, value): else: return value - def execute(self, driver_command: str, params: dict = None) -> dict: + def execute(self, driver_command: str, params: dict = None) -> ExecuteResponse: """ Sends a command to be executed by a command.CommandExecutor. From 92155328d9d959851855faced81c8fc531b2b078 Mon Sep 17 00:00:00 2001 From: colons Date: Wed, 22 Dec 2021 17:45:06 +0000 Subject: [PATCH 07/53] Fix some easy-to-fix errors in existing type annotations. Some of the remaining ones might require some refactoring to fix. Perhaps they should just be removed? I feel pretty strongly that incorrect type annotations should never have been here in the first place. --- py/selenium/webdriver/chromium/options.py | 12 ++++++------ py/selenium/webdriver/common/log.py | 9 +++++---- py/selenium/webdriver/common/utils.py | 3 +-- py/selenium/webdriver/firefox/options.py | 8 +++++--- py/selenium/webdriver/firefox/webdriver.py | 2 +- py/selenium/webdriver/ie/options.py | 2 +- py/selenium/webdriver/remote/mobile.py | 14 +++++++------- .../webdriver/remote/remote_connection.py | 3 ++- py/selenium/webdriver/remote/webdriver.py | 19 +++++++++++-------- py/selenium/webdriver/remote/webelement.py | 13 +++++++------ 10 files changed, 46 insertions(+), 39 deletions(-) diff --git a/py/selenium/webdriver/chromium/options.py b/py/selenium/webdriver/chromium/options.py index d118a16eaa77a..e0ffb9113858d 100644 --- a/py/selenium/webdriver/chromium/options.py +++ b/py/selenium/webdriver/chromium/options.py @@ -18,7 +18,7 @@ import base64 import os import warnings -from typing import List, Union +from typing import Any, Dict, List, Optional, Union from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.common.options import ArgOptions @@ -30,10 +30,10 @@ class ChromiumOptions(ArgOptions): def __init__(self) -> None: super(ChromiumOptions, self).__init__() self._binary_location = '' - self._extension_files = [] - self._extensions = [] - self._experimental_options = {} - self._debugger_address = None + self._extension_files: List[str] = [] + self._extensions: List[str] = [] + self._experimental_options: Dict[str, Any] = {} + self._debugger_address: Optional[str] = None @property def binary_location(self) -> str: @@ -52,7 +52,7 @@ def binary_location(self, value: str) -> None: self._binary_location = value @property - def debugger_address(self: str) -> str: + def debugger_address(self) -> Optional[str]: """ :Returns: The address of the remote devtools instance """ diff --git a/py/selenium/webdriver/common/log.py b/py/selenium/webdriver/common/log.py index b64c5cd7cb9f1..b6600fa2f3464 100644 --- a/py/selenium/webdriver/common/log.py +++ b/py/selenium/webdriver/common/log.py @@ -18,6 +18,7 @@ import json import pkgutil import sys +from typing import Any, AsyncIterator, Dict, cast from contextlib import asynccontextmanager from importlib import import_module @@ -49,10 +50,10 @@ def __init__(self, driver, bidi_session) -> None: self.cdp = bidi_session.cdp self.devtools = bidi_session.devtools _pkg = '.'.join(__name__.split('.')[:-1]) - self._mutation_listener_js = pkgutil.get_data(_pkg, 'mutation-listener.js').decode('utf8').strip() + self._mutation_listener_js = cast(bytes, pkgutil.get_data(_pkg, 'mutation-listener.js')).decode('utf8').strip() @asynccontextmanager - async def mutation_events(self) -> dict: + async def mutation_events(self) -> AsyncIterator[Dict[str, Any]]: """ Listens for mutation events and emits them as it finds them @@ -81,7 +82,7 @@ async def mutation_events(self) -> dict: script_key = await page.execute(self.devtools.page.add_script_to_evaluate_on_new_document(self._mutation_listener_js)) self.driver.pin_script(self._mutation_listener_js, script_key) self.driver.execute_script(f"return {self._mutation_listener_js}") - event = {} + event: Dict[str, Any] = {} async with runtime.wait_for(self.devtools.runtime.BindingCalled) as evnt: yield event @@ -119,7 +120,7 @@ async def add_js_error_listener(self): js_exception.exception_details = exception.value.exception_details @asynccontextmanager - async def add_listener(self, event_type) -> dict: + async def add_listener(self, event_type) -> AsyncIterator[Dict[str, Any]]: ''' Listens for certain events that are passed in. diff --git a/py/selenium/webdriver/common/utils.py b/py/selenium/webdriver/common/utils.py index c001e024fdc59..ce32e9fc87dca 100644 --- a/py/selenium/webdriver/common/utils.py +++ b/py/selenium/webdriver/common/utils.py @@ -23,7 +23,6 @@ import socket from selenium.types import AnyKey -from selenium.webdriver.common.keys import Keys _is_connectable_exceptions = (socket.error, ConnectionResetError) @@ -138,7 +137,7 @@ def keys_to_typing(value: Iterable[AnyKey]) -> List[str]: """Processes the values that will be typed in the element.""" typing: List[str] = [] for val in value: - if isinstance(val, Keys): + if isinstance(val, str): typing.append(val) elif isinstance(val, int) or isinstance(val, float): val = str(val) diff --git a/py/selenium/webdriver/firefox/options.py b/py/selenium/webdriver/firefox/options.py index 441dfe0965292..ce69d6b71a17b 100644 --- a/py/selenium/webdriver/firefox/options.py +++ b/py/selenium/webdriver/firefox/options.py @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from typing import Union +from typing import Optional, Union import warnings from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.firefox.firefox_binary import FirefoxBinary @@ -68,7 +68,7 @@ def binary_location(self) -> str: @binary_location.setter # noqa def binary_location(self, value: str): """ Sets the location of the browser binary by string """ - self.binary = value + self.binary = FirefoxBinary(value) @property def preferences(self) -> dict: @@ -127,7 +127,9 @@ def headless(self, value: bool): elif '-headless' in self._arguments: self._arguments.remove('-headless') - def enable_mobile(self, android_package: str = "org.mozilla.firefox", android_activity=None, device_serial=None): + def enable_mobile( + self, android_package: Optional[str] = "org.mozilla.firefox", android_activity: str = None, device_serial: str = None + ): super().enable_mobile(android_package, android_activity, device_serial) def to_capabilities(self) -> dict: diff --git a/py/selenium/webdriver/firefox/webdriver.py b/py/selenium/webdriver/firefox/webdriver.py index 2c0fce0850818..dd6f5733dc7df 100644 --- a/py/selenium/webdriver/firefox/webdriver.py +++ b/py/selenium/webdriver/firefox/webdriver.py @@ -307,7 +307,7 @@ def save_full_page_screenshot(self, filename) -> bool: """ return self.get_full_page_screenshot_as_file(filename) - def get_full_page_screenshot_as_png(self) -> str: + def get_full_page_screenshot_as_png(self) -> bytes: """ Gets the full document screenshot of the current window as a binary data. diff --git a/py/selenium/webdriver/ie/options.py b/py/selenium/webdriver/ie/options.py index cc72e26578272..0c5f61cbeed03 100644 --- a/py/selenium/webdriver/ie/options.py +++ b/py/selenium/webdriver/ie/options.py @@ -259,7 +259,7 @@ def persistent_hover(self, value: bool): self._options[self.PERSISTENT_HOVER] = value @property - def require_window_focus(self: bool): + def require_window_focus(self) -> bool: """:Returns: The options Require Window Focus value """ return self._options.get(self.REQUIRE_WINDOW_FOCUS) diff --git a/py/selenium/webdriver/remote/mobile.py b/py/selenium/webdriver/remote/mobile.py index f7d9a099871e7..81c89ac8c6d1c 100644 --- a/py/selenium/webdriver/remote/mobile.py +++ b/py/selenium/webdriver/remote/mobile.py @@ -71,16 +71,16 @@ def context(self): """ return self._driver.execute(Command.CURRENT_CONTEXT_HANDLE) - @property - def contexts(self): - """ - returns a list of available contexts - """ - return self._driver.execute(Command.CONTEXT_HANDLES) - @context.setter def context(self, new_context): """ sets the current context """ self._driver.execute(Command.SWITCH_TO_CONTEXT, {"name": new_context}) + + @property + def contexts(self): + """ + returns a list of available contexts + """ + return self._driver.execute(Command.CONTEXT_HANDLES) diff --git a/py/selenium/webdriver/remote/remote_connection.py b/py/selenium/webdriver/remote/remote_connection.py index d70770b28a98e..17a93f5642e58 100644 --- a/py/selenium/webdriver/remote/remote_connection.py +++ b/py/selenium/webdriver/remote/remote_connection.py @@ -18,6 +18,7 @@ import logging import socket import string +from typing import Optional import os import certifi @@ -41,7 +42,7 @@ class RemoteConnection(object): Communicates with the server using the WebDriver wire protocol: https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol""" - browser_name = None + browser_name: Optional[str] = None _timeout = socket._GLOBAL_DEFAULT_TIMEOUT _ca_certs = certifi.where() diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index f5f55ad33c280..b88404fa62629 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -23,7 +23,7 @@ import pkgutil import sys -from typing import Any, Dict, List, Optional, TypedDict, Union +from typing import Any, Dict, List, Optional, TYPE_CHECKING, TypedDict, Union, cast import warnings @@ -54,6 +54,9 @@ from selenium.webdriver.common.html5.application_cache import ApplicationCache from selenium.webdriver.support.relative_locator import RelativeBy +if TYPE_CHECKING: + from selenium.webdriver.common.print_page_options import _PrintOpts + _W3C_CAPABILITY_NAMES = frozenset([ 'acceptInsecureCerts', @@ -265,8 +268,8 @@ def __init__(self, command_executor='http://127.0.0.1:4444', ignore_local_proxy=_ignore_local_proxy) self._is_remote = True self.session_id = None - self.caps = {} - self.pinned_scripts = {} + self.caps: Dict[str, Any] = {} + self.pinned_scripts: Dict[str, Any] = {} self.error_handler = ErrorHandler() self._switch_to = SwitchTo(self) self._mobile = Mobile(self) @@ -1007,11 +1010,11 @@ def print_page(self, print_options: Optional[PrintOptions] = None) -> str: Takes PDF of the current page. The driver makes a best effort to return a PDF based on the provided parameters. """ - options = {} + options: '_PrintOpts' = {} if print_options: options = print_options.to_dict() - return self.execute(Command.PRINT_PAGE, options)['value'] + return self.execute(Command.PRINT_PAGE, cast(dict, options))['value'] @property def switch_to(self) -> SwitchTo: @@ -1079,7 +1082,7 @@ def get_cookies(self) -> List[dict]: """ return self.execute(Command.GET_ALL_COOKIES)['value'] - def get_cookie(self, name) -> dict: + def get_cookie(self, name) -> Optional[dict]: """ Get a single cookie by name. Returns the cookie if found, None if not. @@ -1268,7 +1271,7 @@ def find_elements(self, by=By.ID, value=None) -> List[WebElement]: """ if isinstance(by, RelativeBy): _pkg = '.'.join(__name__.split('.')[:-1]) - raw_function = pkgutil.get_data(_pkg, 'findElements.js').decode('utf8') + raw_function = cast(bytes, pkgutil.get_data(_pkg, 'findElements.js')).decode('utf8') find_element_js = "return ({}).apply(null, arguments);".format(raw_function) return self.execute_script(find_element_js, by.to_dict()) @@ -1374,7 +1377,7 @@ def get_screenshot_as_base64(self) -> str: """ return self.execute(Command.SCREENSHOT)['value'] - def set_window_size(self, width, height, windowHandle='current') -> dict: + def set_window_size(self, width, height, windowHandle='current') -> None: """ Sets the width and height of the current window. (window.resizeTo) diff --git a/py/selenium/webdriver/remote/webelement.py b/py/selenium/webdriver/remote/webelement.py index 6c71dc92e1730..39817f24426b6 100644 --- a/py/selenium/webdriver/remote/webelement.py +++ b/py/selenium/webdriver/remote/webelement.py @@ -23,6 +23,7 @@ import zipfile from abc import ABCMeta from io import BytesIO +from typing import cast from selenium.common.exceptions import WebDriverException from selenium.webdriver.common.by import By @@ -34,8 +35,8 @@ # TODO: When moving to supporting python 3.9 as the minimum version we can # use built in importlib_resources.files. _pkg = '.'.join(__name__.split('.')[:-1]) -getAttribute_js = pkgutil.get_data(_pkg, 'getAttribute.js').decode('utf8') -isDisplayed_js = pkgutil.get_data(_pkg, 'isDisplayed.js').decode('utf8') +getAttribute_js = cast(bytes, pkgutil.get_data(_pkg, 'getAttribute.js')).decode('utf8') +isDisplayed_js = cast(bytes, pkgutil.get_data(_pkg, 'isDisplayed.js')).decode('utf8') class BaseWebElement(metaclass=ABCMeta): @@ -535,11 +536,11 @@ def send_keys(self, *value) -> None: remote_files = [] for file in local_files: remote_files.append(self._upload(file)) - value = '\n'.join(remote_files) + output = '\n'.join(remote_files) self._execute(Command.SEND_KEYS_TO_ELEMENT, - {'text': "".join(keys_to_typing(value)), - 'value': keys_to_typing(value)}) + {'text': "".join(keys_to_typing(output)), + 'value': keys_to_typing(output)}) @property def shadow_root(self) -> ShadowRoot: @@ -630,7 +631,7 @@ def screenshot_as_base64(self) -> str: return self._execute(Command.ELEMENT_SCREENSHOT)['value'] @property - def screenshot_as_png(self) -> str: + def screenshot_as_png(self) -> bytes: """ Gets the screenshot of the current element as a binary data. From 153289da9dbaf8e69e5f9550e1a0c9de3167ba4c Mon Sep 17 00:00:00 2001 From: colons Date: Thu, 30 Dec 2021 16:55:46 +0000 Subject: [PATCH 08/53] Make send_keys behave like it did before this branch. --- py/selenium/webdriver/remote/webelement.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/remote/webelement.py b/py/selenium/webdriver/remote/webelement.py index 39817f24426b6..f2986b6bb2048 100644 --- a/py/selenium/webdriver/remote/webelement.py +++ b/py/selenium/webdriver/remote/webelement.py @@ -23,9 +23,10 @@ import zipfile from abc import ABCMeta from io import BytesIO -from typing import cast +from typing import Iterable, cast from selenium.common.exceptions import WebDriverException +from selenium.types import AnyKey from selenium.webdriver.common.by import By from selenium.webdriver.common.utils import keys_to_typing from .command import Command @@ -503,7 +504,7 @@ def find_elements_by_css_selector(self, css_selector): warnings.warn("find_elements_by_* commands are deprecated. Please use find_elements() instead") return self.find_elements(by=By.CSS_SELECTOR, value=css_selector) - def send_keys(self, *value) -> None: + def send_keys(self, *value: AnyKey) -> None: """Simulates typing into the element. :Args: @@ -528,6 +529,9 @@ def send_keys(self, *value) -> None: """ # transfer file to another machine only if remote driver is used # the same behaviour as for java binding + + output: Iterable[AnyKey] + if self.parent._is_remote: local_files = list(map(lambda keys_to_send: self.parent.file_detector.is_local_file(str(keys_to_send)), @@ -537,6 +541,8 @@ def send_keys(self, *value) -> None: for file in local_files: remote_files.append(self._upload(file)) output = '\n'.join(remote_files) + else: + output = value self._execute(Command.SEND_KEYS_TO_ELEMENT, {'text': "".join(keys_to_typing(output)), From 62911c1c34a15bdb8e9ab8bf9aa058e6ffea3e07 Mon Sep 17 00:00:00 2001 From: colons Date: Thu, 30 Dec 2021 17:15:51 +0000 Subject: [PATCH 09/53] Placate a few more fair mypy complaints. --- py/selenium/webdriver/common/actions/action_builder.py | 4 ++-- py/selenium/webdriver/common/desired_capabilities.py | 5 ++++- py/selenium/webdriver/firefox/remote_connection.py | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/py/selenium/webdriver/common/actions/action_builder.py b/py/selenium/webdriver/common/actions/action_builder.py index 6588d083042b2..9e0e2b540eeaf 100644 --- a/py/selenium/webdriver/common/actions/action_builder.py +++ b/py/selenium/webdriver/common/actions/action_builder.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -from typing import Union, List +from typing import Optional, Union, List from selenium.webdriver.remote.command import Command from . import interaction from .key_actions import KeyActions @@ -40,7 +40,7 @@ def __init__(self, driver, mouse=None, wheel=None, keyboard=None, duration=250) self._wheel_action = WheelActions(wheel) self.driver = driver - def get_device_with(self, name) -> Union["WheelInput", "PointerInput", "KeyInput"]: + def get_device_with(self, name) -> Optional[Union["WheelInput", "PointerInput", "KeyInput"]]: return next(filter(lambda x: x == name, self.devices), None) @property diff --git a/py/selenium/webdriver/common/desired_capabilities.py b/py/selenium/webdriver/common/desired_capabilities.py index 0f97e7273aeda..f68649836ab16 100644 --- a/py/selenium/webdriver/common/desired_capabilities.py +++ b/py/selenium/webdriver/common/desired_capabilities.py @@ -20,6 +20,9 @@ """ +FIREFOX_NAME = "firefox" + + class DesiredCapabilities(object): """ Set of default supported desired capabilities. @@ -48,7 +51,7 @@ class DesiredCapabilities(object): """ FIREFOX = { - "browserName": "firefox", + "browserName": FIREFOX_NAME, "acceptInsecureCerts": True, "moz:debuggerAddress": True, } diff --git a/py/selenium/webdriver/firefox/remote_connection.py b/py/selenium/webdriver/firefox/remote_connection.py index f1860ee005eb7..ae3a981562ad3 100644 --- a/py/selenium/webdriver/firefox/remote_connection.py +++ b/py/selenium/webdriver/firefox/remote_connection.py @@ -16,12 +16,12 @@ # under the License. from selenium.webdriver.remote.remote_connection import RemoteConnection -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.common.desired_capabilities import FIREFOX_NAME class FirefoxRemoteConnection(RemoteConnection): - browser_name = DesiredCapabilities.FIREFOX['browserName'] + browser_name = FIREFOX_NAME def __init__(self, remote_server_addr, keep_alive=True, ignore_proxy=False): RemoteConnection.__init__(self, remote_server_addr, keep_alive, ignore_proxy=ignore_proxy) From 0fcffcc3cd1f783419559c26bda0f2835ae953fa Mon Sep 17 00:00:00 2001 From: colons Date: Thu, 30 Dec 2021 17:31:49 +0000 Subject: [PATCH 10/53] Annotate this 'enc' dict. --- py/selenium/webdriver/common/actions/action_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/common/actions/action_builder.py b/py/selenium/webdriver/common/actions/action_builder.py index 9e0e2b540eeaf..b154ebd5652d4 100644 --- a/py/selenium/webdriver/common/actions/action_builder.py +++ b/py/selenium/webdriver/common/actions/action_builder.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -from typing import Optional, Union, List +from typing import Any, Dict, Optional, Union, List from selenium.webdriver.remote.command import Command from . import interaction from .key_actions import KeyActions @@ -79,7 +79,7 @@ def add_wheel_input(self, kind, name) -> WheelInput: return new_input def perform(self) -> None: - enc = {"actions": []} + enc: Dict[str, List[Dict[str, Any]]] = {"actions": []} for device in self.devices: encoded = device.encode() if encoded['actions']: From 275fec4d2a4f5dd43c29c4201a8f61ec0768056c Mon Sep 17 00:00:00 2001 From: colons Date: Thu, 30 Dec 2021 17:35:20 +0000 Subject: [PATCH 11/53] Properly annotate ActionBuilder.devices. --- .../webdriver/common/actions/action_builder.py | 11 +++++++---- py/selenium/webdriver/common/actions/input_device.py | 6 ++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/py/selenium/webdriver/common/actions/action_builder.py b/py/selenium/webdriver/common/actions/action_builder.py index b154ebd5652d4..b7847ccfddaca 100644 --- a/py/selenium/webdriver/common/actions/action_builder.py +++ b/py/selenium/webdriver/common/actions/action_builder.py @@ -15,9 +15,10 @@ # specific language governing permissions and limitations # under the License. -from typing import Any, Dict, Optional, Union, List +from typing import Any, Dict, Optional, List from selenium.webdriver.remote.command import Command from . import interaction +from .input_device import InputDevice from .key_actions import KeyActions from .key_input import KeyInput from .pointer_actions import PointerActions @@ -27,6 +28,8 @@ class ActionBuilder(object): + devices: List[InputDevice] + def __init__(self, driver, mouse=None, wheel=None, keyboard=None, duration=250) -> None: if not mouse: mouse = PointerInput(interaction.POINTER_MOUSE, "mouse") @@ -40,16 +43,16 @@ def __init__(self, driver, mouse=None, wheel=None, keyboard=None, duration=250) self._wheel_action = WheelActions(wheel) self.driver = driver - def get_device_with(self, name) -> Optional[Union["WheelInput", "PointerInput", "KeyInput"]]: + def get_device_with(self, name) -> Optional[InputDevice]: return next(filter(lambda x: x == name, self.devices), None) @property def pointer_inputs(self) -> List[PointerInput]: - return [device for device in self.devices if device.type == interaction.POINTER] + return [device for device in self.devices if isinstance(device, PointerInput)] @property def key_inputs(self) -> List[KeyInput]: - return [device for device in self.devices if device.type == interaction.KEY] + return [device for device in self.devices if isinstance(device, KeyInput)] @property def key_action(self) -> KeyActions: diff --git a/py/selenium/webdriver/common/actions/input_device.py b/py/selenium/webdriver/common/actions/input_device.py index 7ea26c5b2c95c..f64d537b3dbc0 100644 --- a/py/selenium/webdriver/common/actions/input_device.py +++ b/py/selenium/webdriver/common/actions/input_device.py @@ -15,6 +15,8 @@ # specific language governing permissions and limitations # under the License. +from abc import abstractmethod +from typing import Any, Dict import uuid @@ -41,3 +43,7 @@ def clear_actions(self): def create_pause(self, duration=0): pass + + @abstractmethod + def encode(self) -> Dict[str, Any]: + raise NotImplementedError() From 049ced29e163b1d8be9c3cd831573a74afb4d4d0 Mon Sep 17 00:00:00 2001 From: colons Date: Fri, 31 Dec 2021 12:13:13 +0000 Subject: [PATCH 12/53] Fix an ImportError on Python 3.7 and earlier. --- py/selenium/webdriver/remote/webdriver.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 6b9bbb0fc8204..0e645b472bd38 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -23,7 +23,11 @@ import pkgutil import sys -from typing import Any, Dict, List, Optional, TYPE_CHECKING, TypedDict, Union, cast +from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union, cast +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict import warnings From fb2b991762d9c4690e3b3c520dc171274d2ae74e Mon Sep 17 00:00:00 2001 From: colons Date: Fri, 31 Dec 2021 18:48:04 +0000 Subject: [PATCH 13/53] Shuffle my feet nervously. --- .mailmap | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.mailmap b/.mailmap index 27d944a97709f..1997006a681be 100644 --- a/.mailmap +++ b/.mailmap @@ -7,7 +7,6 @@ - @@ -23,6 +22,8 @@ Andreas Tolfsen Andreas Tolf Tolfsen Andreas Tolfsen Aslak Hellesøy Aslak Hellesoy Chethana Paniyadi CHETANA PANIYADI +colons +colons Daniel Davison ddavison David Burns David Burns Eran Messeri Eran Mes From 5b1d1d1d2993e99633dfec80e9052029889fa2ee Mon Sep 17 00:00:00 2001 From: colons Date: Tue, 4 Jan 2022 11:30:10 +0000 Subject: [PATCH 14/53] Don't special case instances of Keys. The if case that was here before this PR existed required someone to hand in an instance of Keys, which I don't think was ever happening. Here, then, we just remove the case entirely and fall back to iterating across any string, even ones of length 1. --- py/selenium/webdriver/common/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/py/selenium/webdriver/common/utils.py b/py/selenium/webdriver/common/utils.py index ce32e9fc87dca..e22d72a05c223 100644 --- a/py/selenium/webdriver/common/utils.py +++ b/py/selenium/webdriver/common/utils.py @@ -137,9 +137,7 @@ def keys_to_typing(value: Iterable[AnyKey]) -> List[str]: """Processes the values that will be typed in the element.""" typing: List[str] = [] for val in value: - if isinstance(val, str): - typing.append(val) - elif isinstance(val, int) or isinstance(val, float): + if isinstance(val, int) or isinstance(val, float): val = str(val) for i in range(len(val)): typing.append(val[i]) From 41b47ead89f29aa5c9535d46385a94228f6d517e Mon Sep 17 00:00:00 2001 From: colons Date: Wed, 26 Jan 2022 15:27:00 +0000 Subject: [PATCH 15/53] Prevent an UnboundLocalError in WebElement.send_keys(). --- py/selenium/webdriver/remote/webelement.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/py/selenium/webdriver/remote/webelement.py b/py/selenium/webdriver/remote/webelement.py index f2986b6bb2048..62633cbbae136 100644 --- a/py/selenium/webdriver/remote/webelement.py +++ b/py/selenium/webdriver/remote/webelement.py @@ -530,7 +530,7 @@ def send_keys(self, *value: AnyKey) -> None: # transfer file to another machine only if remote driver is used # the same behaviour as for java binding - output: Iterable[AnyKey] + output: Iterable[AnyKey] = value if self.parent._is_remote: local_files = list(map(lambda keys_to_send: @@ -541,8 +541,6 @@ def send_keys(self, *value: AnyKey) -> None: for file in local_files: remote_files.append(self._upload(file)) output = '\n'.join(remote_files) - else: - output = value self._execute(Command.SEND_KEYS_TO_ELEMENT, {'text': "".join(keys_to_typing(output)), From a09d7ba0475c4b4a865e98cc21660a092d594fb6 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 16:41:24 +0100 Subject: [PATCH 16/53] Fix most of the new mypy complaints. I may have broken things in the meantime. --- py/mypy.ini | 1 + py/selenium/webdriver/chrome/options.py | 2 +- py/selenium/webdriver/chromium/options.py | 2 +- .../webdriver/common/actions/input_device.py | 4 ++-- .../webdriver/common/actions/wheel_input.py | 4 ++-- py/selenium/webdriver/common/by.py | 4 +++- py/selenium/webdriver/common/options.py | 14 ++++++++------ .../webdriver/common/virtual_authenticator.py | 10 ++++++++-- py/selenium/webdriver/remote/remote_connection.py | 3 ++- py/selenium/webdriver/remote/shadowroot.py | 4 ++-- py/selenium/webdriver/remote/webdriver.py | 14 +++++++++----- py/selenium/webdriver/safari/service.py | 4 +--- py/selenium/webdriver/support/relative_locator.py | 2 +- py/selenium/webdriver/support/wait.py | 7 +++++-- 14 files changed, 46 insertions(+), 29 deletions(-) diff --git a/py/mypy.ini b/py/mypy.ini index 16a7349a274b7..9236d08520797 100644 --- a/py/mypy.ini +++ b/py/mypy.ini @@ -1,3 +1,4 @@ [mypy] files = selenium ignore_missing_imports = True +warn_unused_ignores = True diff --git a/py/selenium/webdriver/chrome/options.py b/py/selenium/webdriver/chrome/options.py index 80ef3064c6e84..c94a062d53c0b 100644 --- a/py/selenium/webdriver/chrome/options.py +++ b/py/selenium/webdriver/chrome/options.py @@ -27,7 +27,7 @@ def default_capabilities(self) -> dict: return DesiredCapabilities.CHROME.copy() def enable_mobile(self, - android_package: str = "com.android.chrome", + android_package: Optional[str] = "com.android.chrome", android_activity: Optional[str] = None, device_serial: Optional[str] = None ) -> None: diff --git a/py/selenium/webdriver/chromium/options.py b/py/selenium/webdriver/chromium/options.py index 02bb704aaaf40..bcf8df5e979d3 100644 --- a/py/selenium/webdriver/chromium/options.py +++ b/py/selenium/webdriver/chromium/options.py @@ -18,7 +18,7 @@ import base64 import os import warnings -from typing import Any, Dict, List, Optional, Union +from typing import Any, BinaryIO, Dict, List, Optional, Union from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.common.options import ArgOptions diff --git a/py/selenium/webdriver/common/actions/input_device.py b/py/selenium/webdriver/common/actions/input_device.py index eb869ad14dede..0ad947196cdb5 100644 --- a/py/selenium/webdriver/common/actions/input_device.py +++ b/py/selenium/webdriver/common/actions/input_device.py @@ -16,7 +16,7 @@ # under the License. from abc import abstractmethod -from typing import Any, Dict +from typing import Any, Dict, Union import uuid @@ -41,7 +41,7 @@ def add_action(self, action): def clear_actions(self): self.actions = [] - def create_pause(self, duration=0): + def create_pause(self, duration: Union[int, float] = 0) -> None: pass @abstractmethod diff --git a/py/selenium/webdriver/common/actions/wheel_input.py b/py/selenium/webdriver/common/actions/wheel_input.py index c20089ec2f7e9..43400c4d72f40 100644 --- a/py/selenium/webdriver/common/actions/wheel_input.py +++ b/py/selenium/webdriver/common/actions/wheel_input.py @@ -69,6 +69,6 @@ def create_scroll(self, x: int, y: int, delta_x: int, "deltaY": delta_y, "duration": duration, "origin": origin}) - def create_pause(self, pause_duration: Union[int, float]) -> None: + def create_pause(self, duration: Union[int, float] = 0) -> None: self.add_action( - {"type": "pause", "duration": int(pause_duration * 1000)}) + {"type": "pause", "duration": int(duration * 1000)}) diff --git a/py/selenium/webdriver/common/by.py b/py/selenium/webdriver/common/by.py index 905c32567b203..b6289658443d2 100644 --- a/py/selenium/webdriver/common/by.py +++ b/py/selenium/webdriver/common/by.py @@ -19,8 +19,10 @@ The By implementation. """ +from enum import Enum -class By: + +class By(Enum): """ Set of supported locator strategies. """ diff --git a/py/selenium/webdriver/common/options.py b/py/selenium/webdriver/common/options.py index 57927d9b1a3d9..09a3ac0a1ccac 100644 --- a/py/selenium/webdriver/common/options.py +++ b/py/selenium/webdriver/common/options.py @@ -14,8 +14,8 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -import typing from abc import ABCMeta, abstractmethod +from typing import List, Optional from selenium.common.exceptions import InvalidArgumentException from selenium.webdriver.common.proxy import Proxy @@ -26,11 +26,13 @@ class BaseOptions(metaclass=ABCMeta): Base class for individual browser options """ + _ignore_local_proxy: bool + def __init__(self) -> None: super().__init__() self._caps = self.default_capabilities self.set_capability("pageLoadStrategy", "normal") - self.mobile_options = None + self.mobile_options: Optional[dict] = None @property def capabilities(self): @@ -134,8 +136,8 @@ def timeouts(self, timeouts: dict) -> None: else: raise ValueError("Timeout keys can only be one of the following: implicit, pageLoad, script") - def enable_mobile(self, android_package: typing.Optional[str] = None, android_activity: typing.Optional[str] = None, - device_serial: typing.Optional[str] = None) -> None: + def enable_mobile(self, android_package: Optional[str] = None, android_activity: Optional[str] = None, + device_serial: Optional[str] = None) -> None: """ Enables mobile browser use for browsers that support it @@ -228,7 +230,7 @@ def default_capabilities(self): class ArgOptions(BaseOptions): def __init__(self) -> None: super().__init__() - self._arguments = [] + self._arguments: List[str] = [] @property def arguments(self): @@ -237,7 +239,7 @@ def arguments(self): """ return self._arguments - def add_argument(self, argument): + def add_argument(self, argument: str) -> None: """ Adds an argument to the list diff --git a/py/selenium/webdriver/common/virtual_authenticator.py b/py/selenium/webdriver/common/virtual_authenticator.py index 6d44ceeb6a7fa..ba33fa049c34c 100644 --- a/py/selenium/webdriver/common/virtual_authenticator.py +++ b/py/selenium/webdriver/common/virtual_authenticator.py @@ -40,6 +40,12 @@ class Transport(Enum): INTERNAL = "internal" +# to enable these types to be referenced in the class scope of +# VirtualAuthenticatorOptions, which shadows these enum's normal names: +_Protocol = Protocol +_Transport = Transport + + class VirtualAuthenticatorOptions: Protocol = Protocol @@ -69,7 +75,7 @@ def protocol(self) -> str: return self._protocol.value @protocol.setter - def protocol(self, protocol: Protocol) -> None: + def protocol(self, protocol: _Protocol) -> None: self._protocol = protocol @property @@ -77,7 +83,7 @@ def transport(self) -> str: return self._transport.value @transport.setter - def transport(self, transport: Transport) -> None: + def transport(self, transport: _Transport) -> None: self._transport = transport @property diff --git a/py/selenium/webdriver/remote/remote_connection.py b/py/selenium/webdriver/remote/remote_connection.py index 0d1e035336698..5f964e76873d9 100644 --- a/py/selenium/webdriver/remote/remote_connection.py +++ b/py/selenium/webdriver/remote/remote_connection.py @@ -45,7 +45,8 @@ class RemoteConnection: https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol""" browser_name: Optional[str] = None - _timeout = socket._GLOBAL_DEFAULT_TIMEOUT + # this is not part of socket's public API: + _timeout = socket._GLOBAL_DEFAULT_TIMEOUT # type: ignore _ca_certs = certifi.where() @classmethod diff --git a/py/selenium/webdriver/remote/shadowroot.py b/py/selenium/webdriver/remote/shadowroot.py index 72db4d2940c88..02432081322c5 100644 --- a/py/selenium/webdriver/remote/shadowroot.py +++ b/py/selenium/webdriver/remote/shadowroot.py @@ -40,7 +40,7 @@ def __repr__(self) -> str: type(self), self.session.session_id, self._id ) - def find_element(self, by: str = By.ID, value: str = None): + def find_element(self, by: By = By.ID, value: str = None): if by == By.ID: by = By.CSS_SELECTOR value = '[id="%s"]' % value @@ -55,7 +55,7 @@ def find_element(self, by: str = By.ID, value: str = None): Command.FIND_ELEMENT_FROM_SHADOW_ROOT, {"using": by, "value": value} )["value"] - def find_elements(self, by: str = By.ID, value: str = None): + def find_elements(self, by: By = By.ID, value: str = None): if by == By.ID: by = By.CSS_SELECTOR value = '[id="%s"]' % value diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index da38bfbdda323..efdd1cf180c46 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -27,7 +27,7 @@ from contextlib import asynccontextmanager, contextmanager from importlib import import_module import sys -from typing import Any, Dict, List, Optional, TYPE_CHECKING, TracebackType, Union, cast +from typing import Any, Dict, List, Optional, TYPE_CHECKING, Type, Union, cast if sys.version_info >= (3, 8): from typing import TypedDict else: @@ -60,6 +60,9 @@ from .switch_to import SwitchTo from .webelement import WebElement +if TYPE_CHECKING: + from selenium.webdriver.common.print_page_options import _PrintOpts + _W3C_CAPABILITY_NAMES = frozenset([ 'acceptInsecureCerts', 'browserName', @@ -290,7 +293,7 @@ def __enter__(self): def __exit__(self, exc_type: Optional[Type[BaseException]], exc: Optional[BaseException], - traceback: Optional[TracebackType]): + traceback: Optional[types.TracebackType]): self.quit() @contextmanager @@ -695,7 +698,7 @@ def get_cookies(self) -> List[dict]: """ return self.execute(Command.GET_ALL_COOKIES)['value'] - def get_cookie(self, name) -> Optional[dict]: + def get_cookie(self, name) -> Optional[ExecuteResponse]: """ Get a single cookie by name. Returns the cookie if found, None if not. @@ -706,6 +709,7 @@ def get_cookie(self, name) -> Optional[dict]: """ with contextlib.suppress(NoSuchCookieException): return self.execute(Command.GET_COOKIE, {"name": name})['value'] + return None def delete_cookie(self, name) -> None: """ @@ -880,7 +884,7 @@ def find_elements(self, by=By.ID, value=None) -> List[WebElement]: """ if isinstance(by, RelativeBy): _pkg = '.'.join(__name__.split('.')[:-1]) - raw_function = pkgutil.get_data(_pkg, 'findElements.js').decode('utf8') + raw_function = cast(bytes, pkgutil.get_data(_pkg, 'findElements.js')).decode('utf8') find_element_js = f"return ({raw_function}).apply(null, arguments);" return self.execute_script(find_element_js, by.to_dict()) @@ -1230,7 +1234,7 @@ def add_virtual_authenticator(self, options: VirtualAuthenticatorOptions) -> Non self._authenticator_id = self.execute(Command.ADD_VIRTUAL_AUTHENTICATOR, options.to_dict())['value'] @property - def virtual_authenticator_id(self) -> str: + def virtual_authenticator_id(self) -> Optional[str]: """ Returns the id of the virtual authenticator. """ diff --git a/py/selenium/webdriver/safari/service.py b/py/selenium/webdriver/safari/service.py index 7ec8c421a00fb..7ef45ccfeafef 100644 --- a/py/selenium/webdriver/safari/service.py +++ b/py/selenium/webdriver/safari/service.py @@ -52,9 +52,7 @@ def __init__(self, executable_path: str = DEFAULT_EXECUTABLE_PATH, self.service_args = service_args or [] self.quiet = quiet - log = PIPE - if quiet: - log = open(os.devnull, 'w') + log = open(os.devnull, 'w') if quiet else PIPE super().__init__(executable_path, port, log) def command_line_args(self): diff --git a/py/selenium/webdriver/support/relative_locator.py b/py/selenium/webdriver/support/relative_locator.py index 8363e5d0f79ff..5ce91c142e360 100644 --- a/py/selenium/webdriver/support/relative_locator.py +++ b/py/selenium/webdriver/support/relative_locator.py @@ -36,7 +36,7 @@ def with_tag_name(tag_name: str) -> "RelativeBy": """ if not tag_name: raise WebDriverException("tag_name can not be null") - return RelativeBy({"css selector": tag_name}) + return RelativeBy({By.CSS_SELECTOR: tag_name}) def locate_with(by: By, using: str) -> "RelativeBy": diff --git a/py/selenium/webdriver/support/wait.py b/py/selenium/webdriver/support/wait.py index 4952d4c8575c8..5905a48f70962 100644 --- a/py/selenium/webdriver/support/wait.py +++ b/py/selenium/webdriver/support/wait.py @@ -55,8 +55,11 @@ def __init__(self, driver, timeout: float, poll_frequency: float = POLL_FREQUENC if ignored_exceptions: try: exceptions.extend(iter(ignored_exceptions)) - except TypeError: # ignored_exceptions is not iterable - exceptions.append(ignored_exceptions) + except TypeError: + # ignored_exceptions is not iterable. This is a violation of + # the types declared on this method, but for compatability, we + # allow it: + exceptions.append(ignored_exceptions) # type: ignore self._ignored_exceptions = tuple(exceptions) def __repr__(self): From 0f38f31ef99b7539a6c8895da9bcc22f2ddfe407 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:00:56 +0100 Subject: [PATCH 17/53] Remove some unnecessary type: ignore instances. --- py/generate.py | 2 +- py/mypy.ini | 2 +- py/selenium/types.py | 5 ++--- py/selenium/webdriver/remote/errorhandler.py | 5 ++--- py/selenium/webdriver/support/color.py | 6 +++--- py/selenium/webdriver/support/wait.py | 17 +++++++---------- 6 files changed, 16 insertions(+), 21 deletions(-) diff --git a/py/generate.py b/py/generate.py index 7b7689dd85c4b..ac9c2b48cc5d4 100644 --- a/py/generate.py +++ b/py/generate.py @@ -38,7 +38,7 @@ from textwrap import dedent, indent as tw_indent import typing -import inflection # type: ignore +import inflection log_level = getattr(logging, os.environ.get('LOG_LEVEL', 'warning').upper()) diff --git a/py/mypy.ini b/py/mypy.ini index 9236d08520797..6334c0429df4d 100644 --- a/py/mypy.ini +++ b/py/mypy.ini @@ -1,4 +1,4 @@ [mypy] -files = selenium +files = selenium,generate.py,setup.py ignore_missing_imports = True warn_unused_ignores = True diff --git a/py/selenium/types.py b/py/selenium/types.py index 96d3830ba47ed..8b29ad9cafd54 100644 --- a/py/selenium/types.py +++ b/py/selenium/types.py @@ -17,8 +17,7 @@ """Selenium type definitions.""" -import typing +from typing import Iterable, Type, Union -AnyKey = typing.Union[str, int, float] -WaitExcTypes = typing.Iterable[typing.Type[Exception]] +AnyKey = Union[str, int, float] diff --git a/py/selenium/webdriver/remote/errorhandler.py b/py/selenium/webdriver/remote/errorhandler.py index 676b99afab53d..c18454d3914e6 100644 --- a/py/selenium/webdriver/remote/errorhandler.py +++ b/py/selenium/webdriver/remote/errorhandler.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -from typing import Any, Dict, Type +from typing import Any, Dict, Optional, Type from selenium.common.exceptions import (ElementClickInterceptedException, ElementNotInteractableException, @@ -114,7 +114,6 @@ def check_response(self, response: Dict[str, Any]) -> None: return value = None message = response.get("message", "") - screen: str = response.get("screen", "") stacktrace = None if isinstance(status, int): value_json = response.get('value', None) @@ -208,7 +207,7 @@ def check_response(self, response: Dict[str, Any]) -> None: if message == "" and 'message' in value: message = value['message'] - screen = None # type: ignore[assignment] + screen: Optional[str] = None if 'screen' in value: screen = value['screen'] diff --git a/py/selenium/webdriver/support/color.py b/py/selenium/webdriver/support/color.py index 429b1517fa0e2..8cfeba701c4f5 100644 --- a/py/selenium/webdriver/support/color.py +++ b/py/selenium/webdriver/support/color.py @@ -17,7 +17,7 @@ from __future__ import annotations import sys -from typing import Any, Sequence, TYPE_CHECKING +from typing import Any, Sequence, TYPE_CHECKING, Tuple if sys.version_info >= (3, 9): from re import Match @@ -87,8 +87,8 @@ def groups(self) -> Sequence[str]: elif m.match(RGBA_PATTERN, str_): return cls(*m.groups) elif m.match(RGBA_PCT_PATTERN, str_): - rgba = tuple( - [float(each) / 100 * 255 for each in m.groups[:3]] + [m.groups[3]]) # type: ignore + r, g, b = (float(each) / 100 * 255 for each in m.groups[:3]) + rgba: Tuple[float, float, float, str] = (r, g, b, m.groups[3]) return cls(*rgba) elif m.match(HEX_PATTERN, str_): rgb = tuple(int(each, 16) for each in m.groups) diff --git a/py/selenium/webdriver/support/wait.py b/py/selenium/webdriver/support/wait.py index 5905a48f70962..ce98b0902cb3a 100644 --- a/py/selenium/webdriver/support/wait.py +++ b/py/selenium/webdriver/support/wait.py @@ -16,18 +16,18 @@ # under the License. import time -import typing +from typing import Iterable, Optional, Tuple, Type, Union -from selenium.types import WaitExcTypes from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import TimeoutException POLL_FREQUENCY: float = 0.5 # How long to sleep in between calls to the method -IGNORED_EXCEPTIONS: typing.Tuple[typing.Type[Exception]] = (NoSuchElementException,) # default to be ignored. +IGNORED_EXCEPTIONS: Tuple[Type[Exception]] = (NoSuchElementException,) # default to be ignored. class WebDriverWait: - def __init__(self, driver, timeout: float, poll_frequency: float = POLL_FREQUENCY, ignored_exceptions: typing.Optional[WaitExcTypes] = None): + def __init__(self, driver, timeout: float, poll_frequency: float = POLL_FREQUENCY, + ignored_exceptions: Optional[Union[Iterable[Type[Exception]], Type[Exception]]] = None): """Constructor, takes a WebDriver instance and timeout in seconds. :Args: @@ -53,13 +53,10 @@ def __init__(self, driver, timeout: float, poll_frequency: float = POLL_FREQUENC self._poll = POLL_FREQUENCY exceptions = list(IGNORED_EXCEPTIONS) if ignored_exceptions: - try: + if isinstance(ignored_exceptions, Iterable): exceptions.extend(iter(ignored_exceptions)) - except TypeError: - # ignored_exceptions is not iterable. This is a violation of - # the types declared on this method, but for compatability, we - # allow it: - exceptions.append(ignored_exceptions) # type: ignore + else: + exceptions.append(ignored_exceptions) self._ignored_exceptions = tuple(exceptions) def __repr__(self): From 73778f1cdec0d1a0b264f6e69a5b644b97cb7d5e Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:03:25 +0100 Subject: [PATCH 18/53] Store alpha as a float when dealing with rgba values. It seemed like it being a string at all was maybe an oversight. --- py/selenium/webdriver/support/color.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/py/selenium/webdriver/support/color.py b/py/selenium/webdriver/support/color.py index 8cfeba701c4f5..a89c4f41ebdb4 100644 --- a/py/selenium/webdriver/support/color.py +++ b/py/selenium/webdriver/support/color.py @@ -87,8 +87,7 @@ def groups(self) -> Sequence[str]: elif m.match(RGBA_PATTERN, str_): return cls(*m.groups) elif m.match(RGBA_PCT_PATTERN, str_): - r, g, b = (float(each) / 100 * 255 for each in m.groups[:3]) - rgba: Tuple[float, float, float, str] = (r, g, b, m.groups[3]) + rgba = tuple(float(each) / 100 * 255 for each in m.groups[:3]) + (float(m.groups[3]),) return cls(*rgba) elif m.match(HEX_PATTERN, str_): rgb = tuple(int(each, 16) for each in m.groups) From 4b49f35112accd5bb1fc62c292d8ddfaed851703 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:05:58 +0100 Subject: [PATCH 19/53] Placate flake8. --- py/selenium/types.py | 2 +- py/selenium/webdriver/remote/webelement.py | 2 +- py/selenium/webdriver/support/color.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/py/selenium/types.py b/py/selenium/types.py index 8b29ad9cafd54..299aa64c48998 100644 --- a/py/selenium/types.py +++ b/py/selenium/types.py @@ -17,7 +17,7 @@ """Selenium type definitions.""" -from typing import Iterable, Type, Union +from typing import Union AnyKey = Union[str, int, float] diff --git a/py/selenium/webdriver/remote/webelement.py b/py/selenium/webdriver/remote/webelement.py index 9d5f2eca154c0..2d571dfa01f9e 100644 --- a/py/selenium/webdriver/remote/webelement.py +++ b/py/selenium/webdriver/remote/webelement.py @@ -24,7 +24,7 @@ import zipfile from abc import ABCMeta from io import BytesIO -from typing import Iterable, cast +from typing import Iterable from selenium.common.exceptions import WebDriverException, JavascriptException from selenium.types import AnyKey diff --git a/py/selenium/webdriver/support/color.py b/py/selenium/webdriver/support/color.py index a89c4f41ebdb4..ea781938a43c9 100644 --- a/py/selenium/webdriver/support/color.py +++ b/py/selenium/webdriver/support/color.py @@ -17,7 +17,7 @@ from __future__ import annotations import sys -from typing import Any, Sequence, TYPE_CHECKING, Tuple +from typing import Any, Sequence, TYPE_CHECKING if sys.version_info >= (3, 9): from re import Match From bc7b7b87368d9e98a3a1ff71cc1a6ef0ff1e5789 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:10:21 +0100 Subject: [PATCH 20/53] Fix an incorrect merge conflict resolution. --- py/selenium/webdriver/common/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/selenium/webdriver/common/log.py b/py/selenium/webdriver/common/log.py index 1df9a3bafe37d..a94219ee9c841 100644 --- a/py/selenium/webdriver/common/log.py +++ b/py/selenium/webdriver/common/log.py @@ -118,7 +118,7 @@ async def add_js_error_listener(self): @asynccontextmanager async def add_listener(self, event_type) -> AsyncIterator[Dict[str, Any]]: """ - Listens for certain events that are passed in. + Listen for certain events that are passed in. :Args: - event_type: The type of event that we want to look at. From b59e684d2123cc628fc4639ae0b6173056f79656 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:12:15 +0100 Subject: [PATCH 21/53] Put the _ignore_local_proxy assignment somewhere more like where it was. I messed up when resolving the merge conflict by turning this into an annotation without assignment, but it does need to be moved from ArgOptions to BaseOptions for annotations elsewhere to be correct. --- py/selenium/webdriver/common/options.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/py/selenium/webdriver/common/options.py b/py/selenium/webdriver/common/options.py index 09a3ac0a1ccac..58a1f894744b2 100644 --- a/py/selenium/webdriver/common/options.py +++ b/py/selenium/webdriver/common/options.py @@ -26,13 +26,12 @@ class BaseOptions(metaclass=ABCMeta): Base class for individual browser options """ - _ignore_local_proxy: bool - def __init__(self) -> None: super().__init__() self._caps = self.default_capabilities self.set_capability("pageLoadStrategy", "normal") self.mobile_options: Optional[dict] = None + self._ignore_local_proxy: bool = False @property def capabilities(self): From 0c936ff19eee42d52e5012f50a592d61fecaf76f Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:16:09 +0100 Subject: [PATCH 22/53] Complete this thought. --- py/selenium/webdriver/common/virtual_authenticator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/common/virtual_authenticator.py b/py/selenium/webdriver/common/virtual_authenticator.py index ba33fa049c34c..950b78bbb3192 100644 --- a/py/selenium/webdriver/common/virtual_authenticator.py +++ b/py/selenium/webdriver/common/virtual_authenticator.py @@ -40,8 +40,9 @@ class Transport(Enum): INTERNAL = "internal" -# to enable these types to be referenced in the class scope of -# VirtualAuthenticatorOptions, which shadows these enum's normal names: +# These aliases are necessary for these types to be referenced in the class +# scope of VirtualAuthenticatorOptions, which shadows the names "Protocol" and +# "Transport" as class attributes: _Protocol = Protocol _Transport = Transport From c75b9d1f7fafd4928a066bbd79dd01ed6a36c2ad Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:24:01 +0100 Subject: [PATCH 23/53] Reflect `android_package`'s nature as a required argument. It has a default, though, so it's not required for consumers. I have no idea why this method throws an AttributeError, of all things. --- py/selenium/webdriver/chrome/options.py | 2 +- py/selenium/webdriver/common/options.py | 2 +- py/selenium/webdriver/firefox/options.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/py/selenium/webdriver/chrome/options.py b/py/selenium/webdriver/chrome/options.py index c94a062d53c0b..80ef3064c6e84 100644 --- a/py/selenium/webdriver/chrome/options.py +++ b/py/selenium/webdriver/chrome/options.py @@ -27,7 +27,7 @@ def default_capabilities(self) -> dict: return DesiredCapabilities.CHROME.copy() def enable_mobile(self, - android_package: Optional[str] = "com.android.chrome", + android_package: str = "com.android.chrome", android_activity: Optional[str] = None, device_serial: Optional[str] = None ) -> None: diff --git a/py/selenium/webdriver/common/options.py b/py/selenium/webdriver/common/options.py index 58a1f894744b2..46f3d52f0eb0f 100644 --- a/py/selenium/webdriver/common/options.py +++ b/py/selenium/webdriver/common/options.py @@ -135,7 +135,7 @@ def timeouts(self, timeouts: dict) -> None: else: raise ValueError("Timeout keys can only be one of the following: implicit, pageLoad, script") - def enable_mobile(self, android_package: Optional[str] = None, android_activity: Optional[str] = None, + def enable_mobile(self, android_package: str = "", android_activity: Optional[str] = None, device_serial: Optional[str] = None) -> None: """ Enables mobile browser use for browsers that support it diff --git a/py/selenium/webdriver/firefox/options.py b/py/selenium/webdriver/firefox/options.py index 3f8a39fed3c50..1da1a401f8b98 100644 --- a/py/selenium/webdriver/firefox/options.py +++ b/py/selenium/webdriver/firefox/options.py @@ -128,7 +128,7 @@ def headless(self, value: bool) -> None: self._arguments.remove('-headless') def enable_mobile( - self, android_package: Optional[str] = "org.mozilla.firefox", android_activity: str = None, device_serial: str = None + self, android_package: str = "org.mozilla.firefox", android_activity: str = None, device_serial: str = None ): super().enable_mobile(android_package, android_activity, device_serial) From 3c0d215954efcfe8f36e9ffc3d8a0e46c3c0b372 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:26:19 +0100 Subject: [PATCH 24/53] Remove an errant newline that I added in a merge. --- py/selenium/webdriver/remote/webdriver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index efdd1cf180c46..4e43ad75dbd3f 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -19,7 +19,6 @@ import contextlib import copy import pkgutil - import types import warnings from abc import ABCMeta From 83ee29e7f0ebb6346d012ce7ecf9bf02adb916ca Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:41:24 +0100 Subject: [PATCH 25/53] Set up these type aliases properly. --- .../webdriver/common/virtual_authenticator.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/py/selenium/webdriver/common/virtual_authenticator.py b/py/selenium/webdriver/common/virtual_authenticator.py index 950b78bbb3192..e27d4cb75d9d1 100644 --- a/py/selenium/webdriver/common/virtual_authenticator.py +++ b/py/selenium/webdriver/common/virtual_authenticator.py @@ -40,17 +40,10 @@ class Transport(Enum): INTERNAL = "internal" -# These aliases are necessary for these types to be referenced in the class -# scope of VirtualAuthenticatorOptions, which shadows the names "Protocol" and -# "Transport" as class attributes: -_Protocol = Protocol -_Transport = Transport - - class VirtualAuthenticatorOptions: - Protocol = Protocol - Transport = Transport + Protocol: typing.TypeAlias = Protocol + Transport: typing.TypeAlias = Transport def __init__(self) -> None: """Constructor. Initialize VirtualAuthenticatorOptions object. @@ -76,7 +69,7 @@ def protocol(self) -> str: return self._protocol.value @protocol.setter - def protocol(self, protocol: _Protocol) -> None: + def protocol(self, protocol: Protocol) -> None: self._protocol = protocol @property @@ -84,7 +77,7 @@ def transport(self) -> str: return self._transport.value @transport.setter - def transport(self, transport: _Transport) -> None: + def transport(self, transport: Transport) -> None: self._transport = transport @property From 510074840bf8b0df22eb1ed0d22d09698925e5bf Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:54:38 +0100 Subject: [PATCH 26/53] Enable type checking for tests and fix the problems that brought up. It's opt-in (for now), just like the rest of the type checking, but I've found type-checking tests to be very useful, so I'd encourage it. --- py/mypy.ini | 2 +- .../webdriver/common/virtual_authenticator.py | 23 +++++++++++++------ .../webdriver/remote/remote_connection.py | 5 ++-- py/selenium/webdriver/remote/webdriver.py | 2 +- .../common/executing_javascript_tests.py | 5 ---- .../common/virtual_authenticator_tests.py | 2 +- .../selenium/webdriver/common/webserver.py | 6 ++--- .../remote/remote_connection_tests.py | 14 ----------- 8 files changed, 25 insertions(+), 34 deletions(-) diff --git a/py/mypy.ini b/py/mypy.ini index 6334c0429df4d..de2b38891809f 100644 --- a/py/mypy.ini +++ b/py/mypy.ini @@ -1,4 +1,4 @@ [mypy] -files = selenium,generate.py,setup.py +files = selenium,generate.py,setup.py,test ignore_missing_imports = True warn_unused_ignores = True diff --git a/py/selenium/webdriver/common/virtual_authenticator.py b/py/selenium/webdriver/common/virtual_authenticator.py index e27d4cb75d9d1..ece22e8a08558 100644 --- a/py/selenium/webdriver/common/virtual_authenticator.py +++ b/py/selenium/webdriver/common/virtual_authenticator.py @@ -40,6 +40,15 @@ class Transport(Enum): INTERNAL = "internal" +class VirtualAuthenticatorOptionsDict(typing.TypedDict): + protocol: str + transport: str + hasResidentKey: bool + hasUserVerification: bool + isUserConsenting: bool + isUserVerified: bool + + class VirtualAuthenticatorOptions: Protocol: typing.TypeAlias = Protocol @@ -65,16 +74,16 @@ def __init__(self) -> None: self._is_user_verified: bool = False @property - def protocol(self) -> str: - return self._protocol.value + def protocol(self) -> Protocol: + return self._protocol @protocol.setter def protocol(self, protocol: Protocol) -> None: self._protocol = protocol @property - def transport(self) -> str: - return self._transport.value + def transport(self) -> Transport: + return self._transport @transport.setter def transport(self, transport: Transport) -> None: @@ -112,10 +121,10 @@ def is_user_verified(self) -> bool: def is_user_verified(self, value: bool) -> None: self._is_user_verified = value - def to_dict(self) -> typing.Dict[str, typing.Any]: + def to_dict(self) -> VirtualAuthenticatorOptionsDict: return { - "protocol": self.protocol, - "transport": self.transport, + "protocol": self.protocol.value, + "transport": self.transport.value, "hasResidentKey": self.has_resident_key, "hasUserVerification": self.has_user_verification, "isUserConsenting": self.is_user_consenting, diff --git a/py/selenium/webdriver/remote/remote_connection.py b/py/selenium/webdriver/remote/remote_connection.py index 5f964e76873d9..d4e889e7520ad 100644 --- a/py/selenium/webdriver/remote/remote_connection.py +++ b/py/selenium/webdriver/remote/remote_connection.py @@ -18,7 +18,8 @@ import logging import socket import string -from typing import Optional +from typing import Dict, Optional +from urllib.parse import ParseResult import os import typing @@ -94,7 +95,7 @@ def set_certificate_bundle_path(cls, path): cls._ca_certs = path @classmethod - def get_remote_connection_headers(cls, parsed_url, keep_alive=False): + def get_remote_connection_headers(cls, parsed_url, keep_alive: bool = False) -> Dict[str, str]: """ Get headers for remote request. diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 4e43ad75dbd3f..0d46ac9284365 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -1230,7 +1230,7 @@ def add_virtual_authenticator(self, options: VirtualAuthenticatorOptions) -> Non """ Adds a virtual authenticator with the given options. """ - self._authenticator_id = self.execute(Command.ADD_VIRTUAL_AUTHENTICATOR, options.to_dict())['value'] + self._authenticator_id = self.execute(Command.ADD_VIRTUAL_AUTHENTICATOR, cast(dict, options.to_dict()))['value'] @property def virtual_authenticator_id(self) -> Optional[str]: diff --git a/py/test/selenium/webdriver/common/executing_javascript_tests.py b/py/test/selenium/webdriver/common/executing_javascript_tests.py index c29b8ea19e966..d28c5347aecf5 100644 --- a/py/test/selenium/webdriver/common/executing_javascript_tests.py +++ b/py/test/selenium/webdriver/common/executing_javascript_tests.py @@ -21,11 +21,6 @@ from selenium.webdriver.common.by import By from selenium.webdriver.remote.webelement import WebElement -try: - str = unicode -except NameError: - pass - def test_should_be_able_to_execute_simple_javascript_and_return_astring(driver, pages): pages.load("xhtmlTest.html") diff --git a/py/test/selenium/webdriver/common/virtual_authenticator_tests.py b/py/test/selenium/webdriver/common/virtual_authenticator_tests.py index 914bba375baed..0d7071cfacb2d 100644 --- a/py/test/selenium/webdriver/common/virtual_authenticator_tests.py +++ b/py/test/selenium/webdriver/common/virtual_authenticator_tests.py @@ -56,7 +56,7 @@ }]).then(arguments[arguments.length - 1]);''' -def create_rk_enabled_u2f_authenticator(driver) -> WebDriver: +def create_rk_enabled_u2f_authenticator(driver: WebDriver) -> WebDriver: options = VirtualAuthenticatorOptions() options.protocol = VirtualAuthenticatorOptions.Protocol.U2F diff --git a/py/test/selenium/webdriver/common/webserver.py b/py/test/selenium/webdriver/common/webserver.py index c2c7ac41b18e4..f3f6f3a3ac2dd 100644 --- a/py/test/selenium/webdriver/common/webserver.py +++ b/py/test/selenium/webdriver/common/webserver.py @@ -25,13 +25,13 @@ try: from urllib import request as urllib_request except ImportError: - import urllib as urllib_request + import urllib as urllib_request # type: ignore try: from http.server import BaseHTTPRequestHandler, HTTPServer from socketserver import ThreadingMixIn except ImportError: - from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer - from SocketServer import ThreadingMixIn + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer # type: ignore + from SocketServer import ThreadingMixIn # type: ignore def updir(): diff --git a/py/test/unit/selenium/webdriver/remote/remote_connection_tests.py b/py/test/unit/selenium/webdriver/remote/remote_connection_tests.py index 8bfdfab38231c..2b2cc7cd12bd8 100644 --- a/py/test/unit/selenium/webdriver/remote/remote_connection_tests.py +++ b/py/test/unit/selenium/webdriver/remote/remote_connection_tests.py @@ -166,20 +166,6 @@ def test_get_socks_proxy_when_set(mock_socks_proxy_settings): assert type(conn) == SOCKSProxyManager -class MockResponse: - code = 200 - headers = [] - - def read(self): - return b"{}" - - def close(self): - pass - - def getheader(self, *args, **kwargs): - pass - - @pytest.fixture(scope="function") def mock_proxy_settings_missing(monkeypatch): monkeypatch.delenv("HTTPS_PROXY", raising=False) From 5f87c652a89c2a9c04d9b71aacfe8c57471dc0ce Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:57:59 +0100 Subject: [PATCH 27/53] Placate flake8. --- py/selenium/webdriver/firefox/options.py | 2 +- py/selenium/webdriver/remote/remote_connection.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/firefox/options.py b/py/selenium/webdriver/firefox/options.py index 1da1a401f8b98..0e9a070653fe6 100644 --- a/py/selenium/webdriver/firefox/options.py +++ b/py/selenium/webdriver/firefox/options.py @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from typing import Optional, Union +from typing import Union import warnings from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.firefox.firefox_binary import FirefoxBinary diff --git a/py/selenium/webdriver/remote/remote_connection.py b/py/selenium/webdriver/remote/remote_connection.py index d4e889e7520ad..8b98ec1034bea 100644 --- a/py/selenium/webdriver/remote/remote_connection.py +++ b/py/selenium/webdriver/remote/remote_connection.py @@ -95,7 +95,7 @@ def set_certificate_bundle_path(cls, path): cls._ca_certs = path @classmethod - def get_remote_connection_headers(cls, parsed_url, keep_alive: bool = False) -> Dict[str, str]: + def get_remote_connection_headers(cls, parsed_url: ParseResult, keep_alive: bool = False) -> Dict[str, str]: """ Get headers for remote request. From 0f83235c5c5087416c4a79ee38dcf9ed2861eebb Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 17:59:49 +0100 Subject: [PATCH 28/53] Don't break on Python versions that don't have TypeAlias. --- py/selenium/webdriver/common/virtual_authenticator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/py/selenium/webdriver/common/virtual_authenticator.py b/py/selenium/webdriver/common/virtual_authenticator.py index ece22e8a08558..cedb9e7c966bf 100644 --- a/py/selenium/webdriver/common/virtual_authenticator.py +++ b/py/selenium/webdriver/common/virtual_authenticator.py @@ -15,6 +15,8 @@ # specific language governing permissions and limitations # under the License. +from __future__ import annotations + import functools from base64 import urlsafe_b64encode, urlsafe_b64decode From 576ea7b59a9221e97715b03cec024c687009cebe Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 18:05:35 +0100 Subject: [PATCH 29/53] Don't expect these attributes to magically chage their own type. I think maybe this Enum setup could have been better served with some constants and maybe a Literal, but I'm trying to trust what is here already. I am worried that there may be client code depending on being able to hand strings in here, in which case almost everything about the annotations on VirtualAuthenticatorOptions is wrong from the start. --- .../virtual_authenticator_options_tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/py/test/unit/selenium/webdriver/virtual_authenticator/virtual_authenticator_options_tests.py b/py/test/unit/selenium/webdriver/virtual_authenticator/virtual_authenticator_options_tests.py index d99427765b41e..3846c13e91336 100644 --- a/py/test/unit/selenium/webdriver/virtual_authenticator/virtual_authenticator_options_tests.py +++ b/py/test/unit/selenium/webdriver/virtual_authenticator/virtual_authenticator_options_tests.py @@ -30,22 +30,22 @@ def options(): def test_set_transport(options): options.transport = VirtualAuthenticatorOptions.Transport.USB - assert options.transport == VirtualAuthenticatorOptions.Transport.USB.value + assert options.transport == VirtualAuthenticatorOptions.Transport.USB def test_get_transport(options): options._transport = VirtualAuthenticatorOptions.Transport.NFC - assert options.transport == VirtualAuthenticatorOptions.Transport.NFC.value + assert options.transport == VirtualAuthenticatorOptions.Transport.NFC def test_set_protocol(options): options.protocol = VirtualAuthenticatorOptions.Protocol.U2F - assert options.protocol == VirtualAuthenticatorOptions.Protocol.U2F.value + assert options.protocol == VirtualAuthenticatorOptions.Protocol.U2F def test_get_protocol(options): options._protocol = VirtualAuthenticatorOptions.Protocol.CTAP2 - assert options.protocol == VirtualAuthenticatorOptions.Protocol.CTAP2.value + assert options.protocol == VirtualAuthenticatorOptions.Protocol.CTAP2 def test_set_has_resident_key(options): From 19c212539f46ccb720a7e7fc50b111dfcea183e3 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 18:19:18 +0100 Subject: [PATCH 30/53] Mention that mypy is an environment that exists. --- py/tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/tox.ini b/py/tox.ini index 32951267f7232..81e7cb8b470d6 100644 --- a/py/tox.ini +++ b/py/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = docs, flake8 +envlist = docs, flake8, mypy [testenv:docs] skip_install = true From 05b96a3a5d401dff4ec2637617d82f8f3fa995d6 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 18:29:35 +0100 Subject: [PATCH 31/53] Placate mypy. --- py/selenium/webdriver/chromium/webdriver.py | 2 +- py/selenium/webdriver/common/bidi/cdp.py | 1 + py/selenium/webdriver/remote/webdriver.py | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/py/selenium/webdriver/chromium/webdriver.py b/py/selenium/webdriver/chromium/webdriver.py index 0388c9b892f51..c57ff3243e49b 100644 --- a/py/selenium/webdriver/chromium/webdriver.py +++ b/py/selenium/webdriver/chromium/webdriver.py @@ -63,7 +63,7 @@ def __init__(self, browser_name, vendor_prefix, if service_log_path != DEFAULT_SERVICE_LOG_PATH: warnings.warn('service_log_path has been deprecated, please pass in a Service object', DeprecationWarning, stacklevel=2) - if keep_alive != DEFAULT_KEEP_ALIVE and type(self) == __class__: + if keep_alive != DEFAULT_KEEP_ALIVE and type(self) == ChromiumDriver: warnings.warn('keep_alive has been deprecated, please pass in a Service object', DeprecationWarning, stacklevel=2) else: diff --git a/py/selenium/webdriver/common/bidi/cdp.py b/py/selenium/webdriver/common/bidi/cdp.py index d5b9726d36e81..761a9bb56dab8 100644 --- a/py/selenium/webdriver/common/bidi/cdp.py +++ b/py/selenium/webdriver/common/bidi/cdp.py @@ -418,6 +418,7 @@ async def connect_session(self, target_id) -> 'CdpSession': Returns a new :class:`CdpSession` connected to the specified target. ''' global devtools + assert devtools is not None, "Devtools have not been imported. Call import_devtools(ver) first." session_id = await self.execute(devtools.target.attach_to_target( target_id, True)) session = CdpSession(self.ws, session_id, target_id) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 0d46ac9284365..8827896dba7dc 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -377,12 +377,12 @@ def start_session(self, capabilities: dict, browser_profile=None) -> None: if 'sessionId' not in response: response = response['value'] self.session_id = response['sessionId'] - self.caps = response.get('value') - # if capabilities is none we are probably speaking to - # a W3C endpoint - if not self.caps: - self.caps = response.get('capabilities') + self.caps = cast(Dict[str, Any], response.get( + 'value', + # if capabilities is none we are probably speaking to a W3C endpoint + response.get('capabilities') + )) def _wrap_value(self, value): if isinstance(value, dict): From 33a9fc85aa0c5d55a1a62baeddabefe1ad91ebfa Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 18:52:10 +0100 Subject: [PATCH 32/53] Annotate this chrome proxy test. --- py/test/selenium/webdriver/chrome/proxy_tests.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/py/test/selenium/webdriver/chrome/proxy_tests.py b/py/test/selenium/webdriver/chrome/proxy_tests.py index 49cec9c5124fb..da793f0047938 100644 --- a/py/test/selenium/webdriver/chrome/proxy_tests.py +++ b/py/test/selenium/webdriver/chrome/proxy_tests.py @@ -21,7 +21,7 @@ from selenium import webdriver -def test_bad_proxy_doesnt_interfere(): +def test_bad_proxy_doesnt_interfere() -> None: # these values should be ignored if ignore_local_proxy_environment_variables() is called. os.environ['https_proxy'] = 'bad' @@ -30,8 +30,7 @@ def test_bad_proxy_doesnt_interfere(): options.ignore_local_proxy_environment_variables() - chrome_kwargs = {'options': options} - driver = webdriver.Chrome(**chrome_kwargs) + driver = webdriver.Chrome(options=options) assert hasattr(driver, 'command_executor') assert hasattr(driver.command_executor, '_proxy_url') From 5d8f00a2de978471f1e3aec749f9c11e9e4b4fe4 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 18:57:21 +0100 Subject: [PATCH 33/53] Annotate common alert tests. --- py/conftest.py | 14 ++---- .../selenium/webdriver/common/alerts_tests.py | 49 ++++++++++--------- .../selenium/webdriver/common/webserver.py | 14 ++++++ 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/py/conftest.py b/py/conftest.py index 60735ec318a86..41ecc1c880bab 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -25,7 +25,9 @@ from selenium import webdriver from selenium.webdriver import DesiredCapabilities -from test.selenium.webdriver.common.webserver import SimpleWebServer +from selenium.webdriver.base import WebDriver +from test.selenium.webdriver.common.webserver import Pages, SimpleWebServer +from test.selenium.webdriver.common.network import get_lan_ip from test.selenium.webdriver.common.network import get_lan_ip from urllib.request import urlopen @@ -185,14 +187,8 @@ def pytest_exception_interact(node, call, report): @pytest.fixture -def pages(driver, webserver): - class Pages: - def url(self, name, localhost=False): - return webserver.where_is(name, localhost) - - def load(self, name): - driver.get(self.url(name)) - return Pages() +def pages(driver: WebDriver, webserver: SimpleWebServer) -> Pages: + return Pages(driver, webserver) @pytest.fixture(autouse=True, scope='session') diff --git a/py/test/selenium/webdriver/common/alerts_tests.py b/py/test/selenium/webdriver/common/alerts_tests.py index 6bc191488ca36..759c74979ef59 100644 --- a/py/test/selenium/webdriver/common/alerts_tests.py +++ b/py/test/selenium/webdriver/common/alerts_tests.py @@ -16,9 +16,11 @@ # under the License. import sys +from typing import Iterator import pytest +from selenium.webdriver.base import WebDriver from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait @@ -26,10 +28,11 @@ InvalidElementStateException, NoAlertPresentException, UnexpectedAlertPresentException) +from test.selenium.webdriver.common.webserver import Pages @pytest.fixture(autouse=True) -def close_alert(driver): +def close_alert(driver) -> Iterator[None]: yield try: driver.switch_to.alert.dismiss() @@ -37,7 +40,7 @@ def close_alert(driver): pass -def test_should_be_able_to_override_the_window_alert_method(driver, pages): +def test_should_be_able_to_override_the_window_alert_method(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.execute_script( "window.alert = function(msg) { document.getElementById('text').innerHTML = msg; }") @@ -54,7 +57,7 @@ def test_should_be_able_to_override_the_window_alert_method(driver, pages): raise e -def test_should_allow_users_to_accept_an_alert_manually(driver, pages): +def test_should_allow_users_to_accept_an_alert_manually(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(by=By.ID, value="alert").click() alert = _wait_for_alert(driver) @@ -63,7 +66,7 @@ def test_should_allow_users_to_accept_an_alert_manually(driver, pages): assert "Testing Alerts" == driver.title -def test_should_allow_users_to_accept_an_alert_with_no_text_manually(driver, pages): +def test_should_allow_users_to_accept_an_alert_with_no_text_manually(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(By.ID, "empty-alert").click() alert = _wait_for_alert(driver) @@ -73,7 +76,7 @@ def test_should_allow_users_to_accept_an_alert_with_no_text_manually(driver, pag assert "Testing Alerts" == driver.title -def test_should_get_text_of_alert_opened_in_set_timeout(driver, pages): +def test_should_get_text_of_alert_opened_in_set_timeout(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(By.ID, "slow-alert").click() @@ -88,7 +91,7 @@ def test_should_get_text_of_alert_opened_in_set_timeout(driver, pages): alert.accept() -def test_should_allow_users_to_dismiss_an_alert_manually(driver, pages): +def test_should_allow_users_to_dismiss_an_alert_manually(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(by=By.ID, value="alert").click() alert = _wait_for_alert(driver) @@ -97,7 +100,7 @@ def test_should_allow_users_to_dismiss_an_alert_manually(driver, pages): assert "Testing Alerts" == driver.title -def test_should_allow_auser_to_accept_aprompt(driver, pages): +def test_should_allow_auser_to_accept_aprompt(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(by=By.ID, value="prompt").click() alert = _wait_for_alert(driver) @@ -107,7 +110,7 @@ def test_should_allow_auser_to_accept_aprompt(driver, pages): assert "Testing Alerts" == driver.title -def test_should_allow_auser_to_dismiss_aprompt(driver, pages): +def test_should_allow_auser_to_dismiss_aprompt(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(by=By.ID, value="prompt").click() alert = _wait_for_alert(driver) @@ -117,7 +120,7 @@ def test_should_allow_auser_to_dismiss_aprompt(driver, pages): assert "Testing Alerts" == driver.title -def test_should_allow_auser_to_set_the_value_of_aprompt(driver, pages): +def test_should_allow_auser_to_set_the_value_of_aprompt(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(by=By.ID, value="prompt").click() alert = _wait_for_alert(driver) @@ -130,7 +133,7 @@ def test_should_allow_auser_to_set_the_value_of_aprompt(driver, pages): @pytest.mark.xfail_firefox @pytest.mark.xfail_remote -def test_setting_the_value_of_an_alert_throws(driver, pages): +def test_setting_the_value_of_an_alert_throws(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(By.ID, "alert").click() @@ -148,7 +151,7 @@ def test_setting_the_value_of_an_alert_throws(driver, pages): condition=sys.platform == 'darwin', reason='https://bugs.chromium.org/p/chromedriver/issues/detail?id=26', run=False) -def test_alert_should_not_allow_additional_commands_if_dimissed(driver, pages): +def test_alert_should_not_allow_additional_commands_if_dimissed(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(By.ID, "alert").click() @@ -162,7 +165,7 @@ def test_alert_should_not_allow_additional_commands_if_dimissed(driver, pages): @pytest.mark.xfail_firefox(reason='Fails on travis') @pytest.mark.xfail_remote(reason='Fails on travis') @pytest.mark.xfail_safari -def test_should_allow_users_to_accept_an_alert_in_aframe(driver, pages): +def test_should_allow_users_to_accept_an_alert_in_aframe(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.switch_to.frame(driver.find_element(By.NAME, "iframeWithAlert")) driver.find_element(By.ID, "alertInFrame").click() @@ -176,7 +179,7 @@ def test_should_allow_users_to_accept_an_alert_in_aframe(driver, pages): @pytest.mark.xfail_firefox(reason='Fails on travis') @pytest.mark.xfail_remote(reason='Fails on travis') @pytest.mark.xfail_safari -def test_should_allow_users_to_accept_an_alert_in_anested_frame(driver, pages): +def test_should_allow_users_to_accept_an_alert_in_anested_frame(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.switch_to.frame(driver.find_element(By.NAME, "iframeWithIframe")) driver.switch_to.frame(driver.find_element(By.NAME, "iframeWithAlert")) @@ -194,7 +197,7 @@ def test_should_throw_an_exception_if_an_alert_has_not_been_dealt_with_and_dismi # //TODO(David) Complete this test -def test_prompt_should_use_default_value_if_no_keys_sent(driver, pages): +def test_prompt_should_use_default_value_if_no_keys_sent(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(By.ID, "prompt-with-default").click() @@ -205,7 +208,7 @@ def test_prompt_should_use_default_value_if_no_keys_sent(driver, pages): assert "This is a default value" == txt -def test_prompt_should_have_null_value_if_dismissed(driver, pages): +def test_prompt_should_have_null_value_if_dismissed(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(By.ID, "prompt-with-default").click() alert = _wait_for_alert(driver) @@ -214,7 +217,7 @@ def test_prompt_should_have_null_value_if_dismissed(driver, pages): assert "null" == driver.find_element(By.ID, "text").text -def test_handles_two_alerts_from_one_interaction(driver, pages): +def test_handles_two_alerts_from_one_interaction(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(By.ID, "double-prompt").click() @@ -232,7 +235,7 @@ def test_handles_two_alerts_from_one_interaction(driver, pages): @pytest.mark.xfail_safari -def test_should_handle_alert_on_page_load(driver, pages): +def test_should_handle_alert_on_page_load(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(By.ID, "open-page-with-onload-alert").click() alert = _wait_for_alert(driver) @@ -241,7 +244,7 @@ def test_should_handle_alert_on_page_load(driver, pages): assert "onload" == value -def test_should_handle_alert_on_page_load_using_get(driver, pages): +def test_should_handle_alert_on_page_load_using_get(driver: WebDriver, pages: Pages) -> None: pages.load("pageWithOnLoad.html") alert = _wait_for_alert(driver) value = alert.text @@ -253,7 +256,7 @@ def test_should_handle_alert_on_page_load_using_get(driver, pages): @pytest.mark.xfail_chrome(reason='Non W3C conformant') @pytest.mark.xfail_chromiumedge(reason='Non W3C conformant') -def test_should_handle_alert_on_page_before_unload(driver, pages): +def test_should_handle_alert_on_page_before_unload(driver: WebDriver, pages: Pages) -> None: pages.load("pageWithOnBeforeUnloadMessage.html") element = driver.find_element(By.ID, "navigate") @@ -261,7 +264,7 @@ def test_should_handle_alert_on_page_before_unload(driver, pages): WebDriverWait(driver, 3).until(EC.title_is("Testing Alerts")) -def test_should_allow_the_user_to_get_the_text_of_an_alert(driver, pages): +def test_should_allow_the_user_to_get_the_text_of_an_alert(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(by=By.ID, value="alert").click() alert = _wait_for_alert(driver) @@ -270,7 +273,7 @@ def test_should_allow_the_user_to_get_the_text_of_an_alert(driver, pages): assert "cheese" == value -def test_should_allow_the_user_to_get_the_text_of_aprompt(driver, pages): +def test_should_allow_the_user_to_get_the_text_of_aprompt(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(By.ID, "prompt").click() @@ -281,7 +284,7 @@ def test_should_allow_the_user_to_get_the_text_of_aprompt(driver, pages): assert "Enter something" == value -def test_alert_should_not_allow_additional_commands_if_dismissed(driver, pages): +def test_alert_should_not_allow_additional_commands_if_dismissed(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(By.ID, "alert").click() @@ -297,7 +300,7 @@ def test_alert_should_not_allow_additional_commands_if_dismissed(driver, pages): @pytest.mark.xfail_remote( reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1279211') @pytest.mark.xfail_chrome -def test_unexpected_alert_present_exception_contains_alert_text(driver, pages): +def test_unexpected_alert_present_exception_contains_alert_text(driver: WebDriver, pages: Pages) -> None: pages.load("alerts.html") driver.find_element(by=By.ID, value="alert").click() alert = _wait_for_alert(driver) diff --git a/py/test/selenium/webdriver/common/webserver.py b/py/test/selenium/webdriver/common/webserver.py index f3f6f3a3ac2dd..2c883f6511d63 100644 --- a/py/test/selenium/webdriver/common/webserver.py +++ b/py/test/selenium/webdriver/common/webserver.py @@ -33,6 +33,8 @@ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer # type: ignore from SocketServer import ThreadingMixIn # type: ignore +from selenium.webdriver.base import WebDriver + def updir(): dirname = os.path.dirname @@ -169,6 +171,18 @@ def where_is(self, path, localhost=False) -> str: return f"http://{self.host}:{self.port}/{path}" +class Pages: + def __init__(self, driver: WebDriver, webserver: SimpleWebServer) -> None: + self._driver = driver + self._webserver = webserver + + def url(self, name: str, localhost: bool = False) -> str: + return self._webserver.where_is(name, localhost) + + def load(self, name: str): + return self._driver.get(self.url(name)) + + def main(argv=None): from optparse import OptionParser from time import sleep From 122ab171249653e39ca1e0872e9a7381e183f6cc Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 19:00:37 +0100 Subject: [PATCH 34/53] Don't hand enums into the executor. It can't serialize them. --- py/selenium/webdriver/remote/webdriver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 8827896dba7dc..1a75a084ee6aa 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -867,7 +867,7 @@ def find_element(self, by=By.ID, value=None) -> WebElement: value = '[name="%s"]' % value return self.execute(Command.FIND_ELEMENT, { - 'using': by, + 'using': by.value, 'value': value})['value'] def find_elements(self, by=By.ID, value=None) -> List[WebElement]: From 4f5d61b8089f52b4bda0c700ab01a75946d1b9f9 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 19:03:45 +0100 Subject: [PATCH 35/53] Remove an accidental duplicate import. --- py/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/py/conftest.py b/py/conftest.py index 41ecc1c880bab..b6c58eba18893 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -28,7 +28,6 @@ from selenium.webdriver.base import WebDriver from test.selenium.webdriver.common.webserver import Pages, SimpleWebServer from test.selenium.webdriver.common.network import get_lan_ip -from test.selenium.webdriver.common.network import get_lan_ip from urllib.request import urlopen From a718ef56225396991e868b49edc59d4e910ba5e3 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 19:21:33 +0100 Subject: [PATCH 36/53] Fix some WebDriver imports. --- py/conftest.py | 2 +- py/test/selenium/webdriver/common/alerts_tests.py | 2 +- py/test/selenium/webdriver/common/webserver.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/py/conftest.py b/py/conftest.py index b6c58eba18893..91e2eb7826506 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -25,7 +25,7 @@ from selenium import webdriver from selenium.webdriver import DesiredCapabilities -from selenium.webdriver.base import WebDriver +from selenium.webdriver.remote.webdriver import WebDriver from test.selenium.webdriver.common.webserver import Pages, SimpleWebServer from test.selenium.webdriver.common.network import get_lan_ip diff --git a/py/test/selenium/webdriver/common/alerts_tests.py b/py/test/selenium/webdriver/common/alerts_tests.py index 759c74979ef59..9d73fd2ffe152 100644 --- a/py/test/selenium/webdriver/common/alerts_tests.py +++ b/py/test/selenium/webdriver/common/alerts_tests.py @@ -20,7 +20,7 @@ import pytest -from selenium.webdriver.base import WebDriver +from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait diff --git a/py/test/selenium/webdriver/common/webserver.py b/py/test/selenium/webdriver/common/webserver.py index 2c883f6511d63..b034bf8e43327 100644 --- a/py/test/selenium/webdriver/common/webserver.py +++ b/py/test/selenium/webdriver/common/webserver.py @@ -33,7 +33,7 @@ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer # type: ignore from SocketServer import ThreadingMixIn # type: ignore -from selenium.webdriver.base import WebDriver +from selenium.webdriver.remote.webdriver import WebDriver def updir(): From 73223d37d0608de03ea1de406ca1de0cbfa63f1b Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 19:51:26 +0100 Subject: [PATCH 37/53] Fix another case of handing enums into an executor. --- py/selenium/webdriver/remote/webelement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/selenium/webdriver/remote/webelement.py b/py/selenium/webdriver/remote/webelement.py index 2d571dfa01f9e..0b1f8ff74972c 100644 --- a/py/selenium/webdriver/remote/webelement.py +++ b/py/selenium/webdriver/remote/webelement.py @@ -422,7 +422,7 @@ def find_element(self, by=By.ID, value=None) -> WebElement: value = '[name="%s"]' % value return self._execute(Command.FIND_CHILD_ELEMENT, - {"using": by, "value": value})['value'] + {"using": by.value, "value": value})['value'] def find_elements(self, by=By.ID, value=None) -> list[WebElement]: """ From ab2b7c7a8b219066cc2b8341a77e728e43290788 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 20:08:39 +0100 Subject: [PATCH 38/53] Fix two more handing-enums-to-executors problem. Wow, I really wasn't thorough with these at all. Sorry everyone. --- py/selenium/webdriver/remote/webdriver.py | 2 +- py/selenium/webdriver/remote/webelement.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 1a75a084ee6aa..45cecb8478051 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -900,7 +900,7 @@ def find_elements(self, by=By.ID, value=None) -> List[WebElement]: # Return empty list if driver returns null # See https://github.com/SeleniumHQ/selenium/issues/4555 return self.execute(Command.FIND_ELEMENTS, { - 'using': by, + 'using': by.value, 'value': value})['value'] or [] @property diff --git a/py/selenium/webdriver/remote/webelement.py b/py/selenium/webdriver/remote/webelement.py index 0b1f8ff74972c..00555ca558dd4 100644 --- a/py/selenium/webdriver/remote/webelement.py +++ b/py/selenium/webdriver/remote/webelement.py @@ -446,7 +446,7 @@ def find_elements(self, by=By.ID, value=None) -> list[WebElement]: value = '[name="%s"]' % value return self._execute(Command.FIND_CHILD_ELEMENTS, - {"using": by, "value": value})['value'] + {"using": by.value, "value": value})['value'] def __hash__(self): return int(md5_hash(self._id.encode('utf-8')).hexdigest(), 16) From 58a6fa81ef0f009c3a7ffed894f4d51cba63942f Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 20:45:06 +0100 Subject: [PATCH 39/53] Fix and annotate the construction of RelativeRoot dicts. --- py/selenium/webdriver/remote/webdriver.py | 9 +++-- .../webdriver/support/relative_locator.py | 33 ++++++++++++++----- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 45cecb8478051..954feeaea3f50 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -16,6 +16,9 @@ # under the License. """The WebDriver implementation.""" + +from __future__ import annotations + import contextlib import copy import pkgutil @@ -839,7 +842,7 @@ def timeouts(self, timeouts) -> None: """ _ = self.execute(Command.SET_TIMEOUTS, timeouts._to_json())['value'] - def find_element(self, by=By.ID, value=None) -> WebElement: + def find_element(self, by: By | RelativeBy = By.ID, value=None) -> WebElement: """ Find an element given a By strategy and locator. @@ -870,7 +873,7 @@ def find_element(self, by=By.ID, value=None) -> WebElement: 'using': by.value, 'value': value})['value'] - def find_elements(self, by=By.ID, value=None) -> List[WebElement]: + def find_elements(self, by: By | RelativeBy = By.ID, value=None) -> List[WebElement]: """ Find elements given a By strategy and locator. @@ -1267,7 +1270,7 @@ def get_credentials(self) -> List[Credential]: return [Credential.from_dict(credential) for credential in credential_data['value']] @required_virtual_authenticator - def remove_credential(self, credential_id: Union[str, bytearray]) -> None: + def remove_credential(self, credential_id: str | bytearray) -> None: """ Removes a credential from the authenticator. """ diff --git a/py/selenium/webdriver/support/relative_locator.py b/py/selenium/webdriver/support/relative_locator.py index 5ce91c142e360..790ae08df7511 100644 --- a/py/selenium/webdriver/support/relative_locator.py +++ b/py/selenium/webdriver/support/relative_locator.py @@ -15,8 +15,9 @@ # specific language governing permissions and limitations # under the License. +from __future__ import annotations -from typing import Dict, List, Union +from typing import Literal, Optional, TypedDict, Union from selenium.common.exceptions import WebDriverException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webelement import WebElement @@ -55,6 +56,20 @@ def locate_with(by: By, using: str) -> "RelativeBy": return RelativeBy({by: using}) +class Filter(TypedDict): + kind: Literal["above", "below", "left", "right", "near"] + args: list[WebElement | dict | int] + + +class RelativeByDictRelative(TypedDict): + root: Optional[dict[str, str]] + filters: list[Filter] + + +class RelativeByDict(TypedDict): + relative: RelativeByDictRelative + + class RelativeBy: """ Gives the opportunity to find elements based on their relative location @@ -71,7 +86,7 @@ class RelativeBy: assert "mid" in ids """ - def __init__(self, root: Dict[By, str] = None, filters: List = None): + def __init__(self, root: dict[By, str] = None, filters: list[Filter] = None) -> None: """ Creates a new RelativeBy object. It is preferred if you use the `locate_with` method as this signature could change. @@ -83,7 +98,7 @@ def __init__(self, root: Dict[By, str] = None, filters: List = None): self.root = root self.filters = filters or [] - def above(self, element_or_locator: Union[WebElement, Dict] = None) -> "RelativeBy": + def above(self, element_or_locator: Union[WebElement, dict] = None) -> "RelativeBy": """ Add a filter to look for elements above. :Args: @@ -95,7 +110,7 @@ def above(self, element_or_locator: Union[WebElement, Dict] = None) -> "Relative self.filters.append({"kind": "above", "args": [element_or_locator]}) return self - def below(self, element_or_locator: Union[WebElement, Dict] = None) -> "RelativeBy": + def below(self, element_or_locator: Union[WebElement, dict] = None) -> "RelativeBy": """ Add a filter to look for elements below. :Args: @@ -107,7 +122,7 @@ def below(self, element_or_locator: Union[WebElement, Dict] = None) -> "Relative self.filters.append({"kind": "below", "args": [element_or_locator]}) return self - def to_left_of(self, element_or_locator: Union[WebElement, Dict] = None) -> "RelativeBy": + def to_left_of(self, element_or_locator: Union[WebElement, dict] = None) -> "RelativeBy": """ Add a filter to look for elements to the left of. :Args: @@ -119,7 +134,7 @@ def to_left_of(self, element_or_locator: Union[WebElement, Dict] = None) -> "Rel self.filters.append({"kind": "left", "args": [element_or_locator]}) return self - def to_right_of(self, element_or_locator: Union[WebElement, Dict] = None) -> "RelativeBy": + def to_right_of(self, element_or_locator: Union[WebElement, dict] = None) -> "RelativeBy": """ Add a filter to look for elements right of. :Args: @@ -131,7 +146,7 @@ def to_right_of(self, element_or_locator: Union[WebElement, Dict] = None) -> "Re self.filters.append({"kind": "right", "args": [element_or_locator]}) return self - def near(self, element_or_locator_distance: Union[WebElement, Dict, int] = None) -> "RelativeBy": + def near(self, element_or_locator_distance: Union[WebElement, dict, int] = None) -> "RelativeBy": """ Add a filter to look for elements near. :Args: @@ -143,13 +158,13 @@ def near(self, element_or_locator_distance: Union[WebElement, Dict, int] = None) self.filters.append({"kind": "near", "args": [element_or_locator_distance]}) return self - def to_dict(self) -> Dict: + def to_dict(self) -> RelativeByDict: """ Create a dict that will be passed to the driver to start searching for the element """ return { 'relative': { - 'root': self.root, + 'root': {k.value: v for k, v in self.root.items()} if self.root is not None else None, 'filters': self.filters, } } From 00c801c09376f7e1a882098226ac4d5f2e4681a0 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 20:55:45 +0100 Subject: [PATCH 40/53] Remove one more type: ignore. --- py/selenium/webdriver/remote/errorhandler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/remote/errorhandler.py b/py/selenium/webdriver/remote/errorhandler.py index c18454d3914e6..945601155b88f 100644 --- a/py/selenium/webdriver/remote/errorhandler.py +++ b/py/selenium/webdriver/remote/errorhandler.py @@ -232,11 +232,11 @@ def check_response(self, response: Dict[str, Any]) -> None: stacktrace.append(msg) except TypeError: pass - if exception_class == UnexpectedAlertPresentException: + if issubclass(exception_class, UnexpectedAlertPresentException): alert_text = None if 'data' in value: alert_text = value['data'].get('text') elif 'alert' in value: alert_text = value['alert'].get('text') - raise exception_class(message, screen, stacktrace, alert_text) # type: ignore[call-arg] # mypy is not smart enough here + raise exception_class(message, screen, stacktrace, alert_text) raise exception_class(message, screen, stacktrace) From cfd676c30aa8d972523fb5cca03505819ba41c79 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 21:04:21 +0100 Subject: [PATCH 41/53] Complain manually about non-By by arguments. The tests require it, but these methods now naturally throw AttributeError in these cases instead. --- py/selenium/webdriver/remote/webelement.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/py/selenium/webdriver/remote/webelement.py b/py/selenium/webdriver/remote/webelement.py index 00555ca558dd4..777adb9046aae 100644 --- a/py/selenium/webdriver/remote/webelement.py +++ b/py/selenium/webdriver/remote/webelement.py @@ -411,6 +411,10 @@ def find_element(self, by=By.ID, value=None) -> WebElement: :rtype: WebElement """ + + if not isinstance(by, By): + raise WebDriverException("The by argument must be a By") + if by == By.ID: by = By.CSS_SELECTOR value = '[id="%s"]' % value @@ -435,6 +439,10 @@ def find_elements(self, by=By.ID, value=None) -> list[WebElement]: :rtype: list of WebElement """ + + if not isinstance(by, By): + raise WebDriverException("The by argument must be a By") + if by == By.ID: by = By.CSS_SELECTOR value = '[id="%s"]' % value From 9f4636d410b91edc1648ae191385d663306a0d7c Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 21:17:36 +0100 Subject: [PATCH 42/53] Fix the log constructed as part of event firing tests. Also, annotate these tests in the hopes of catching similar stuff. --- .../support/event_firing_webdriver_tests.py | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/py/test/selenium/webdriver/support/event_firing_webdriver_tests.py b/py/test/selenium/webdriver/support/event_firing_webdriver_tests.py index e9f5e7e1e4a84..c9f4d19895eee 100644 --- a/py/test/selenium/webdriver/support/event_firing_webdriver_tests.py +++ b/py/test/selenium/webdriver/support/event_firing_webdriver_tests.py @@ -17,6 +17,7 @@ from io import BytesIO +from typing import Iterator import pytest @@ -25,35 +26,37 @@ from selenium.webdriver.support.events import EventFiringWebDriver, AbstractEventListener from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.actions.action_builder import ActionBuilder +from selenium.webdriver.remote.webdriver import WebDriver +from test.selenium.common.webserver import Pages @pytest.fixture -def log(): +def log() -> Iterator[BytesIO]: log = BytesIO() yield log log.close() -def test_should_fire_navigation_events(driver, log, pages): +def test_should_fire_navigation_events(driver: WebDriver, log: BytesIO, pages: Pages) -> None: class EventListener(AbstractEventListener): - def before_navigate_to(self, url, driver): + def before_navigate_to(self, url: str, driver: WebDriver) -> None: log.write(("before_navigate_to %s" % url.split("/")[-1]).encode()) - def after_navigate_to(self, url, driver): + def after_navigate_to(self, url: str, driver: WebDriver) -> None: log.write(("after_navigate_to %s" % url.split("/")[-1]).encode()) - def before_navigate_back(self, driver): + def before_navigate_back(self, driver: WebDriver) -> None: log.write(b"before_navigate_back") - def after_navigate_back(self, driver): + def after_navigate_back(self, driver: WebDriver) -> None: log.write(b"after_navigate_back") - def before_navigate_forward(self, driver): + def before_navigate_forward(self, driver: WebDriver) -> None: log.write(b"before_navigate_forward") - def after_navigate_forward(self, driver): + def after_navigate_forward(self, driver: WebDriver) -> None: log.write(b"after_navigate_forward") ef_driver = EventFiringWebDriver(driver, EventListener()) @@ -77,7 +80,7 @@ def after_navigate_forward(self, driver): @pytest.mark.xfail_safari -def test_should_fire_click_event(driver, log, pages): +def test_should_fire_click_event(driver: WebDriver, log: BytesIO, pages: Pages) -> None: class EventListener(AbstractEventListener): @@ -95,7 +98,7 @@ def after_click(self, element, driver): assert b"before_click" + b"after_click" == log.getvalue() -def test_should_fire_change_value_event(driver, log, pages): +def test_should_fire_change_value_event(driver: WebDriver, log: BytesIO, pages: Pages) -> None: class EventListener(AbstractEventListener): @@ -122,15 +125,15 @@ def after_change_value_of(self, element, driver): b"after_change_value_of") == log.getvalue() -def test_should_fire_find_event(driver, log, pages): +def test_should_fire_find_event(driver: WebDriver, log: BytesIO, pages: Pages) -> None: class EventListener(AbstractEventListener): - def before_find(self, by, value, driver): - log.write((f"before_find by {by} {value}").encode()) + def before_find(self, by: By, value: str, driver: WebDriver): + log.write((f"before_find by {by.value} {value}").encode()) - def after_find(self, by, value, driver): - log.write((f"after_find by {by} {value}").encode()) + def after_find(self, by: By, value: str, driver: WebDriver): + log.write((f"after_find by {by.value} {value}").encode()) ef_driver = EventFiringWebDriver(driver, EventListener()) ef_driver.get(pages.url("simpleTest.html")) @@ -154,10 +157,10 @@ def after_find(self, by, value, driver): b"after_find by css selector frame#sixth") == log.getvalue() -def test_should_call_listener_when_an_exception_is_thrown(driver, log, pages): +def test_should_call_listener_when_an_exception_is_thrown(driver: WebDriver, log, pages: Pages) -> None: class EventListener(AbstractEventListener): - def on_exception(self, exception, driver): + def on_exception(self, exception: BaseException, driver: WebDriver) -> None: if isinstance(exception, NoSuchElementException): log.write(b"NoSuchElementException is thrown") @@ -168,7 +171,7 @@ def on_exception(self, exception, driver): assert b"NoSuchElementException is thrown" == log.getvalue() -def test_should_unwrap_element_args_when_calling_scripts(driver, log, pages): +def test_should_unwrap_element_args_when_calling_scripts(driver: WebDriver, log: BytesIO, pages: Pages) -> None: ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) ef_driver.get(pages.url("javascriptPage.html")) button = ef_driver.find_element(By.ID, "plainButton") @@ -178,7 +181,7 @@ def test_should_unwrap_element_args_when_calling_scripts(driver, log, pages): assert "plainButton" == value -def test_should_unwrap_element_args_when_switching_frames(driver, log, pages): +def test_should_unwrap_element_args_when_switching_frames(driver: WebDriver, log: BytesIO, pages: Pages) -> None: ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) ef_driver.get(pages.url("iframes.html")) frame = ef_driver.find_element(By.ID, "iframe1") @@ -186,7 +189,7 @@ def test_should_unwrap_element_args_when_switching_frames(driver, log, pages): assert "click me!" == ef_driver.find_element(By.ID, "imageButton").get_attribute("alt") -def test_should_be_able_to_access_wrapped_instance_from_event_calls(driver): +def test_should_be_able_to_access_wrapped_instance_from_event_calls(driver: WebDriver) -> None: class EventListener(AbstractEventListener): def before_navigate_to(url, d): @@ -197,7 +200,7 @@ def before_navigate_to(url, d): assert driver is wrapped_driver -def test_using_kwargs(driver, pages): +def test_using_kwargs(driver: WebDriver, pages: Pages) -> None: ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) ef_driver.get(pages.url("javascriptPage.html")) ef_driver.get_cookie(name="cookie_name") @@ -205,7 +208,7 @@ def test_using_kwargs(driver, pages): element.get_attribute(name="id") -def test_missing_attributes_raise_error(driver, pages): +def test_missing_attributes_raise_error(driver: WebDriver, pages: Pages) -> None: ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) with pytest.raises(AttributeError): @@ -215,10 +218,10 @@ def test_missing_attributes_raise_error(driver, pages): element = ef_driver.find_element(By.ID, "writableTextInput") with pytest.raises(AttributeError): - element.attribute_should_not_exist + element.attribute_should_not_exist # type: ignore [attr-defined] -def test_can_use_pointer_input_with_event_firing_webdriver(driver, pages): +def test_can_use_pointer_input_with_event_firing_webdriver(driver: WebDriver, pages: Pages) -> None: ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) pages.load("javascriptPage.html") to_click = ef_driver.find_element(By.ID, "clickField") @@ -232,7 +235,7 @@ def test_can_use_pointer_input_with_event_firing_webdriver(driver, pages): @pytest.mark.xfail_safari -def test_can_use_key_input_with_event_firing_webdriver(driver, pages): +def test_can_use_key_input_with_event_firing_webdriver(driver: WebDriver, pages: Pages) -> None: ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) pages.load("javascriptPage.html") ef_driver.find_element(By.ID, "keyUp").click() From 5fb2542ed9e6ea32640269926b5e89be618fc18e Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 21:19:37 +0100 Subject: [PATCH 43/53] Restore error codes to the remaining type: ignore instances. --- py/selenium/webdriver/remote/remote_connection.py | 2 +- py/test/selenium/webdriver/common/webserver.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/py/selenium/webdriver/remote/remote_connection.py b/py/selenium/webdriver/remote/remote_connection.py index 8b98ec1034bea..e9409c9c75ee0 100644 --- a/py/selenium/webdriver/remote/remote_connection.py +++ b/py/selenium/webdriver/remote/remote_connection.py @@ -47,7 +47,7 @@ class RemoteConnection: browser_name: Optional[str] = None # this is not part of socket's public API: - _timeout = socket._GLOBAL_DEFAULT_TIMEOUT # type: ignore + _timeout = socket._GLOBAL_DEFAULT_TIMEOUT # type: ignore [attr-defined] _ca_certs = certifi.where() @classmethod diff --git a/py/test/selenium/webdriver/common/webserver.py b/py/test/selenium/webdriver/common/webserver.py index b034bf8e43327..c233a18a93f00 100644 --- a/py/test/selenium/webdriver/common/webserver.py +++ b/py/test/selenium/webdriver/common/webserver.py @@ -25,13 +25,13 @@ try: from urllib import request as urllib_request except ImportError: - import urllib as urllib_request # type: ignore + import urllib as urllib_request # type: ignore [no-redef] try: from http.server import BaseHTTPRequestHandler, HTTPServer from socketserver import ThreadingMixIn except ImportError: - from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer # type: ignore - from SocketServer import ThreadingMixIn # type: ignore + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer # type: ignore [no-redef] + from SocketServer import ThreadingMixIn # type: ignore [no-redef] from selenium.webdriver.remote.webdriver import WebDriver From 2b5fd5f9ddc94f6be3689452d7c361589b6d729d Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 21:26:25 +0100 Subject: [PATCH 44/53] Fix how RelativeBy problems get surfaced. --- py/selenium/webdriver/remote/webdriver.py | 2 +- py/selenium/webdriver/support/relative_locator.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 954feeaea3f50..1c44e5ab93a13 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -856,7 +856,7 @@ def find_element(self, by: By | RelativeBy = By.ID, value=None) -> WebElement: if isinstance(by, RelativeBy): elements = self.find_elements(by=by, value=value) if not elements: - raise NoSuchElementException(f"Cannot locate relative element with: {by.root}") + raise NoSuchElementException(f"Cannot locate relative element with: {by}") return elements[0] if by == By.ID: diff --git a/py/selenium/webdriver/support/relative_locator.py b/py/selenium/webdriver/support/relative_locator.py index 790ae08df7511..a5dd910945866 100644 --- a/py/selenium/webdriver/support/relative_locator.py +++ b/py/selenium/webdriver/support/relative_locator.py @@ -98,6 +98,9 @@ def __init__(self, root: dict[By, str] = None, filters: list[Filter] = None) -> self.root = root self.filters = filters or [] + def __str__(self) -> str: + return str(self.to_dict()["relative"]["root"]) + def above(self, element_or_locator: Union[WebElement, dict] = None) -> "RelativeBy": """ Add a filter to look for elements above. From 4029f449b9ef09751b8b9a7452dee955867df580 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 21:28:52 +0100 Subject: [PATCH 45/53] Fix a bad import. --- .../selenium/webdriver/support/event_firing_webdriver_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/test/selenium/webdriver/support/event_firing_webdriver_tests.py b/py/test/selenium/webdriver/support/event_firing_webdriver_tests.py index c9f4d19895eee..4307ce472fde2 100644 --- a/py/test/selenium/webdriver/support/event_firing_webdriver_tests.py +++ b/py/test/selenium/webdriver/support/event_firing_webdriver_tests.py @@ -27,7 +27,7 @@ from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.actions.action_builder import ActionBuilder from selenium.webdriver.remote.webdriver import WebDriver -from test.selenium.common.webserver import Pages +from test.selenium.webdriver.common.webserver import Pages @pytest.fixture From b901712d2dcb09a36071282dd6aae6265ff59bc3 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 27 Aug 2022 21:40:04 +0100 Subject: [PATCH 46/53] Fix some more enums-to-executors stuff in the shadowroot module. --- py/selenium/webdriver/remote/shadowroot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/remote/shadowroot.py b/py/selenium/webdriver/remote/shadowroot.py index 02432081322c5..7df4f0c69241e 100644 --- a/py/selenium/webdriver/remote/shadowroot.py +++ b/py/selenium/webdriver/remote/shadowroot.py @@ -52,7 +52,7 @@ def find_element(self, by: By = By.ID, value: str = None): value = '[name="%s"]' % value return self._execute( - Command.FIND_ELEMENT_FROM_SHADOW_ROOT, {"using": by, "value": value} + Command.FIND_ELEMENT_FROM_SHADOW_ROOT, {"using": by.value, "value": value} )["value"] def find_elements(self, by: By = By.ID, value: str = None): @@ -67,7 +67,7 @@ def find_elements(self, by: By = By.ID, value: str = None): value = '[name="%s"]' % value return self._execute( - Command.FIND_ELEMENTS_FROM_SHADOW_ROOT, {"using": by, "value": value} + Command.FIND_ELEMENTS_FROM_SHADOW_ROOT, {"using": by.value, "value": value} )["value"] # Private Methods From 840c5b34f13852209986fcfab2f1bbf481ca7ef5 Mon Sep 17 00:00:00 2001 From: colons Date: Sun, 28 Aug 2022 08:35:01 +0100 Subject: [PATCH 47/53] Annotate the by arguments in WebElement's element finding methods. --- py/selenium/webdriver/remote/webelement.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/remote/webelement.py b/py/selenium/webdriver/remote/webelement.py index 777adb9046aae..e7f28b63024eb 100644 --- a/py/selenium/webdriver/remote/webelement.py +++ b/py/selenium/webdriver/remote/webelement.py @@ -400,7 +400,7 @@ def _execute(self, command, params=None): params['id'] = self._id return self._parent.execute(command, params) - def find_element(self, by=By.ID, value=None) -> WebElement: + def find_element(self, by: By = By.ID, value=None) -> WebElement: """ Find an element given a By strategy and locator. @@ -428,7 +428,7 @@ def find_element(self, by=By.ID, value=None) -> WebElement: return self._execute(Command.FIND_CHILD_ELEMENT, {"using": by.value, "value": value})['value'] - def find_elements(self, by=By.ID, value=None) -> list[WebElement]: + def find_elements(self, by: By = By.ID, value=None) -> list[WebElement]: """ Find elements given a By strategy and locator. From 7b8a87e96d9c8c3bd94e0161f84db970f0c236c6 Mon Sep 17 00:00:00 2001 From: colons Date: Mon, 29 Aug 2022 19:33:40 +0100 Subject: [PATCH 48/53] Accept strings as `by` arguments, to avoid breaking compatibility. --- py/selenium/webdriver/common/by.py | 21 +++++++++++++++++++ py/selenium/webdriver/remote/shadowroot.py | 10 +++++++-- py/selenium/webdriver/remote/webdriver.py | 8 +++++-- py/selenium/webdriver/remote/webelement.py | 12 +++++------ .../webdriver/support/relative_locator.py | 6 +++++- 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/py/selenium/webdriver/common/by.py b/py/selenium/webdriver/common/by.py index b6289658443d2..5848b8bc1e190 100644 --- a/py/selenium/webdriver/common/by.py +++ b/py/selenium/webdriver/common/by.py @@ -19,8 +19,12 @@ The By implementation. """ +from __future__ import annotations + from enum import Enum +from selenium.common.exceptions import WebDriverException + class By(Enum): """ @@ -35,3 +39,20 @@ class By(Enum): TAG_NAME = "tag name" CLASS_NAME = "class name" CSS_SELECTOR = "css selector" + + @classmethod + def from_str(cls, by_str: str) -> By: + """ + Take the string representation of a locator strategy ("id", "css + selector", etc.), and return the appropriate By enum value for it. + Throw a WebDriverException if no such By type exists. + """ + + for value in cls.__members__.values(): + if value.value == by_str: + return value + + raise WebDriverException( + f"{by_str!r} is not a valid locator strategy. Use a member of the By enum directly or one of: " + f"{', '.join(repr(v.value) for v in cls.__members__.values())}" + ) diff --git a/py/selenium/webdriver/remote/shadowroot.py b/py/selenium/webdriver/remote/shadowroot.py index 7df4f0c69241e..15aae84228d54 100644 --- a/py/selenium/webdriver/remote/shadowroot.py +++ b/py/selenium/webdriver/remote/shadowroot.py @@ -40,7 +40,10 @@ def __repr__(self) -> str: type(self), self.session.session_id, self._id ) - def find_element(self, by: By = By.ID, value: str = None): + def find_element(self, by: By | str = By.ID, value: str = None): + if isinstance(by, str): + by = By.from_str(by) + if by == By.ID: by = By.CSS_SELECTOR value = '[id="%s"]' % value @@ -55,7 +58,10 @@ def find_element(self, by: By = By.ID, value: str = None): Command.FIND_ELEMENT_FROM_SHADOW_ROOT, {"using": by.value, "value": value} )["value"] - def find_elements(self, by: By = By.ID, value: str = None): + def find_elements(self, by: By | str = By.ID, value: str = None): + if isinstance(by, str): + by = By.from_str(by) + if by == By.ID: by = By.CSS_SELECTOR value = '[id="%s"]' % value diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 1c44e5ab93a13..6fe84259ee2e6 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -842,7 +842,7 @@ def timeouts(self, timeouts) -> None: """ _ = self.execute(Command.SET_TIMEOUTS, timeouts._to_json())['value'] - def find_element(self, by: By | RelativeBy = By.ID, value=None) -> WebElement: + def find_element(self, by: By | RelativeBy | str = By.ID, value=None) -> WebElement: """ Find an element given a By strategy and locator. @@ -858,6 +858,8 @@ def find_element(self, by: By | RelativeBy = By.ID, value=None) -> WebElement: if not elements: raise NoSuchElementException(f"Cannot locate relative element with: {by}") return elements[0] + elif isinstance(by, str): + by = By.from_str(by) if by == By.ID: by = By.CSS_SELECTOR @@ -873,7 +875,7 @@ def find_element(self, by: By | RelativeBy = By.ID, value=None) -> WebElement: 'using': by.value, 'value': value})['value'] - def find_elements(self, by: By | RelativeBy = By.ID, value=None) -> List[WebElement]: + def find_elements(self, by: By | RelativeBy | str = By.ID, value=None) -> List[WebElement]: """ Find elements given a By strategy and locator. @@ -889,6 +891,8 @@ def find_elements(self, by: By | RelativeBy = By.ID, value=None) -> List[WebElem raw_function = cast(bytes, pkgutil.get_data(_pkg, 'findElements.js')).decode('utf8') find_element_js = f"return ({raw_function}).apply(null, arguments);" return self.execute_script(find_element_js, by.to_dict()) + elif isinstance(by, str): + by = By.from_str(by) if by == By.ID: by = By.CSS_SELECTOR diff --git a/py/selenium/webdriver/remote/webelement.py b/py/selenium/webdriver/remote/webelement.py index e7f28b63024eb..dcfda60fd4852 100644 --- a/py/selenium/webdriver/remote/webelement.py +++ b/py/selenium/webdriver/remote/webelement.py @@ -400,7 +400,7 @@ def _execute(self, command, params=None): params['id'] = self._id return self._parent.execute(command, params) - def find_element(self, by: By = By.ID, value=None) -> WebElement: + def find_element(self, by: By | str = By.ID, value=None) -> WebElement: """ Find an element given a By strategy and locator. @@ -412,8 +412,8 @@ def find_element(self, by: By = By.ID, value=None) -> WebElement: :rtype: WebElement """ - if not isinstance(by, By): - raise WebDriverException("The by argument must be a By") + if isinstance(by, str): + by = By.from_str(by) if by == By.ID: by = By.CSS_SELECTOR @@ -428,7 +428,7 @@ def find_element(self, by: By = By.ID, value=None) -> WebElement: return self._execute(Command.FIND_CHILD_ELEMENT, {"using": by.value, "value": value})['value'] - def find_elements(self, by: By = By.ID, value=None) -> list[WebElement]: + def find_elements(self, by: By | str = By.ID, value=None) -> list[WebElement]: """ Find elements given a By strategy and locator. @@ -440,8 +440,8 @@ def find_elements(self, by: By = By.ID, value=None) -> list[WebElement]: :rtype: list of WebElement """ - if not isinstance(by, By): - raise WebDriverException("The by argument must be a By") + if isinstance(by, str): + by = By.from_str(by) if by == By.ID: by = By.CSS_SELECTOR diff --git a/py/selenium/webdriver/support/relative_locator.py b/py/selenium/webdriver/support/relative_locator.py index a5dd910945866..df3602db46651 100644 --- a/py/selenium/webdriver/support/relative_locator.py +++ b/py/selenium/webdriver/support/relative_locator.py @@ -40,7 +40,7 @@ def with_tag_name(tag_name: str) -> "RelativeBy": return RelativeBy({By.CSS_SELECTOR: tag_name}) -def locate_with(by: By, using: str) -> "RelativeBy": +def locate_with(by: By | str, using: str) -> "RelativeBy": """ Start searching for relative objects your search criteria with By. @@ -53,6 +53,10 @@ def locate_with(by: By, using: str) -> "RelativeBy": """ assert by is not None, "Please pass in a by argument" assert using is not None, "Please pass in a using argument" + + if isinstance(by, str): + by = By.from_str(by) + return RelativeBy({by: using}) From b9843ed4e3d9bfc5d20d825fae8e40aca1de13e2 Mon Sep 17 00:00:00 2001 From: colons Date: Mon, 29 Aug 2022 19:35:42 +0100 Subject: [PATCH 49/53] Make sure this string-based element locating continues to work. --- .../selenium/webdriver/common/children_finding_tests.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/py/test/selenium/webdriver/common/children_finding_tests.py b/py/test/selenium/webdriver/common/children_finding_tests.py index 6f667b2bd14e8..fca3a967652df 100755 --- a/py/test/selenium/webdriver/common/children_finding_tests.py +++ b/py/test/selenium/webdriver/common/children_finding_tests.py @@ -147,6 +147,10 @@ def test_should_find_element_by_tag_name(driver, pages): element = parent.find_element(By.TAG_NAME, "a") assert "link1" == element.get_attribute("name") + parent = driver.find_element("name", "div1") + element = parent.find_element("tag name", "a") + assert "link1" == element.get_attribute("name") + def test_should_find_elements_by_tag_name(driver, pages): pages.load("nestedElements.html") @@ -154,6 +158,10 @@ def test_should_find_elements_by_tag_name(driver, pages): elements = parent.find_elements(By.TAG_NAME, "a") assert 2 == len(elements) + parent = driver.find_element("name", "div1") + elements = parent.find_elements("tag name", "a") + assert 2 == len(elements) + def test_should_be_able_to_find_an_element_by_css_selector(driver, pages): pages.load("nestedElements.html") From aa7809c06be962167b0013cc553fa831efdcc786 Mon Sep 17 00:00:00 2001 From: colons Date: Tue, 30 Aug 2022 15:47:19 +0100 Subject: [PATCH 50/53] Split testing of `by: str`-style calling into separate tests. It was weird having a few tests here that seemed to do more for no reason. --- .../webdriver/common/children_finding_tests.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/py/test/selenium/webdriver/common/children_finding_tests.py b/py/test/selenium/webdriver/common/children_finding_tests.py index fca3a967652df..d171d3e2fa3e9 100755 --- a/py/test/selenium/webdriver/common/children_finding_tests.py +++ b/py/test/selenium/webdriver/common/children_finding_tests.py @@ -147,10 +147,6 @@ def test_should_find_element_by_tag_name(driver, pages): element = parent.find_element(By.TAG_NAME, "a") assert "link1" == element.get_attribute("name") - parent = driver.find_element("name", "div1") - element = parent.find_element("tag name", "a") - assert "link1" == element.get_attribute("name") - def test_should_find_elements_by_tag_name(driver, pages): pages.load("nestedElements.html") @@ -158,6 +154,16 @@ def test_should_find_elements_by_tag_name(driver, pages): elements = parent.find_elements(By.TAG_NAME, "a") assert 2 == len(elements) + +def test_should_find_element_by_tag_name_with_string_by(driver, pages): + pages.load("nestedElements.html") + parent = driver.find_element("name", "div1") + element = parent.find_element("tag name", "a") + assert "link1" == element.get_attribute("name") + + +def test_should_find_elements_by_tag_name_with_string_by(driver, pages): + pages.load("nestedElements.html") parent = driver.find_element("name", "div1") elements = parent.find_elements("tag name", "a") assert 2 == len(elements) From dc0c7e45700c9c7a57ee3629d1361456bc6681e3 Mon Sep 17 00:00:00 2001 From: colons Date: Tue, 30 Aug 2022 15:51:19 +0100 Subject: [PATCH 51/53] Cover the `by: str` methods for shadowroot, too. --- .../webdriver/common/web_components_tests.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/py/test/selenium/webdriver/common/web_components_tests.py b/py/test/selenium/webdriver/common/web_components_tests.py index 0e91aa16e066b..f369d002615f6 100644 --- a/py/test/selenium/webdriver/common/web_components_tests.py +++ b/py/test/selenium/webdriver/common/web_components_tests.py @@ -17,7 +17,7 @@ import pytest -from selenium.common.exceptions import NoSuchShadowRootException +from selenium.common.exceptions import NoSuchShadowRootException, WebDriverException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.remote.shadowroot import ShadowRoot @@ -75,3 +75,27 @@ def test_can_find_elements_in_a_shadow_root(driver, pages): assert len(elements) == 1 assert isinstance(elements[0], WebElement) + + +@pytest.mark.xfail_safari +@pytest.mark.xfail_firefox +@pytest.mark.xfail_remote +def test_can_find_elements_in_a_shadow_root_using_a_string_locator(driver, pages): + pages.load("webComponents.html") + custom_element = driver.find_element("css selector", "custom-checkbox-element") + shadow_root = custom_element.shadow_root + elements = shadow_root.find_elements("css selector", "input") + assert len(elements) == 1 + + assert isinstance(elements[0], WebElement) + + +@pytest.mark.xfail_safari +@pytest.mark.xfail_firefox +@pytest.mark.xfail_remote +def test_finding_element_in_a_shadowroot_fails_with_invalid_string_locator(driver, pages): + pages.load("webComponents.html") + custom_element = driver.find_element("css selector", "custom-checkbox-element") + shadow_root = custom_element.shadow_root + with pytest.raises(WebDriverException): + shadow_root.find_elements("not a real locator strategy", "value") From d281d18cc7b48e6055e0170fa5649d6e7f248354 Mon Sep 17 00:00:00 2001 From: colons Date: Tue, 30 Aug 2022 16:04:58 +0100 Subject: [PATCH 52/53] Ask Python to stop treating this annotation as code. --- py/selenium/webdriver/remote/shadowroot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/py/selenium/webdriver/remote/shadowroot.py b/py/selenium/webdriver/remote/shadowroot.py index 15aae84228d54..8eeb2dd7d29e4 100644 --- a/py/selenium/webdriver/remote/shadowroot.py +++ b/py/selenium/webdriver/remote/shadowroot.py @@ -15,6 +15,8 @@ # specific language governing permissions and limitations # under the License. +from __future__ import annotations + from hashlib import md5 as md5_hash from .command import Command From 4f86baf67ced22a630435631682faf7e3142d97c Mon Sep 17 00:00:00 2001 From: colons Date: Tue, 30 Aug 2022 16:35:49 +0100 Subject: [PATCH 53/53] Make the mypy CI job fail when mypy complains. --- .github/workflows/ci-python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index 9e39a0d0b9961..a1df36c8d9fa9 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -77,7 +77,7 @@ jobs: pip install tox==2.4.1 - name: Test with tox run: | - tox -c py/tox.ini -- --cobertura-xml-report ci || true + tox -c py/tox.ini -- --cobertura-xml-report ci bash <(curl -s https://codecov.io/bash) -f py/ci/cobertura.xml env: TOXENV: mypy