diff --git a/README.md b/README.md index aeba4c15..e28ed7ea 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,37 @@ For example, some changes in the Selenium binding could break the Appium client. > to keep compatible version combinations. +### Quick migration guide from v4 to v5 +- This change affects only for users who specify `keep_alive`, `direct_connection` and `strict_ssl` arguments for `webdriver.Remote`: + - Please use `AppiumClientConfig` as `client_config` argument similar to how it is specified below: + ```python + SERVER_URL_BASE = 'http://127.0.0.1:4723' + # before + driver = webdriver.Remote( + SERVER_URL_BASE, + options=UiAutomator2Options().load_capabilities(desired_caps), + direct_connection=True, + keep_alive=False, + strict_ssl=False + ) + + # after + from appium.webdriver.client_config import AppiumClientConfig + client_config = AppiumClientConfig( + remote_server_addr=SERVER_URL_BASE, + direct_connection=True, + keep_alive=False, + ignore_certificates=True, + ) + driver = webdriver.Remote( + options=UiAutomator2Options().load_capabilities(desired_caps), + client_config=client_config + ) + ``` + - Note that you can keep using `webdriver.Remote(url, options=options, client_config=client_config)` format as well. + In such case the `remote_server_addr` argument of `AppiumClientConfig` constructor would have priority over the `url` argument of `webdriver.Remote` constructor. +- Use `http://127.0.0.1:4723` as the default server url instead of `http://127.0.0.1:4444/wd/hub` + ### Quick migration guide from v3 to v4 - Removal - `MultiAction` and `TouchAction` are removed. Please use W3C WebDriver actions or `mobile:` extensions @@ -274,6 +305,7 @@ from appium import webdriver # If you use an older client then switch to desired_capabilities # instead: https://github.com/appium/python-client/pull/720 from appium.options.ios import XCUITestOptions +from appium.webdriver.client_config import AppiumClientConfig # load_capabilities API could be used to # load options mapping stored in a dictionary @@ -283,11 +315,16 @@ options = XCUITestOptions().load_capabilities({ 'app': '/full/path/to/app/UICatalog.app.zip', }) +client_config = AppiumClientConfig( + remote_server_addr='http://127.0.0.1:4723', + direct_connection=True +) + driver = webdriver.Remote( # Appium1 points to http://127.0.0.1:4723/wd/hub by default 'http://127.0.0.1:4723', options=options, - direct_connection=True + client_config=client_config ) ``` diff --git a/appium/webdriver/client_config.py b/appium/webdriver/client_config.py new file mode 100644 index 00000000..ae2c0e29 --- /dev/null +++ b/appium/webdriver/client_config.py @@ -0,0 +1,38 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from selenium.webdriver.remote.client_config import ClientConfig + + +class AppiumClientConfig(ClientConfig): + """ClientConfig class for Appium Python client. + This class inherits selenium.webdriver.remote.client_config.ClientConfig. + """ + + def __init__(self, remote_server_addr: str, *args, **kwargs): + """ + Please refer to selenium.webdriver.remote.client_config.ClientConfig documentation + about available arguments. Only 'direct_connection' below is AppiumClientConfig + specific argument. + + Args: + direct_connection: If enables [directConnect](https://github.com/appium/python-client?tab=readme-ov-file#direct-connect-urls) + feature. + """ + self._direct_connection = kwargs.pop('direct_connection', False) + super().__init__(remote_server_addr, *args, **kwargs) + + @property + def direct_connection(self) -> bool: + """Return if [directConnect](https://github.com/appium/python-client?tab=readme-ov-file#direct-connect-urls) + is enabled.""" + return self._direct_connection diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 7b93e3b0..9886c197 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -22,7 +22,6 @@ WebDriverException, ) from selenium.webdriver.common.by import By -from selenium.webdriver.remote.client_config import ClientConfig from selenium.webdriver.remote.command import Command as RemoteCommand from selenium.webdriver.remote.remote_connection import RemoteConnection from typing_extensions import Self @@ -32,6 +31,7 @@ from appium.webdriver.common.appiumby import AppiumBy from .appium_connection import AppiumConnection +from .client_config import AppiumClientConfig from .errorhandler import MobileErrorHandler from .extensions.action_helpers import ActionHelpers from .extensions.android.activities import Activities @@ -174,6 +174,27 @@ def add_command(self) -> Tuple[str, str]: raise NotImplementedError() +def _get_remote_connection_and_client_config( + command_executor: Union[str, AppiumConnection], client_config: Optional[AppiumClientConfig] = None +) -> tuple[AppiumConnection, Optional[AppiumClientConfig]]: + """Return the pair of command executor and client config. + If the given command executor is a custom one, returned client config will + be None since the custom command executor has its own client config already. + The custom command executor's one will be prior than the given client config. + """ + if not isinstance(command_executor, str): + # client config already defined in the custom command executor + # will be prior than the given one. + return (command_executor, None) + + # command_executor is str + + # Do not keep None to avoid warnings in Selenium + # which can prevent with ClientConfig instance usage. + new_client_config = AppiumClientConfig(remote_server_addr=command_executor) if client_config is None else client_config + return (AppiumConnection(client_config=new_client_config), new_client_config) + + class WebDriver( webdriver.Remote, ActionHelpers, @@ -202,28 +223,16 @@ class WebDriver( Sms, SystemBars, ): - def __init__( # noqa: PLR0913 + def __init__( self, - command_executor: Union[str, AppiumConnection] = 'http://127.0.0.1:4444/wd/hub', - keep_alive: bool = True, - direct_connection: bool = True, + command_executor: Union[str, AppiumConnection] = 'http://127.0.0.1:4723', extensions: Optional[List['WebDriver']] = None, - strict_ssl: bool = True, options: Union[AppiumOptions, List[AppiumOptions], None] = None, - client_config: Optional[ClientConfig] = None, + client_config: Optional[AppiumClientConfig] = None, ): - if isinstance(command_executor, str): - client_config = client_config or ClientConfig( - remote_server_addr=command_executor, keep_alive=keep_alive, ignore_certificates=not strict_ssl - ) - client_config.remote_server_addr = command_executor - command_executor = AppiumConnection(client_config=client_config) - elif isinstance(command_executor, AppiumConnection) and strict_ssl is False: - logger.warning( - "Please set 'ignore_certificates' in the given 'appium.webdriver.appium_connection.AppiumConnection' or " - "'selenium.webdriver.remote.client_config.ClientConfig' instead. Ignoring." - ) - + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor=command_executor, client_config=client_config + ) super().__init__( command_executor=command_executor, options=options, @@ -232,13 +241,12 @@ def __init__( # noqa: PLR0913 client_config=client_config, ) - if hasattr(self, 'command_executor'): - self._add_commands() + self._add_commands() self.error_handler = MobileErrorHandler() - if direct_connection: - self._update_command_executor(keep_alive=keep_alive) + if client_config and client_config.direct_connection: + self._update_command_executor(keep_alive=client_config.keep_alive) # add new method to the `find_by_*` pantheon By.IOS_PREDICATE = AppiumBy.IOS_PREDICATE diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index 47a4353e..443885d5 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -21,7 +21,8 @@ from appium import webdriver from appium.options.android import UiAutomator2Options from appium.webdriver.appium_connection import AppiumConnection -from appium.webdriver.webdriver import ExtensionBase, WebDriver +from appium.webdriver.client_config import AppiumClientConfig +from appium.webdriver.webdriver import ExtensionBase, WebDriver, _get_remote_connection_and_client_config from test.helpers.constants import SERVER_URL_BASE from test.unit.helper.test_helper import ( android_w3c_driver, @@ -124,10 +125,11 @@ def test_create_session_register_uridirect(self): 'app': 'path/to/app', 'automationName': 'UIAutomator2', } + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True) driver = webdriver.Remote( SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps), - direct_connection=True, + client_config=client_config, ) assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._client_config.remote_server_addr @@ -164,16 +166,54 @@ def test_create_session_register_uridirect_no_direct_connect_path(self): 'app': 'path/to/app', 'automationName': 'UIAutomator2', } + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True) driver = webdriver.Remote( - SERVER_URL_BASE, - options=UiAutomator2Options().load_capabilities(desired_caps), - direct_connection=True, + SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps), client_config=client_config ) assert SERVER_URL_BASE == driver.command_executor._client_config.remote_server_addr assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts assert isinstance(driver.command_executor, AppiumConnection) + @httpretty.activate + def test_create_session_remote_server_addr_treatment_with_appiumclientconfig(self): + # remote server add in AppiumRemoteCong will be prior than the string of 'command_executor' + # as same as Selenium behavior. + httpretty.register_uri( + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body=json.dumps( + { + 'sessionId': 'session-id', + 'capabilities': { + 'deviceName': 'Android Emulator', + }, + } + ), + ) + + httpretty.register_uri( + httpretty.GET, + f'{SERVER_URL_BASE}/session/session-id/contexts', + body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}), + ) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'UIAutomator2', + } + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True) + driver = webdriver.Remote( + 'http://localhost:8080/something/path', + options=UiAutomator2Options().load_capabilities(desired_caps), + client_config=client_config, + ) + + assert SERVER_URL_BASE == driver.command_executor._client_config.remote_server_addr + assert isinstance(driver.command_executor, AppiumConnection) + @httpretty.activate def test_get_events(self): driver = ios_w3c_driver() @@ -380,21 +420,54 @@ def test_extention_command_check(self): 'script': 'mobile: startActivity', } == get_httpretty_request_body(httpretty.last_request()) + def test_get_client_config_and_connection_with_empty_config(self): + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor='http://127.0.0.1:4723', client_config=None + ) + + assert isinstance(command_executor, AppiumConnection) + assert command_executor._client_config == client_config + assert isinstance(client_config, AppiumClientConfig) + assert client_config.remote_server_addr == 'http://127.0.0.1:4723' + + def test_get_client_config_and_connection(self): + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor='http://127.0.0.1:4723', + client_config=AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723/wd/hub'), + ) + + assert isinstance(command_executor, AppiumConnection) + # the client config in the command_executor is the given client config. + assert command_executor._client_config == client_config + assert isinstance(client_config, AppiumClientConfig) + assert client_config.remote_server_addr == 'http://127.0.0.1:4723/wd/hub' + + def test_get_client_config_and_connection_custom_appium_connection(self): + c_config = AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723') + appium_connection = AppiumConnection(client_config=c_config) + + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor=appium_connection, client_config=AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723') + ) + + assert isinstance(command_executor, AppiumConnection) + # client config already defined in the command_executor will be used. + assert command_executor._client_config != client_config + assert client_config is None + class SubWebDriver(WebDriver): - def __init__(self, command_executor, direct_connection=False, options=None): + def __init__(self, command_executor, options=None): super().__init__( command_executor=command_executor, - direct_connection=direct_connection, options=options, ) class SubSubWebDriver(SubWebDriver): - def __init__(self, command_executor, direct_connection=False, options=None): + def __init__(self, command_executor, options=None): super().__init__( command_executor=command_executor, - direct_connection=direct_connection, options=options, )