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: define AppiumClientConfig #1070

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
27cb0ca
initial implementation
KazuCocoa Nov 28, 2024
7823cd7
remove
KazuCocoa Nov 28, 2024
ce1f243
add appium/webdriver/client_config.py
KazuCocoa Nov 28, 2024
d31812d
Merge branch 'master' into use-client-config
KazuCocoa Nov 29, 2024
c658460
Merge branch 'master' into use-client-config
KazuCocoa Dec 4, 2024
09e0c59
remove duplicated args
KazuCocoa Dec 4, 2024
dd19cff
remove duplicated args
KazuCocoa Dec 4, 2024
3c1c02f
fix typo
KazuCocoa Dec 4, 2024
0fbbb8e
Merge branch 'master' into use-client-config
KazuCocoa Dec 30, 2024
69f086e
Merge branch 'master' into use-client-config
KazuCocoa Jan 12, 2025
bf0c96a
Merge branch 'master' into use-client-config
KazuCocoa Feb 23, 2025
2aabd72
add file_detector and remove redundant config
KazuCocoa Feb 23, 2025
9569035
add test to check remote_server_addr priority
KazuCocoa Feb 23, 2025
2bdbea8
remove PLR0913, address http://127.0.0.1:4723
KazuCocoa Mar 10, 2025
b7fdaa4
update the readme
KazuCocoa Mar 10, 2025
d52e243
add comment
KazuCocoa Mar 10, 2025
b06ed8e
fix typo
KazuCocoa Mar 10, 2025
671aa78
Merge branch 'master' into use-client-config
KazuCocoa Mar 10, 2025
64a6553
extract
KazuCocoa Mar 11, 2025
2b19e2e
extract and followed the selenium
KazuCocoa Mar 11, 2025
9262c78
Merge branch 'use-client-config' of github.com:appium/python-client i…
KazuCocoa Mar 11, 2025
f56de0d
add comment
KazuCocoa Mar 11, 2025
2a33f0d
Merge branch 'master' into use-client-config
KazuCocoa Mar 12, 2025
299f283
Update webdriver.py
KazuCocoa Mar 16, 2025
5bf6cc6
Apply suggestions from code review
KazuCocoa Mar 16, 2025
72b2e42
update readme
KazuCocoa Mar 17, 2025
8139b41
remove redundant command_executor check
KazuCocoa Mar 17, 2025
7fc6113
Merge branch 'use-client-config' of github.com:appium/python-client i…
KazuCocoa Mar 17, 2025
25b5af0
modify a bit
KazuCocoa Mar 17, 2025
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
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,36 @@ 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 affecs only for a user who speficies `keep_alive`, `direct_connection` and `strict_ssl` arguments for `webdriver.Remote`:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change affects only user who specify ...

- Please use `AppiumClientConfig` as `client_config` arguemnt as below:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo arguemnt

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as below -> similar to how it is specified below

```python
SERVER_URL_BASE = 'http://127.0.0.1:4723'
# before
driver = webdriver.Remote(
SERVER_URL_BASE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still prefer to keep the url positional argument
I believe most of the clients have the remote client arguments defined as (url, options), so this change would require all of them to change the code, which is a major inconvenience.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upd: I can observe the below code supports such feature, so we just need to mention that in documents

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

The ClinetConfig requires remote_server_addr https://github.com/SeleniumHQ/selenium/blob/dbf3daef5519b0a35a8408510854ec7766455c66/py/selenium/webdriver/remote/client_config.py#L80C9-L80C27 so current example is to reduce adding remote_server_addr to both, but actually current one will confuse users.

I'll add this.

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. Then, the `remote_server_addr` in `AppiumClientConfig` will prior than the `url` specified via `webdriver.Remote`
Copy link
Contributor

@mykola-mokhnach mykola-mokhnach Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, the ... -> 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
Expand Down Expand Up @@ -274,6 +304,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
Expand All @@ -283,11 +314,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
)
```

Expand Down
38 changes: 38 additions & 0 deletions appium/webdriver/client_config.py
Original file line number Diff line number Diff line change
@@ -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
54 changes: 31 additions & 23 deletions appium/webdriver/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
if client_config is None:
# Do not keep None to avoid warnings in Selenium
# which can prevent with ClientConfig instance usage.
client_config = AppiumClientConfig(remote_server_addr=command_executor)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be possible to do the same without reassigning method arguments?

return (AppiumConnection(client_config=client_config), client_config)


class WebDriver(
webdriver.Remote,
ActionHelpers,
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
91 changes: 82 additions & 9 deletions test/unit/webdriver/webdriver_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
)

Expand Down