Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: support selenium 4.26+: support ClientConfig and refactoring internal implementation #1054

Merged
merged 31 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
88ca1e7
feat: require selenium 4.26+
KazuCocoa Oct 31, 2024
d869172
update executor command
KazuCocoa Oct 31, 2024
a9276b7
add more code
KazuCocoa Oct 31, 2024
a9aefe9
tweak the init
KazuCocoa Oct 31, 2024
11bfefa
tweak arguments
KazuCocoa Oct 31, 2024
ac1871a
fix test
KazuCocoa Oct 31, 2024
131776b
apply add_command
KazuCocoa Oct 31, 2024
5491c03
use add_command
KazuCocoa Oct 31, 2024
9a47aad
add GLOBAL_DEFAULT_TIMEOUT
KazuCocoa Oct 31, 2024
edbc263
add a workaround fix
KazuCocoa Oct 31, 2024
bf8b447
Merge branch 'master' into selenium-4.26
KazuCocoa Oct 31, 2024
78e5609
use 4.26.1
KazuCocoa Nov 1, 2024
52e2c60
remove possible redundant init
KazuCocoa Nov 1, 2024
28300e4
add warning
KazuCocoa Nov 1, 2024
1e46087
add todo
KazuCocoa Nov 1, 2024
cf337b9
add description more
KazuCocoa Nov 1, 2024
d37e6df
use Tuple or python 3.8 and lower
KazuCocoa Nov 1, 2024
4599c96
add example of ClientConfig
KazuCocoa Nov 1, 2024
d01d531
add read timeout example
KazuCocoa Nov 1, 2024
9a2cbbc
update readme
KazuCocoa Nov 1, 2024
d497af8
correct headers
KazuCocoa Nov 1, 2024
ae4b248
more timeout
KazuCocoa Nov 1, 2024
9e95458
simplify a bit
KazuCocoa Nov 2, 2024
c97daa1
tweak the readme
KazuCocoa Nov 5, 2024
d2214b3
Merge branch 'master' into selenium-4.26
KazuCocoa Nov 5, 2024
93b40d6
docs: update the readme
KazuCocoa Nov 6, 2024
cddeaa1
Merge branch 'selenium-4.26' of github.com:appium/python-client into …
KazuCocoa Nov 6, 2024
3509353
get new headers
KazuCocoa Nov 9, 2024
96533c8
fix type for py3.8
KazuCocoa Nov 9, 2024
b458679
fix review
KazuCocoa Nov 10, 2024
da595d6
fix review, extract locator_converter
KazuCocoa Nov 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
76 changes: 33 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,46 @@

PREFIX_HEADER = 'appium/'

_HEADER_IDEMOTENCY_KEY = 'X-Idempotency-Key'
KazuCocoa marked this conversation as resolved.
Show resolved Hide resolved

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."""
new_headers = dict()
mykola-mokhnach marked this conversation as resolved.
Show resolved Hide resolved

if self._proxy_url:
if self._proxy_url.lower().startswith('sock'):
from urllib3.contrib.socks import SOCKSProxyManager
key = key.lower()
KazuCocoa marked this conversation as resolved.
Show resolved Hide resolved
for k, v in headers.items():
if k.lower() == key:
continue
new_headers[k] = v
return new_headers

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)

return urllib3.PoolManager(**pool_manager_init_args)
class AppiumConnection(RemoteConnection):
"""
A subclass of selenium.webdriver.remote.remote_connection.Remoteconnection.

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
"""

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
Loading