Skip to content

Commit 525d5b8

Browse files
KazuCocoaCopilot
andauthored
feat: define AppiumClientConfig (#1070)
* initial implementation * remove * add appium/webdriver/client_config.py * remove duplicated args * remove duplicated args * fix typo * add file_detector and remove redundant config * add test to check remote_server_addr priority * remove PLR0913, address http://127.0.0.1:4723 * update the readme * add comment * fix typo * extract * extract and followed the selenium * add comment * Update webdriver.py * Apply suggestions from code review Co-authored-by: Copilot <[email protected]> * update readme * remove redundant command_executor check * modify a bit * fix typo and apply comment * use new variable --------- Co-authored-by: Copilot <[email protected]>
1 parent d2326b9 commit 525d5b8

File tree

4 files changed

+189
-33
lines changed

4 files changed

+189
-33
lines changed

README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,37 @@ For example, some changes in the Selenium binding could break the Appium client.
5959
> to keep compatible version combinations.
6060

6161

62+
### Quick migration guide from v4 to v5
63+
- This change affects only for users who specify `keep_alive`, `direct_connection` and `strict_ssl` arguments for `webdriver.Remote`:
64+
- Please use `AppiumClientConfig` as `client_config` argument similar to how it is specified below:
65+
```python
66+
SERVER_URL_BASE = 'http://127.0.0.1:4723'
67+
# before
68+
driver = webdriver.Remote(
69+
SERVER_URL_BASE,
70+
options=UiAutomator2Options().load_capabilities(desired_caps),
71+
direct_connection=True,
72+
keep_alive=False,
73+
strict_ssl=False
74+
)
75+
76+
# after
77+
from appium.webdriver.client_config import AppiumClientConfig
78+
client_config = AppiumClientConfig(
79+
remote_server_addr=SERVER_URL_BASE,
80+
direct_connection=True,
81+
keep_alive=False,
82+
ignore_certificates=True,
83+
)
84+
driver = webdriver.Remote(
85+
options=UiAutomator2Options().load_capabilities(desired_caps),
86+
client_config=client_config
87+
)
88+
```
89+
- Note that you can keep using `webdriver.Remote(url, options=options, client_config=client_config)` format as well.
90+
In such case the `remote_server_addr` argument of `AppiumClientConfig` constructor would have priority over the `url` argument of `webdriver.Remote` constructor.
91+
- Use `http://127.0.0.1:4723` as the default server url instead of `http://127.0.0.1:4444/wd/hub`
92+
6293
### Quick migration guide from v3 to v4
6394
- Removal
6495
- `MultiAction` and `TouchAction` are removed. Please use W3C WebDriver actions or `mobile:` extensions
@@ -274,6 +305,7 @@ from appium import webdriver
274305
# If you use an older client then switch to desired_capabilities
275306
# instead: https://github.com/appium/python-client/pull/720
276307
from appium.options.ios import XCUITestOptions
308+
from appium.webdriver.client_config import AppiumClientConfig
277309
278310
# load_capabilities API could be used to
279311
# load options mapping stored in a dictionary
@@ -283,11 +315,16 @@ options = XCUITestOptions().load_capabilities({
283315
'app': '/full/path/to/app/UICatalog.app.zip',
284316
})
285317
318+
client_config = AppiumClientConfig(
319+
remote_server_addr='http://127.0.0.1:4723',
320+
direct_connection=True
321+
)
322+
286323
driver = webdriver.Remote(
287324
# Appium1 points to http://127.0.0.1:4723/wd/hub by default
288325
'http://127.0.0.1:4723',
289326
options=options,
290-
direct_connection=True
327+
client_config=client_config
291328
)
292329
```
293330

appium/webdriver/client_config.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
from selenium.webdriver.remote.client_config import ClientConfig
14+
15+
16+
class AppiumClientConfig(ClientConfig):
17+
"""ClientConfig class for Appium Python client.
18+
This class inherits selenium.webdriver.remote.client_config.ClientConfig.
19+
"""
20+
21+
def __init__(self, remote_server_addr: str, *args, **kwargs):
22+
"""
23+
Please refer to selenium.webdriver.remote.client_config.ClientConfig documentation
24+
about available arguments. Only 'direct_connection' below is AppiumClientConfig
25+
specific argument.
26+
27+
Args:
28+
direct_connection: If enables [directConnect](https://github.com/appium/python-client?tab=readme-ov-file#direct-connect-urls)
29+
feature.
30+
"""
31+
self._direct_connection = kwargs.pop('direct_connection', False)
32+
super().__init__(remote_server_addr, *args, **kwargs)
33+
34+
@property
35+
def direct_connection(self) -> bool:
36+
"""Return if [directConnect](https://github.com/appium/python-client?tab=readme-ov-file#direct-connect-urls)
37+
is enabled."""
38+
return self._direct_connection

appium/webdriver/webdriver.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
WebDriverException,
2323
)
2424
from selenium.webdriver.common.by import By
25-
from selenium.webdriver.remote.client_config import ClientConfig
2625
from selenium.webdriver.remote.command import Command as RemoteCommand
2726
from selenium.webdriver.remote.remote_connection import RemoteConnection
2827
from typing_extensions import Self
@@ -32,6 +31,7 @@
3231
from appium.webdriver.common.appiumby import AppiumBy
3332

3433
from .appium_connection import AppiumConnection
34+
from .client_config import AppiumClientConfig
3535
from .errorhandler import MobileErrorHandler
3636
from .extensions.action_helpers import ActionHelpers
3737
from .extensions.android.activities import Activities
@@ -174,6 +174,27 @@ def add_command(self) -> Tuple[str, str]:
174174
raise NotImplementedError()
175175

176176

177+
def _get_remote_connection_and_client_config(
178+
command_executor: Union[str, AppiumConnection], client_config: Optional[AppiumClientConfig] = None
179+
) -> tuple[AppiumConnection, Optional[AppiumClientConfig]]:
180+
"""Return the pair of command executor and client config.
181+
If the given command executor is a custom one, returned client config will
182+
be None since the custom command executor has its own client config already.
183+
The custom command executor's one will be prior than the given client config.
184+
"""
185+
if not isinstance(command_executor, str):
186+
# client config already defined in the custom command executor
187+
# will be prior than the given one.
188+
return (command_executor, None)
189+
190+
# command_executor is str
191+
192+
# Do not keep None to avoid warnings in Selenium
193+
# which can prevent with ClientConfig instance usage.
194+
new_client_config = AppiumClientConfig(remote_server_addr=command_executor) if client_config is None else client_config
195+
return (AppiumConnection(client_config=new_client_config), new_client_config)
196+
197+
177198
class WebDriver(
178199
webdriver.Remote,
179200
ActionHelpers,
@@ -202,28 +223,16 @@ class WebDriver(
202223
Sms,
203224
SystemBars,
204225
):
205-
def __init__( # noqa: PLR0913
226+
def __init__(
206227
self,
207-
command_executor: Union[str, AppiumConnection] = 'http://127.0.0.1:4444/wd/hub',
208-
keep_alive: bool = True,
209-
direct_connection: bool = True,
228+
command_executor: Union[str, AppiumConnection] = 'http://127.0.0.1:4723',
210229
extensions: Optional[List['WebDriver']] = None,
211-
strict_ssl: bool = True,
212230
options: Union[AppiumOptions, List[AppiumOptions], None] = None,
213-
client_config: Optional[ClientConfig] = None,
231+
client_config: Optional[AppiumClientConfig] = None,
214232
):
215-
if isinstance(command_executor, str):
216-
client_config = client_config or ClientConfig(
217-
remote_server_addr=command_executor, keep_alive=keep_alive, ignore_certificates=not strict_ssl
218-
)
219-
client_config.remote_server_addr = command_executor
220-
command_executor = AppiumConnection(client_config=client_config)
221-
elif isinstance(command_executor, AppiumConnection) and strict_ssl is False:
222-
logger.warning(
223-
"Please set 'ignore_certificates' in the given 'appium.webdriver.appium_connection.AppiumConnection' or "
224-
"'selenium.webdriver.remote.client_config.ClientConfig' instead. Ignoring."
225-
)
226-
233+
command_executor, client_config = _get_remote_connection_and_client_config(
234+
command_executor=command_executor, client_config=client_config
235+
)
227236
super().__init__(
228237
command_executor=command_executor,
229238
options=options,
@@ -232,13 +241,12 @@ def __init__( # noqa: PLR0913
232241
client_config=client_config,
233242
)
234243

235-
if hasattr(self, 'command_executor'):
236-
self._add_commands()
244+
self._add_commands()
237245

238246
self.error_handler = MobileErrorHandler()
239247

240-
if direct_connection:
241-
self._update_command_executor(keep_alive=keep_alive)
248+
if client_config and client_config.direct_connection:
249+
self._update_command_executor(keep_alive=client_config.keep_alive)
242250

243251
# add new method to the `find_by_*` pantheon
244252
By.IOS_PREDICATE = AppiumBy.IOS_PREDICATE

test/unit/webdriver/webdriver_test.py

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
from appium import webdriver
2222
from appium.options.android import UiAutomator2Options
2323
from appium.webdriver.appium_connection import AppiumConnection
24-
from appium.webdriver.webdriver import ExtensionBase, WebDriver
24+
from appium.webdriver.client_config import AppiumClientConfig
25+
from appium.webdriver.webdriver import ExtensionBase, WebDriver, _get_remote_connection_and_client_config
2526
from test.helpers.constants import SERVER_URL_BASE
2627
from test.unit.helper.test_helper import (
2728
android_w3c_driver,
@@ -124,10 +125,11 @@ def test_create_session_register_uridirect(self):
124125
'app': 'path/to/app',
125126
'automationName': 'UIAutomator2',
126127
}
128+
client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True)
127129
driver = webdriver.Remote(
128130
SERVER_URL_BASE,
129131
options=UiAutomator2Options().load_capabilities(desired_caps),
130-
direct_connection=True,
132+
client_config=client_config,
131133
)
132134

133135
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):
164166
'app': 'path/to/app',
165167
'automationName': 'UIAutomator2',
166168
}
169+
client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True)
167170
driver = webdriver.Remote(
168-
SERVER_URL_BASE,
169-
options=UiAutomator2Options().load_capabilities(desired_caps),
170-
direct_connection=True,
171+
SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps), client_config=client_config
171172
)
172173

173174
assert SERVER_URL_BASE == driver.command_executor._client_config.remote_server_addr
174175
assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts
175176
assert isinstance(driver.command_executor, AppiumConnection)
176177

178+
@httpretty.activate
179+
def test_create_session_remote_server_addr_treatment_with_appiumclientconfig(self):
180+
# remote server add in AppiumRemoteCong will be prior than the string of 'command_executor'
181+
# as same as Selenium behavior.
182+
httpretty.register_uri(
183+
httpretty.POST,
184+
f'{SERVER_URL_BASE}/session',
185+
body=json.dumps(
186+
{
187+
'sessionId': 'session-id',
188+
'capabilities': {
189+
'deviceName': 'Android Emulator',
190+
},
191+
}
192+
),
193+
)
194+
195+
httpretty.register_uri(
196+
httpretty.GET,
197+
f'{SERVER_URL_BASE}/session/session-id/contexts',
198+
body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}),
199+
)
200+
201+
desired_caps = {
202+
'platformName': 'Android',
203+
'deviceName': 'Android Emulator',
204+
'app': 'path/to/app',
205+
'automationName': 'UIAutomator2',
206+
}
207+
client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True)
208+
driver = webdriver.Remote(
209+
'http://localhost:8080/something/path',
210+
options=UiAutomator2Options().load_capabilities(desired_caps),
211+
client_config=client_config,
212+
)
213+
214+
assert SERVER_URL_BASE == driver.command_executor._client_config.remote_server_addr
215+
assert isinstance(driver.command_executor, AppiumConnection)
216+
177217
@httpretty.activate
178218
def test_get_events(self):
179219
driver = ios_w3c_driver()
@@ -380,21 +420,54 @@ def test_extention_command_check(self):
380420
'script': 'mobile: startActivity',
381421
} == get_httpretty_request_body(httpretty.last_request())
382422

423+
def test_get_client_config_and_connection_with_empty_config(self):
424+
command_executor, client_config = _get_remote_connection_and_client_config(
425+
command_executor='http://127.0.0.1:4723', client_config=None
426+
)
427+
428+
assert isinstance(command_executor, AppiumConnection)
429+
assert command_executor._client_config == client_config
430+
assert isinstance(client_config, AppiumClientConfig)
431+
assert client_config.remote_server_addr == 'http://127.0.0.1:4723'
432+
433+
def test_get_client_config_and_connection(self):
434+
command_executor, client_config = _get_remote_connection_and_client_config(
435+
command_executor='http://127.0.0.1:4723',
436+
client_config=AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723/wd/hub'),
437+
)
438+
439+
assert isinstance(command_executor, AppiumConnection)
440+
# the client config in the command_executor is the given client config.
441+
assert command_executor._client_config == client_config
442+
assert isinstance(client_config, AppiumClientConfig)
443+
assert client_config.remote_server_addr == 'http://127.0.0.1:4723/wd/hub'
444+
445+
def test_get_client_config_and_connection_custom_appium_connection(self):
446+
c_config = AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723')
447+
appium_connection = AppiumConnection(client_config=c_config)
448+
449+
command_executor, client_config = _get_remote_connection_and_client_config(
450+
command_executor=appium_connection, client_config=AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723')
451+
)
452+
453+
assert isinstance(command_executor, AppiumConnection)
454+
# client config already defined in the command_executor will be used.
455+
assert command_executor._client_config != client_config
456+
assert client_config is None
457+
383458

384459
class SubWebDriver(WebDriver):
385-
def __init__(self, command_executor, direct_connection=False, options=None):
460+
def __init__(self, command_executor, options=None):
386461
super().__init__(
387462
command_executor=command_executor,
388-
direct_connection=direct_connection,
389463
options=options,
390464
)
391465

392466

393467
class SubSubWebDriver(SubWebDriver):
394-
def __init__(self, command_executor, direct_connection=False, options=None):
468+
def __init__(self, command_executor, options=None):
395469
super().__init__(
396470
command_executor=command_executor,
397-
direct_connection=direct_connection,
398471
options=options,
399472
)
400473

0 commit comments

Comments
 (0)