Skip to content

Commit

Permalink
feat: support selenium 4.26+: support ClientConfig and refactoring in…
Browse files Browse the repository at this point in the history
…ternal implementation (#1054)

* feat: require selenium 4.26+

* update executor command

* add more code

* tweak the init

* tweak arguments

* fix test

* apply add_command

* use add_command

* add GLOBAL_DEFAULT_TIMEOUT

* add a workaround fix

* use 4.26.1

* remove possible redundant init

* add warning

* add todo

* add description more

* use Tuple or python 3.8 and lower

* add example of ClientConfig

* add read timeout example

* update readme

* correct headers

* more timeout

* simplify a bit

* tweak the readme

* docs: update the readme

* get new headers

* fix type for py3.8

* fix review

* fix review, extract locator_converter
  • Loading branch information
KazuCocoa authored Nov 11, 2024
1 parent f26f763 commit 94a6da7
Show file tree
Hide file tree
Showing 34 changed files with 308 additions and 321 deletions.
1 change: 1 addition & 0 deletions .github/workflows/functional-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
XCODE_VERSION: 15.3
IOS_VERSION: 17.4
IPHONE_MODEL: iPhone 15 Plus
GLOBAL_DEFAULT_TIMEOUT: 600

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ tox = "~=4.23"
types-python-dateutil = "~=2.9"

[packages]
selenium = "==4.25"
selenium = "==4.26.1"
typing-extensions = "~=4.12.2"
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz).

|Appium Python Client| Selenium binding| Python version |
|----|----|----|
|`4.3.0`+ |`4.26.0`+ | 3.8+ |
|`3.0.0` - `4.2.1` |`4.12.0` - `4.25.0` | 3.8+ |
|`2.10.0` - `2.11.1` |`4.1.0` - `4.11.2` | 3.7+ |
|`2.2.0` - `2.9.0` |`4.1.0` - `4.9.0` | 3.7+ |
Expand Down Expand Up @@ -311,6 +312,21 @@ options.set_capability('browser_name', 'safari')
driver = webdriver.Remote('http://127.0.0.1:4723', options=options, strict_ssl=False)
```
Since Appium Python client v4.3.0, we recommend using `selenium.webdriver.remote.client_config.ClientConfig`
instead of giving `strict_ssl` as an argument of `webdriver.Remote` below to configure the validation.
```python
from appium import webdriver
from selenium.webdriver.remote.client_config import ClientConfig
client_config = ClientConfig(
remote_server_addr='http://127.0.0.1:4723',
ignore_certificates=True
)
driver = webdriver.Remote(client_config.remote_server_addr, options=options, client_config=client_config)
```
## Set custom `AppiumConnection`
The first argument of `webdriver.Remote` can set an arbitrary command executor for you.
Expand Down Expand Up @@ -364,6 +380,18 @@ driver = webdriver.Remote(custom_executor, options=options)
```
The `AppiumConnection` can set `selenium.webdriver.remote.client_config.ClientConfig` as well.
## Relaxing HTTP request read timeout
Appium Python Client has `120` seconds read timeout on each HTTP request since the version v4.3.0 because of
the corresponding selenium binding version.
You have two methods to extend the read timeout.
1. Set `GLOBAL_DEFAULT_TIMEOUT` environment variable
2. Configure timeout via `selenium.webdriver.remote.client_config.ClientConfig`
- `timeout` argument, or
- `init_args_for_pool_manager` argument for `urllib3.PoolManager`
## Documentation
Expand Down
70 changes: 27 additions & 43 deletions appium/webdriver/appium_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@
# limitations under the License.

import uuid
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from typing import TYPE_CHECKING, Any, Dict

import urllib3
from selenium.webdriver.remote.remote_connection import RemoteConnection

from appium.common.helper import library_version
Expand All @@ -26,55 +25,40 @@

PREFIX_HEADER = 'appium/'

HEADER_IDEMOTENCY_KEY = 'X-Idempotency-Key'

class AppiumConnection(RemoteConnection):
_proxy_url: Optional[str]

def __init__(
self,
remote_server_addr: str,
keep_alive: bool = False,
ignore_proxy: Optional[bool] = False,
init_args_for_pool_manager: Union[Dict[str, Any], None] = None,
):
# Need to call before super().__init__ in order to pass arguments for the pool manager in the super.
self._init_args_for_pool_manager = init_args_for_pool_manager or {}

super().__init__(remote_server_addr, keep_alive=keep_alive, ignore_proxy=ignore_proxy)

def _get_connection_manager(self) -> Union[urllib3.PoolManager, urllib3.ProxyManager]:
# https://github.com/SeleniumHQ/selenium/blob/0e0194b0e52a34e7df4b841f1ed74506beea5c3e/py/selenium/webdriver/remote/remote_connection.py#L134
pool_manager_init_args = {'timeout': self.get_timeout()}

if self._ca_certs:
pool_manager_init_args['cert_reqs'] = 'CERT_REQUIRED'
pool_manager_init_args['ca_certs'] = self._ca_certs
else:
# This line is necessary to disable certificate verification
pool_manager_init_args['cert_reqs'] = 'CERT_NONE'

pool_manager_init_args.update(self._init_args_for_pool_manager)
def _get_new_headers(key: str, headers: Dict[str, str]) -> Dict[str, str]:
"""Return a new dictionary of heafers without the given key.
The key match is case-insensitive."""
lower_key = key.lower()
return {k: v for k, v in headers.items() if k.lower() != lower_key}


if self._proxy_url:
if self._proxy_url.lower().startswith('sock'):
from urllib3.contrib.socks import SOCKSProxyManager
class AppiumConnection(RemoteConnection):
"""
A subclass of selenium.webdriver.remote.remote_connection.Remoteconnection.
return SOCKSProxyManager(self._proxy_url, **pool_manager_init_args)
if self._identify_http_proxy_auth():
self._proxy_url, self._basic_proxy_auth = self._separate_http_proxy_auth()
pool_manager_init_args['proxy_headers'] = urllib3.make_headers(proxy_basic_auth=self._basic_proxy_auth)
return urllib3.ProxyManager(self._proxy_url, **pool_manager_init_args)
The changes are:
- The default user agent
- Adds 'X-Idempotency-Key' header in a new session request to avoid proceeding
the same request multiple times in the Appium server side.
- https://github.com/appium/appium-base-driver/pull/400
"""

return urllib3.PoolManager(**pool_manager_init_args)
user_agent = f'{PREFIX_HEADER}{library_version()} ({RemoteConnection.user_agent})'
extra_headers = {}

@classmethod
def get_remote_connection_headers(cls, parsed_url: 'ParseResult', keep_alive: bool = True) -> Dict[str, Any]:
"""Override get_remote_connection_headers in RemoteConnection"""
headers = RemoteConnection.get_remote_connection_headers(parsed_url, keep_alive=keep_alive)
# e.g. appium/0.49 (selenium/3.141.0 (python linux))
headers['User-Agent'] = f'{PREFIX_HEADER}{library_version()} ({headers["User-Agent"]})'
"""Override get_remote_connection_headers in RemoteConnection to control the extra headers.
This method will be used in sending a request method in this class.
"""

if parsed_url.path.endswith('/session'):
# https://github.com/appium/appium-base-driver/pull/400
headers['X-Idempotency-Key'] = str(uuid.uuid4())
cls.extra_headers[HEADER_IDEMOTENCY_KEY] = str(uuid.uuid4())
else:
cls.extra_headers = _get_new_headers(HEADER_IDEMOTENCY_KEY, cls.extra_headers)

return headers
return {**super().get_remote_connection_headers(parsed_url, keep_alive=keep_alive), **cls.extra_headers}
5 changes: 2 additions & 3 deletions appium/webdriver/extensions/android/activities.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ def wait_activity(self, activity: str, timeout: int, interval: int = 1) -> bool:
return False

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.GET_CURRENT_ACTIVITY] = (
self.command_executor.add_command(
Command.GET_CURRENT_ACTIVITY,
'GET',
'/session/$sessionId/appium/device/current_activity',
)
8 changes: 4 additions & 4 deletions appium/webdriver/extensions/android/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ def current_package(self) -> str:
return self.mark_extension_absence(ext_name).execute(Command.GET_CURRENT_PACKAGE)['value']

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.GET_CURRENT_PACKAGE] = (
self.command_executor.add_command(
Command.GET_CURRENT_PACKAGE,
'GET',
'/session/$sessionId/appium/device/current_package',
)
commands[Command.OPEN_NOTIFICATIONS] = (
self.command_executor.add_command(
Command.OPEN_NOTIFICATIONS,
'POST',
'/session/$sessionId/appium/device/open_notifications',
)
5 changes: 2 additions & 3 deletions appium/webdriver/extensions/android/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@ def get_display_density(self) -> int:
return self.mark_extension_absence(ext_name).execute(Command.GET_DISPLAY_DENSITY)['value']

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.GET_DISPLAY_DENSITY] = (
self.command_executor.add_command(
Command.GET_DISPLAY_DENSITY,
'GET',
'/session/$sessionId/appium/device/display_density',
)
11 changes: 3 additions & 8 deletions appium/webdriver/extensions/android/gsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,6 @@ def set_gsm_voice(self, state: str) -> Self:
return self

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.MAKE_GSM_CALL] = ('POST', '/session/$sessionId/appium/device/gsm_call')
commands[Command.SET_GSM_SIGNAL] = (
'POST',
'/session/$sessionId/appium/device/gsm_signal',
)
commands[Command.SET_GSM_VOICE] = ('POST', '/session/$sessionId/appium/device/gsm_voice')
self.command_executor.add_command(Command.MAKE_GSM_CALL, 'POST', '/session/$sessionId/appium/device/gsm_call')
self.command_executor.add_command(Command.SET_GSM_SIGNAL, 'POST', '/session/$sessionId/appium/device/gsm_signal')
self.command_executor.add_command(Command.SET_GSM_VOICE, 'POST', '/session/$sessionId/appium/device/gsm_voice')
13 changes: 7 additions & 6 deletions appium/webdriver/extensions/android/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,18 +157,19 @@ def set_network_speed(self, speed_type: str) -> Self:
return self

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.TOGGLE_WIFI] = ('POST', '/session/$sessionId/appium/device/toggle_wifi')
commands[Command.GET_NETWORK_CONNECTION] = (
self.command_executor.add_command(Command.TOGGLE_WIFI, 'POST', '/session/$sessionId/appium/device/toggle_wifi')
self.command_executor.add_command(
Command.GET_NETWORK_CONNECTION,
'GET',
'/session/$sessionId/network_connection',
)
commands[Command.SET_NETWORK_CONNECTION] = (
self.command_executor.add_command(
Command.SET_NETWORK_CONNECTION,
'POST',
'/session/$sessionId/network_connection',
)
commands[Command.SET_NETWORK_SPEED] = (
self.command_executor.add_command(
Command.SET_NETWORK_SPEED,
'POST',
'/session/$sessionId/appium/device/network_speed',
)
8 changes: 4 additions & 4 deletions appium/webdriver/extensions/android/performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ def get_performance_data_types(self) -> List[str]:
return self.mark_extension_absence(ext_name).execute(Command.GET_PERFORMANCE_DATA_TYPES)['value']

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.GET_PERFORMANCE_DATA] = (
self.command_executor.add_command(
Command.GET_PERFORMANCE_DATA,
'POST',
'/session/$sessionId/appium/getPerformanceData',
)
commands[Command.GET_PERFORMANCE_DATA_TYPES] = (
self.command_executor.add_command(
Command.GET_PERFORMANCE_DATA_TYPES,
'POST',
'/session/$sessionId/appium/performanceData/types',
)
7 changes: 3 additions & 4 deletions appium/webdriver/extensions/android/power.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,9 @@ def set_power_ac(self, ac_state: str) -> Self:
return self

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.SET_POWER_CAPACITY] = (
self.command_executor.add_command(
Command.SET_POWER_CAPACITY,
'POST',
'/session/$sessionId/appium/device/power_capacity',
)
commands[Command.SET_POWER_AC] = ('POST', '/session/$sessionId/appium/device/power_ac')
self.command_executor.add_command(Command.SET_POWER_AC, 'POST', '/session/$sessionId/appium/device/power_ac')
4 changes: 1 addition & 3 deletions appium/webdriver/extensions/android/sms.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,4 @@ def send_sms(self, phone_number: str, message: str) -> Self:
return self

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.SEND_SMS] = ('POST', '/session/$sessionId/appium/device/send_sms')
self.command_executor.add_command(Command.SEND_SMS, 'POST', '/session/$sessionId/appium/device/send_sms')
5 changes: 2 additions & 3 deletions appium/webdriver/extensions/android/system_bars.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ def get_system_bars(self) -> Dict[str, Dict[str, Union[int, bool]]]:
return self.mark_extension_absence(ext_name).execute(Command.GET_SYSTEM_BARS)['value']

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.GET_SYSTEM_BARS] = (
self.command_executor.add_command(
Command.GET_SYSTEM_BARS,
'GET',
'/session/$sessionId/appium/device/system_bars',
)
22 changes: 12 additions & 10 deletions appium/webdriver/extensions/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,25 +248,27 @@ def app_strings(self, language: Union[str, None] = None, string_file: Union[str,
return self.mark_extension_absence(ext_name).execute(Command.GET_APP_STRINGS, data)['value']

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.BACKGROUND] = ('POST', '/session/$sessionId/appium/app/background')
commands[Command.IS_APP_INSTALLED] = (
self.command_executor.add_command(Command.BACKGROUND, 'POST', '/session/$sessionId/appium/app/background')
self.command_executor.add_command(
Command.IS_APP_INSTALLED,
'POST',
'/session/$sessionId/appium/device/app_installed',
)
commands[Command.INSTALL_APP] = ('POST', '/session/$sessionId/appium/device/install_app')
commands[Command.REMOVE_APP] = ('POST', '/session/$sessionId/appium/device/remove_app')
commands[Command.TERMINATE_APP] = (
self.command_executor.add_command(Command.INSTALL_APP, 'POST', '/session/$sessionId/appium/device/install_app')
self.command_executor.add_command(Command.REMOVE_APP, 'POST', '/session/$sessionId/appium/device/remove_app')
self.command_executor.add_command(
Command.TERMINATE_APP,
'POST',
'/session/$sessionId/appium/device/terminate_app',
)
commands[Command.ACTIVATE_APP] = (
self.command_executor.add_command(
Command.ACTIVATE_APP,
'POST',
'/session/$sessionId/appium/device/activate_app',
)
commands[Command.QUERY_APP_STATE] = (
self.command_executor.add_command(
Command.QUERY_APP_STATE,
'POST',
'/session/$sessionId/appium/device/app_state',
)
commands[Command.GET_APP_STRINGS] = ('POST', '/session/$sessionId/appium/app/strings')
self.command_executor.add_command(Command.GET_APP_STRINGS, 'POST', '/session/$sessionId/appium/app/strings')
8 changes: 4 additions & 4 deletions appium/webdriver/extensions/clipboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ def get_clipboard_text(self) -> str:
return self.get_clipboard(ClipboardContentType.PLAINTEXT).decode('UTF-8')

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.SET_CLIPBOARD] = (
self.command_executor.add_command(
Command.SET_CLIPBOARD,
'POST',
'/session/$sessionId/appium/device/set_clipboard',
)
commands[Command.GET_CLIPBOARD] = (
self.command_executor.add_command(
Command.GET_CLIPBOARD,
'POST',
'/session/$sessionId/appium/device/get_clipboard',
)
8 changes: 3 additions & 5 deletions appium/webdriver/extensions/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ def context(self) -> str:
return self.current_context

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.CONTEXTS] = ('GET', '/session/$sessionId/contexts')
commands[Command.GET_CURRENT_CONTEXT] = ('GET', '/session/$sessionId/context')
commands[Command.SWITCH_TO_CONTEXT] = ('POST', '/session/$sessionId/context')
self.command_executor.add_command(Command.CONTEXTS, 'GET', '/session/$sessionId/contexts')
self.command_executor.add_command(Command.GET_CURRENT_CONTEXT, 'GET', '/session/$sessionId/context')
self.command_executor.add_command(Command.SWITCH_TO_CONTEXT, 'POST', '/session/$sessionId/context')
8 changes: 4 additions & 4 deletions appium/webdriver/extensions/device_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ def get_device_time(self, format: Optional[str] = None) -> str:
return self.mark_extension_absence(ext_name).execute(Command.GET_DEVICE_TIME_POST, {'format': format})['value']

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.GET_DEVICE_TIME_GET] = (
self.command_executor.add_command(
Command.GET_DEVICE_TIME_GET,
'GET',
'/session/$sessionId/appium/device/system_time',
)
commands[Command.GET_DEVICE_TIME_POST] = (
self.command_executor.add_command(
Command.GET_DEVICE_TIME_POST,
'POST',
'/session/$sessionId/appium/device/system_time',
)
4 changes: 1 addition & 3 deletions appium/webdriver/extensions/execute_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,4 @@ def __init__(self, res: Dict):
return Result(response)

def _add_commands(self) -> None:
# noinspection PyProtectedMember,PyUnresolvedReferences
commands = self.command_executor._commands
commands[Command.EXECUTE_DRIVER] = ('POST', '/session/$sessionId/appium/execute_driver')
self.command_executor.add_command(Command.EXECUTE_DRIVER, 'POST', '/session/$sessionId/appium/execute_driver')
Loading

0 comments on commit 94a6da7

Please sign in to comment.