Skip to content

Commit 94a6da7

Browse files
authored
feat: support selenium 4.26+: support ClientConfig and refactoring internal 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
1 parent f26f763 commit 94a6da7

34 files changed

+308
-321
lines changed

.github/workflows/functional-test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ jobs:
2929
XCODE_VERSION: 15.3
3030
IOS_VERSION: 17.4
3131
IPHONE_MODEL: iPhone 15 Plus
32+
GLOBAL_DEFAULT_TIMEOUT: 600
3233

3334
steps:
3435
- uses: actions/checkout@v3

Pipfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ tox = "~=4.23"
1515
types-python-dateutil = "~=2.9"
1616

1717
[packages]
18-
selenium = "==4.25"
18+
selenium = "==4.26.1"
1919
typing-extensions = "~=4.12.2"

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz).
4141

4242
|Appium Python Client| Selenium binding| Python version |
4343
|----|----|----|
44+
|`4.3.0`+ |`4.26.0`+ | 3.8+ |
4445
|`3.0.0` - `4.2.1` |`4.12.0` - `4.25.0` | 3.8+ |
4546
|`2.10.0` - `2.11.1` |`4.1.0` - `4.11.2` | 3.7+ |
4647
|`2.2.0` - `2.9.0` |`4.1.0` - `4.9.0` | 3.7+ |
@@ -311,6 +312,21 @@ options.set_capability('browser_name', 'safari')
311312
driver = webdriver.Remote('http://127.0.0.1:4723', options=options, strict_ssl=False)
312313
```
313314
315+
Since Appium Python client v4.3.0, we recommend using `selenium.webdriver.remote.client_config.ClientConfig`
316+
instead of giving `strict_ssl` as an argument of `webdriver.Remote` below to configure the validation.
317+
318+
```python
319+
from appium import webdriver
320+
321+
from selenium.webdriver.remote.client_config import ClientConfig
322+
323+
client_config = ClientConfig(
324+
remote_server_addr='http://127.0.0.1:4723',
325+
ignore_certificates=True
326+
)
327+
driver = webdriver.Remote(client_config.remote_server_addr, options=options, client_config=client_config)
328+
```
329+
314330
## Set custom `AppiumConnection`
315331
316332
The first argument of `webdriver.Remote` can set an arbitrary command executor for you.
@@ -364,6 +380,18 @@ driver = webdriver.Remote(custom_executor, options=options)
364380
365381
```
366382
383+
The `AppiumConnection` can set `selenium.webdriver.remote.client_config.ClientConfig` as well.
384+
385+
## Relaxing HTTP request read timeout
386+
387+
Appium Python Client has `120` seconds read timeout on each HTTP request since the version v4.3.0 because of
388+
the corresponding selenium binding version.
389+
You have two methods to extend the read timeout.
390+
391+
1. Set `GLOBAL_DEFAULT_TIMEOUT` environment variable
392+
2. Configure timeout via `selenium.webdriver.remote.client_config.ClientConfig`
393+
- `timeout` argument, or
394+
- `init_args_for_pool_manager` argument for `urllib3.PoolManager`
367395
368396
## Documentation
369397

appium/webdriver/appium_connection.py

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313
# limitations under the License.
1414

1515
import uuid
16-
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
16+
from typing import TYPE_CHECKING, Any, Dict
1717

18-
import urllib3
1918
from selenium.webdriver.remote.remote_connection import RemoteConnection
2019

2120
from appium.common.helper import library_version
@@ -26,55 +25,40 @@
2625

2726
PREFIX_HEADER = 'appium/'
2827

28+
HEADER_IDEMOTENCY_KEY = 'X-Idempotency-Key'
2929

30-
class AppiumConnection(RemoteConnection):
31-
_proxy_url: Optional[str]
32-
33-
def __init__(
34-
self,
35-
remote_server_addr: str,
36-
keep_alive: bool = False,
37-
ignore_proxy: Optional[bool] = False,
38-
init_args_for_pool_manager: Union[Dict[str, Any], None] = None,
39-
):
40-
# Need to call before super().__init__ in order to pass arguments for the pool manager in the super.
41-
self._init_args_for_pool_manager = init_args_for_pool_manager or {}
42-
43-
super().__init__(remote_server_addr, keep_alive=keep_alive, ignore_proxy=ignore_proxy)
44-
45-
def _get_connection_manager(self) -> Union[urllib3.PoolManager, urllib3.ProxyManager]:
46-
# https://github.com/SeleniumHQ/selenium/blob/0e0194b0e52a34e7df4b841f1ed74506beea5c3e/py/selenium/webdriver/remote/remote_connection.py#L134
47-
pool_manager_init_args = {'timeout': self.get_timeout()}
48-
49-
if self._ca_certs:
50-
pool_manager_init_args['cert_reqs'] = 'CERT_REQUIRED'
51-
pool_manager_init_args['ca_certs'] = self._ca_certs
52-
else:
53-
# This line is necessary to disable certificate verification
54-
pool_manager_init_args['cert_reqs'] = 'CERT_NONE'
5530

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

58-
if self._proxy_url:
59-
if self._proxy_url.lower().startswith('sock'):
60-
from urllib3.contrib.socks import SOCKSProxyManager
38+
class AppiumConnection(RemoteConnection):
39+
"""
40+
A subclass of selenium.webdriver.remote.remote_connection.Remoteconnection.
6141
62-
return SOCKSProxyManager(self._proxy_url, **pool_manager_init_args)
63-
if self._identify_http_proxy_auth():
64-
self._proxy_url, self._basic_proxy_auth = self._separate_http_proxy_auth()
65-
pool_manager_init_args['proxy_headers'] = urllib3.make_headers(proxy_basic_auth=self._basic_proxy_auth)
66-
return urllib3.ProxyManager(self._proxy_url, **pool_manager_init_args)
42+
The changes are:
43+
- The default user agent
44+
- Adds 'X-Idempotency-Key' header in a new session request to avoid proceeding
45+
the same request multiple times in the Appium server side.
46+
- https://github.com/appium/appium-base-driver/pull/400
47+
"""
6748

68-
return urllib3.PoolManager(**pool_manager_init_args)
49+
user_agent = f'{PREFIX_HEADER}{library_version()} ({RemoteConnection.user_agent})'
50+
extra_headers = {}
6951

7052
@classmethod
7153
def get_remote_connection_headers(cls, parsed_url: 'ParseResult', keep_alive: bool = True) -> Dict[str, Any]:
72-
"""Override get_remote_connection_headers in RemoteConnection"""
73-
headers = RemoteConnection.get_remote_connection_headers(parsed_url, keep_alive=keep_alive)
74-
# e.g. appium/0.49 (selenium/3.141.0 (python linux))
75-
headers['User-Agent'] = f'{PREFIX_HEADER}{library_version()} ({headers["User-Agent"]})'
54+
"""Override get_remote_connection_headers in RemoteConnection to control the extra headers.
55+
This method will be used in sending a request method in this class.
56+
"""
57+
7658
if parsed_url.path.endswith('/session'):
7759
# https://github.com/appium/appium-base-driver/pull/400
78-
headers['X-Idempotency-Key'] = str(uuid.uuid4())
60+
cls.extra_headers[HEADER_IDEMOTENCY_KEY] = str(uuid.uuid4())
61+
else:
62+
cls.extra_headers = _get_new_headers(HEADER_IDEMOTENCY_KEY, cls.extra_headers)
7963

80-
return headers
64+
return {**super().get_remote_connection_headers(parsed_url, keep_alive=keep_alive), **cls.extra_headers}

appium/webdriver/extensions/android/activities.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ def wait_activity(self, activity: str, timeout: int, interval: int = 1) -> bool:
5656
return False
5757

5858
def _add_commands(self) -> None:
59-
# noinspection PyProtectedMember,PyUnresolvedReferences
60-
commands = self.command_executor._commands
61-
commands[Command.GET_CURRENT_ACTIVITY] = (
59+
self.command_executor.add_command(
60+
Command.GET_CURRENT_ACTIVITY,
6261
'GET',
6362
'/session/$sessionId/appium/device/current_activity',
6463
)

appium/webdriver/extensions/android/common.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ def current_package(self) -> str:
4747
return self.mark_extension_absence(ext_name).execute(Command.GET_CURRENT_PACKAGE)['value']
4848

4949
def _add_commands(self) -> None:
50-
# noinspection PyProtectedMember,PyUnresolvedReferences
51-
commands = self.command_executor._commands
52-
commands[Command.GET_CURRENT_PACKAGE] = (
50+
self.command_executor.add_command(
51+
Command.GET_CURRENT_PACKAGE,
5352
'GET',
5453
'/session/$sessionId/appium/device/current_package',
5554
)
56-
commands[Command.OPEN_NOTIFICATIONS] = (
55+
self.command_executor.add_command(
56+
Command.OPEN_NOTIFICATIONS,
5757
'POST',
5858
'/session/$sessionId/appium/device/open_notifications',
5959
)

appium/webdriver/extensions/android/display.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ def get_display_density(self) -> int:
4141
return self.mark_extension_absence(ext_name).execute(Command.GET_DISPLAY_DENSITY)['value']
4242

4343
def _add_commands(self) -> None:
44-
# noinspection PyProtectedMember,PyUnresolvedReferences
45-
commands = self.command_executor._commands
46-
commands[Command.GET_DISPLAY_DENSITY] = (
44+
self.command_executor.add_command(
45+
Command.GET_DISPLAY_DENSITY,
4746
'GET',
4847
'/session/$sessionId/appium/device/display_density',
4948
)

appium/webdriver/extensions/android/gsm.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,6 @@ def set_gsm_voice(self, state: str) -> Self:
142142
return self
143143

144144
def _add_commands(self) -> None:
145-
# noinspection PyProtectedMember,PyUnresolvedReferences
146-
commands = self.command_executor._commands
147-
commands[Command.MAKE_GSM_CALL] = ('POST', '/session/$sessionId/appium/device/gsm_call')
148-
commands[Command.SET_GSM_SIGNAL] = (
149-
'POST',
150-
'/session/$sessionId/appium/device/gsm_signal',
151-
)
152-
commands[Command.SET_GSM_VOICE] = ('POST', '/session/$sessionId/appium/device/gsm_voice')
145+
self.command_executor.add_command(Command.MAKE_GSM_CALL, 'POST', '/session/$sessionId/appium/device/gsm_call')
146+
self.command_executor.add_command(Command.SET_GSM_SIGNAL, 'POST', '/session/$sessionId/appium/device/gsm_signal')
147+
self.command_executor.add_command(Command.SET_GSM_VOICE, 'POST', '/session/$sessionId/appium/device/gsm_voice')

appium/webdriver/extensions/android/network.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,18 +157,19 @@ def set_network_speed(self, speed_type: str) -> Self:
157157
return self
158158

159159
def _add_commands(self) -> None:
160-
# noinspection PyProtectedMember,PyUnresolvedReferences
161-
commands = self.command_executor._commands
162-
commands[Command.TOGGLE_WIFI] = ('POST', '/session/$sessionId/appium/device/toggle_wifi')
163-
commands[Command.GET_NETWORK_CONNECTION] = (
160+
self.command_executor.add_command(Command.TOGGLE_WIFI, 'POST', '/session/$sessionId/appium/device/toggle_wifi')
161+
self.command_executor.add_command(
162+
Command.GET_NETWORK_CONNECTION,
164163
'GET',
165164
'/session/$sessionId/network_connection',
166165
)
167-
commands[Command.SET_NETWORK_CONNECTION] = (
166+
self.command_executor.add_command(
167+
Command.SET_NETWORK_CONNECTION,
168168
'POST',
169169
'/session/$sessionId/network_connection',
170170
)
171-
commands[Command.SET_NETWORK_SPEED] = (
171+
self.command_executor.add_command(
172+
Command.SET_NETWORK_SPEED,
172173
'POST',
173174
'/session/$sessionId/appium/device/network_speed',
174175
)

appium/webdriver/extensions/android/performance.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ def get_performance_data_types(self) -> List[str]:
7373
return self.mark_extension_absence(ext_name).execute(Command.GET_PERFORMANCE_DATA_TYPES)['value']
7474

7575
def _add_commands(self) -> None:
76-
# noinspection PyProtectedMember,PyUnresolvedReferences
77-
commands = self.command_executor._commands
78-
commands[Command.GET_PERFORMANCE_DATA] = (
76+
self.command_executor.add_command(
77+
Command.GET_PERFORMANCE_DATA,
7978
'POST',
8079
'/session/$sessionId/appium/getPerformanceData',
8180
)
82-
commands[Command.GET_PERFORMANCE_DATA_TYPES] = (
81+
self.command_executor.add_command(
82+
Command.GET_PERFORMANCE_DATA_TYPES,
8383
'POST',
8484
'/session/$sessionId/appium/performanceData/types',
8585
)

0 commit comments

Comments
 (0)