From 1ce21d16e2690f019c92e611e8656e61ec9dbee1 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 22 Jun 2021 01:39:20 -0700 Subject: [PATCH 01/12] chore: add placeholder --- appium/webdriver/webdriver.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 393684841..d20b57db8 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -151,6 +151,7 @@ def __init__( proxy: str = None, keep_alive: bool = True, direct_connection: bool = False, + extensions: List[str] = [], ): super().__init__( @@ -176,6 +177,10 @@ def __init__( By.IMAGE = MobileBy.IMAGE By.CUSTOM = MobileBy.CUSTOM + for extention in extensions: + # TODO: add new methods + pass + def _update_command_executor(self, keep_alive: bool) -> None: """Update command executor following directConnect feature""" direct_protocol = 'directConnectProtocol' From 69df40f48c337b8954899e2ea89d4669320bc81e Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 23 Jun 2021 00:07:07 -0700 Subject: [PATCH 02/12] move to extention way --- appium/webdriver/webdriver.py | 123 ++++++-------------------- test/pytest.ini | 2 +- test/unit/helper/test_helper.py | 34 +++++++ test/unit/webdriver/webdriver_test.py | 83 +++++++++-------- 4 files changed, 109 insertions(+), 133 deletions(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index d20b57db8..60df447d6 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -15,7 +15,7 @@ # pylint: disable=too-many-lines,too-many-public-methods,too-many-statements,no-self-use import copy -from typing import Any, Dict, List, Optional, TypeVar, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union from selenium.common.exceptions import InvalidArgumentException from selenium.webdriver.common.by import By @@ -114,6 +114,23 @@ def _make_w3c_caps(caps: Dict) -> Dict[str, List[Dict[str, Any]]]: T = TypeVar('T', bound='WebDriver') +class ExtensionBase: + def __init__(self, execute: Callable): + self._execute = execute + + def execute(self, parameters: Any) -> Any: + return self._execute(self.method_name(), parameters) + + def method_name(self) -> str: + raise NotImplementedError('Please define a method name') + + def command_wrapper(self) -> None: + raise NotImplementedError('Please define exact call') + + def custom_command(self) -> Tuple[str, str]: + raise NotImplementedError('Please define exact call') + + class WebDriver( AppiumSearchContext, ActionHelpers, @@ -151,7 +168,7 @@ def __init__( proxy: str = None, keep_alive: bool = True, direct_connection: bool = False, - extensions: List[str] = [], + extensions: List[T] = [], ): super().__init__( @@ -177,9 +194,15 @@ def __init__( By.IMAGE = MobileBy.IMAGE By.CUSTOM = MobileBy.CUSTOM - for extention in extensions: - # TODO: add new methods - pass + for extension in extensions: + instance = extension(self.execute) + if hasattr(WebDriver, instance.method_name()): + logger.warn( + '{} is alreadt defined. The new one is overriding the old one.'.format(instance.method_name()) + ) + setattr(WebDriver, instance.method_name(), instance.command_wrapper) + method, url_cmd = instance.custom_command() + self.command_executor._commands[instance.method_name()] = (method.upper(), url_cmd) # type: ignore def _update_command_executor(self, keep_alive: bool) -> None: """Update command executor following directConnect feature""" @@ -362,96 +385,6 @@ def switch_to(self) -> MobileSwitchTo: return MobileSwitchTo(self) - def add_command(self, method: CommandMethod, url: str, name: str) -> T: - """Add a custom command as 'name' - - Args: - method: The method of HTTP request. Available methods are CommandMethod. - url: The url is URL template as https://www.w3.org/TR/webdriver/#endpoints. - `$sessionId` is a placeholder of current session id. - Other placeholders can be specified with `$` prefix like `$id`. - Then, `{'id': 'some id'}` argument in `execute_custom_command` replaces - the `$id` with the value, `some id`, in the request. - name: The name of a command to call in `execute_custom_command`. - - Returns: - `appium.webdriver.webdriver.WebDriver`: Self instance - - Raises: - ValueError - - Examples: - Define 'test_command' as GET and 'session/$sessionId/path/to/custom/url' - - >>> from appium.webdriver.command_method import CommandMethod - >>> driver.add_command( - method=CommandMethod.GET, - url='session/$sessionId/path/to/custom/url', - name='test_command' - ) - - """ - if name in self.command_executor._commands: - raise ValueError("{} is already defined".format(name)) - - if not isinstance(method, CommandMethod): - raise ValueError( - "'{}' is invalid. Valid method should be one of '{}'.".format(method, CommandMethod.__name__) - ) - - self.command_executor._commands[name] = (method.value, url) - return self - - def execute_custom_command(self, name: str, args: Dict = {}) -> Any: - """Execute a custom command defined as 'name' with args command. - - Args: - name: The name of command defined by `add_command`. - args: The argument as this command body - - Returns: - 'value' JSON object field in the response body. - - Raises: - ValueError, selenium.common.exceptions.WebDriverException - - Examples: - Calls 'test_command' command without arguments. - - >>> from appium.webdriver.command_method import CommandMethod - >>> driver.add_command( - method=CommandMethod.GET, - url='session/$sessionId/path/to/custom/url', - name='test_command' - ) - >>> result = driver.execute_custom_command('test_command') - - Calls 'test_command' command with arguments. - - >>> from appium.webdriver.command_method import CommandMethod - >>> driver.add_command( - method=CommandMethod.POST, - url='session/$sessionId/path/to/custom/url', - name='test_command' - ) - >>> result = driver.execute_custom_command('test_command', {'dummy': 'test argument'}) - - Calls 'test_command' command with arguments, and the path has 'element id'. - The '$id' will be 'element id' by 'id' key in the given argument. - - >>> from appium.webdriver.command_method import CommandMethod - >>> driver.add_command( - method=CommandMethod.POST, - url='session/$sessionId/path/to/$id/url', - name='test_command' - ) - >>> result = driver.execute_custom_command('test_command', {'id': 'element id', 'dummy': 'test argument'}) - - """ - if name not in self.command_executor._commands: - raise ValueError("No {} custom command. Please add it by 'add_command'".format(name)) - return self.execute(name, args)['value'] - # pylint: disable=protected-access def _addCommands(self) -> None: diff --git a/test/pytest.ini b/test/pytest.ini index 0c95eb57c..12cc5a459 100644 --- a/test/pytest.ini +++ b/test/pytest.ini @@ -1,2 +1,2 @@ [pytest] -addopts = -ra -q --cov=appium + diff --git a/test/unit/helper/test_helper.py b/test/unit/helper/test_helper.py index 44f8fbb96..9766e1b76 100644 --- a/test/unit/helper/test_helper.py +++ b/test/unit/helper/test_helper.py @@ -119,6 +119,40 @@ def ios_w3c_driver() -> 'WebDriver': return driver +def ios_w3c_driver_with_extensions(extensions) -> 'WebDriver': + """Return a W3C driver which is generated by a mock response for iOS + + Returns: + `webdriver.webdriver.WebDriver`: An instance of WebDriver + """ + + response_body_json = json.dumps( + { + 'value': { + 'sessionId': '1234567890', + 'capabilities': { + 'device': 'iphone', + 'browserName': 'UICatalog', + 'sdkVersion': '11.4', + 'CFBundleIdentifier': 'com.example.apple-samplecode.UICatalog', + }, + } + } + ) + + httpretty.register_uri(httpretty.POST, appium_command('/session'), body=response_body_json) + + desired_caps = { + 'platformName': 'iOS', + 'deviceName': 'iPhone Simulator', + 'app': 'path/to/app', + 'automationName': 'XCUITest', + } + + driver = webdriver.Remote(SERVER_URL_BASE, desired_caps, extensions=extensions) + return driver + + def get_httpretty_request_body(request: 'HTTPrettyRequestEmpty') -> Dict[str, Any]: """Returns utf-8 decoded request body""" return json.loads(request.body.decode('utf-8')) diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index df7d5e88f..9c2bccff8 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -21,8 +21,14 @@ from appium import version as appium_version from appium import webdriver from appium.webdriver.command_method import CommandMethod -from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver +from appium.webdriver.webdriver import ExtensionBase, WebDriver +from test.unit.helper.test_helper import ( + android_w3c_driver, + appium_command, + get_httpretty_request_body, + ios_w3c_driver, + ios_w3c_driver_with_extensions, +) class TestWebDriverWebDriver(object): @@ -254,70 +260,73 @@ def exceptionCallback(request, uri, headers): @httpretty.activate def test_add_command(self): - driver = ios_w3c_driver() + class CustomURLCommand(ExtensionBase): + def method_name(self): + return 'test_command' + + def command_wrapper(self): + return self.execute({})['value'] + + def custom_command(self): + return ('get', 'session/$sessionId/path/to/custom/url') + + driver = ios_w3c_driver_with_extensions([CustomURLCommand]) httpretty.register_uri( httpretty.GET, appium_command('session/1234567890/path/to/custom/url'), body=json.dumps({'value': {}}), ) - driver.add_command(method=CommandMethod.GET, url='session/$sessionId/path/to/custom/url', name='test_command') - result = driver.execute_custom_command('test_command') + result = driver.test_command() assert result == {} @httpretty.activate def test_add_command_body(self): - driver = ios_w3c_driver() + class CustomURLCommand(ExtensionBase): + def method_name(self): + return 'test_command' + + def command_wrapper(self, argument): + return self.execute(argument)['value'] + + def custom_command(self): + return ('post', 'session/$sessionId/path/to/custom/url') + + driver = ios_w3c_driver_with_extensions([CustomURLCommand]) httpretty.register_uri( httpretty.POST, appium_command('session/1234567890/path/to/custom/url'), body=json.dumps({'value': {}}), ) - driver.add_command(method=CommandMethod.POST, url='session/$sessionId/path/to/custom/url', name='test_command') - result = driver.execute_custom_command('test_command', {'dummy': 'test argument'}) + result = driver.test_command({'dummy': 'test argument'}) assert result == {} + print(httpretty.last_request()) d = get_httpretty_request_body(httpretty.last_request()) + assert d['dummy'] == 'test argument' @httpretty.activate def test_add_command_with_element_id(self): - driver = ios_w3c_driver() + class CustomURLCommand(ExtensionBase): + def method_name(self): + return 'test_command' + + def command_wrapper(self, element_id): + return self.execute({'id': element_id})['value'] + + def custom_command(self): + return ('GET', 'session/$sessionId/path/to/custom/$id/url') + + driver = ios_w3c_driver_with_extensions([CustomURLCommand]) httpretty.register_uri( httpretty.GET, appium_command('session/1234567890/path/to/custom/element_id/url'), body=json.dumps({'value': {}}), ) - driver.add_command( - method=CommandMethod.GET, url='session/$sessionId/path/to/custom/$id/url', name='test_command' - ) - result = driver.execute_custom_command('test_command', {'id': 'element_id'}) + result = driver.test_command('element_id') assert result == {} - @httpretty.activate - def test_add_command_already_defined(self): - driver = ios_w3c_driver() - driver.add_command(method=CommandMethod.GET, url='session/$sessionId/path/to/custom/url', name='test_command') - with pytest.raises(ValueError): - driver.add_command( - method=CommandMethod.GET, url='session/$sessionId/path/to/custom/url', name='test_command' - ) - - @httpretty.activate - def test_execute_custom_command(self): - driver = ios_w3c_driver() - driver.add_command(method=CommandMethod.GET, url='session/$sessionId/path/to/custom/url', name='test_command') - with pytest.raises(ValueError): - driver.add_command( - method=CommandMethod.GET, url='session/$sessionId/path/to/custom/url', name='test_command' - ) - - @httpretty.activate - def test_invalid_method(self): - driver = ios_w3c_driver() - with pytest.raises(ValueError): - driver.add_command(method='error', url='session/$sessionId/path/to/custom/url', name='test_command') - class SubWebDriver(WebDriver): def __init__(self, command_executor, desired_capabilities, direct_connection=False): From db70cf0ac29467feead708d0d327d5ebe7b2d269 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 23 Jun 2021 00:08:20 -0700 Subject: [PATCH 03/12] revert pytest --- test/pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pytest.ini b/test/pytest.ini index 12cc5a459..0c95eb57c 100644 --- a/test/pytest.ini +++ b/test/pytest.ini @@ -1,2 +1,2 @@ [pytest] - +addopts = -ra -q --cov=appium From 2731aaee2b315a337b4203651b6966df5087723f Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 23 Jun 2021 00:47:03 -0700 Subject: [PATCH 04/12] add todo --- appium/webdriver/webdriver.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 60df447d6..36c3168e4 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -115,6 +115,13 @@ def _make_w3c_caps(caps: Dict) -> Dict[str, List[Dict[str, Any]]]: class ExtensionBase: + """ + Used to define an extension command. + + TODO: + add examples + """ + def __init__(self, execute: Callable): self._execute = execute @@ -122,13 +129,26 @@ def execute(self, parameters: Any) -> Any: return self._execute(self.method_name(), parameters) def method_name(self) -> str: - raise NotImplementedError('Please define a method name') + """ + Expected to return a method name. + This name will be able to call as a driver method. + + Returns: + 'str' The method name. + """ + raise NotImplementedError() - def command_wrapper(self) -> None: - raise NotImplementedError('Please define exact call') + def command_wrapper(self) -> Any: + """ + Expected to do the actual command as `method_name`. + """ + raise NotImplementedError() def custom_command(self) -> Tuple[str, str]: - raise NotImplementedError('Please define exact call') + """ + Expected to define the pair of HTTP method and its URL. + """ + raise NotImplementedError() class WebDriver( From 18a9b6d30355bef9c0502174a458eef11ed0b104 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sat, 3 Jul 2021 13:33:02 -0700 Subject: [PATCH 05/12] call method_name instead of wrapper --- appium/webdriver/webdriver.py | 29 +++++++++++++++------------ test/unit/webdriver/webdriver_test.py | 12 ++++++----- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 36c3168e4..e76bd7300 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -15,6 +15,7 @@ # pylint: disable=too-many-lines,too-many-public-methods,too-many-statements,no-self-use import copy +import types from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union from selenium.common.exceptions import InvalidArgumentException @@ -125,25 +126,19 @@ class ExtensionBase: def __init__(self, execute: Callable): self._execute = execute - def execute(self, parameters: Any) -> Any: + def execute(self, parameters: Any = {}) -> Any: return self._execute(self.method_name(), parameters) def method_name(self) -> str: """ Expected to return a method name. - This name will be able to call as a driver method. + This name will be available as a driver method. Returns: 'str' The method name. """ raise NotImplementedError() - def command_wrapper(self) -> Any: - """ - Expected to do the actual command as `method_name`. - """ - raise NotImplementedError() - def custom_command(self) -> Tuple[str, str]: """ Expected to define the pair of HTTP method and its URL. @@ -214,16 +209,24 @@ def __init__( By.IMAGE = MobileBy.IMAGE By.CUSTOM = MobileBy.CUSTOM - for extension in extensions: + self._extensions = extensions + for extension in self._extensions: instance = extension(self.execute) if hasattr(WebDriver, instance.method_name()): - logger.warn( - '{} is alreadt defined. The new one is overriding the old one.'.format(instance.method_name()) - ) - setattr(WebDriver, instance.method_name(), instance.command_wrapper) + raise ValueError(f'{instance.method_name()} is already defined.') + + # add a new method named 'instance.method_name()' and call it + setattr(WebDriver, instance.method_name(), getattr(instance, instance.method_name())) method, url_cmd = instance.custom_command() self.command_executor._commands[instance.method_name()] = (method.upper(), url_cmd) # type: ignore + def delete_extensions(self) -> None: + """Delete extensions added in the class with 'setattr'""" + for extension in self._extensions: + instance = extension(self.execute) + if hasattr(WebDriver, instance.method_name()): + delattr(WebDriver, instance.method_name()) + def _update_command_executor(self, keep_alive: bool) -> None: """Update command executor following directConnect feature""" direct_protocol = 'directConnectProtocol' diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index 9c2bccff8..98cb3f3e8 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -264,8 +264,8 @@ class CustomURLCommand(ExtensionBase): def method_name(self): return 'test_command' - def command_wrapper(self): - return self.execute({})['value'] + def test_command(self): + return self.execute()['value'] def custom_command(self): return ('get', 'session/$sessionId/path/to/custom/url') @@ -279,6 +279,7 @@ def custom_command(self): result = driver.test_command() assert result == {} + driver.delete_extensions() @httpretty.activate def test_add_command_body(self): @@ -286,7 +287,7 @@ class CustomURLCommand(ExtensionBase): def method_name(self): return 'test_command' - def command_wrapper(self, argument): + def test_command(self, argument): return self.execute(argument)['value'] def custom_command(self): @@ -301,10 +302,10 @@ def custom_command(self): result = driver.test_command({'dummy': 'test argument'}) assert result == {} - print(httpretty.last_request()) d = get_httpretty_request_body(httpretty.last_request()) assert d['dummy'] == 'test argument' + driver.delete_extensions() @httpretty.activate def test_add_command_with_element_id(self): @@ -312,7 +313,7 @@ class CustomURLCommand(ExtensionBase): def method_name(self): return 'test_command' - def command_wrapper(self, element_id): + def test_command(self, element_id): return self.execute({'id': element_id})['value'] def custom_command(self): @@ -326,6 +327,7 @@ def custom_command(self): ) result = driver.test_command('element_id') assert result == {} + driver.delete_extensions() class SubWebDriver(WebDriver): From 14df3c62729f302a6736a003b12d6e50121a42ee Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sat, 3 Jul 2021 13:33:37 -0700 Subject: [PATCH 06/12] remove types --- appium/webdriver/webdriver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index e76bd7300..65ce237c5 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -15,7 +15,6 @@ # pylint: disable=too-many-lines,too-many-public-methods,too-many-statements,no-self-use import copy -import types from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union from selenium.common.exceptions import InvalidArgumentException From 675c0533d65b8045e5d91789ded8b643f52105cf Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sun, 4 Jul 2021 10:00:36 -0700 Subject: [PATCH 07/12] rename a method --- appium/webdriver/webdriver.py | 4 ++-- test/unit/webdriver/webdriver_test.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 65ce237c5..9c8dadc6d 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -138,7 +138,7 @@ def method_name(self) -> str: """ raise NotImplementedError() - def custom_command(self) -> Tuple[str, str]: + def add_command(self) -> Tuple[str, str]: """ Expected to define the pair of HTTP method and its URL. """ @@ -216,7 +216,7 @@ def __init__( # add a new method named 'instance.method_name()' and call it setattr(WebDriver, instance.method_name(), getattr(instance, instance.method_name())) - method, url_cmd = instance.custom_command() + method, url_cmd = instance.add_command() self.command_executor._commands[instance.method_name()] = (method.upper(), url_cmd) # type: ignore def delete_extensions(self) -> None: diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index 98cb3f3e8..d9cfea29b 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -267,7 +267,7 @@ def method_name(self): def test_command(self): return self.execute()['value'] - def custom_command(self): + def add_command(self): return ('get', 'session/$sessionId/path/to/custom/url') driver = ios_w3c_driver_with_extensions([CustomURLCommand]) @@ -290,7 +290,7 @@ def method_name(self): def test_command(self, argument): return self.execute(argument)['value'] - def custom_command(self): + def add_command(self): return ('post', 'session/$sessionId/path/to/custom/url') driver = ios_w3c_driver_with_extensions([CustomURLCommand]) @@ -316,7 +316,7 @@ def method_name(self): def test_command(self, element_id): return self.execute({'id': element_id})['value'] - def custom_command(self): + def add_command(self): return ('GET', 'session/$sessionId/path/to/custom/$id/url') driver = ios_w3c_driver_with_extensions([CustomURLCommand]) From 0a42bd5d52a992ca2a72559edfef74b13d395de8 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 5 Jul 2021 11:19:41 -0700 Subject: [PATCH 08/12] add examples --- README.md | 3 +- appium/webdriver/webdriver.py | 78 +++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dde691c0a..0b2f8d1bd 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,8 @@ self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, dir ## Documentation -https://appium.github.io/python-client-sphinx/ is detailed documentation +- https://appium.github.io/python-client-sphinx/ is detailed documentation +- [functional tests](test/functional) also may help to see concrete examples. ## Development diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 9c8dadc6d..5ea9a99e8 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -116,13 +116,83 @@ def _make_w3c_caps(caps: Dict) -> Dict[str, List[Dict[str, Any]]]: class ExtensionBase: """ - Used to define an extension command. + Used to define an extension command as driver's methods. - TODO: - add examples + Example: + When you want to add `example_command` which calls a get request to + `session/$sessionId/path/to/your/custom/url`. + + 1. Defines an extension as a subclass of `ExtensionBase` + class CustomYourCommand(ExtensionBase): + def method_name(self): + return 'custom_method_name' + + # Define a method with the name of `method_name` + def custom_method_name(self): + # Generally the response of Appium follows `{ 'value': { data } }` + # format. + return self.execute()['value'] + + # Used to register the command pair as "Appium command" in this driver. + def add_command(self): + return ('GET', 'session/$sessionId/path/to/your/custom/url') + + 2. Creates a session with the extension. + # Appium capabilities + desired_caps = { ... } + driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, + extensions=[CustomYourCommand]) + + 3. Calls the custom command + # Then, the driver calls a get request against + # `session/$sessionId/path/to/your/custom/url`. `$sessionId` will be + # replaced properly in the driver. Then, the method returns + # the `value` part of the response. + driver.custom_method_name() + + + You can give arbitrary arguments for the command like the below. + + class CustomYourCommand(ExtensionBase): + def method_name(self): + return 'custom_method_name' + + def test_command(self, argument): + return self.execute(argument)['value'] + + def add_command(self): + return ('post', 'session/$sessionId/path/to/your/custom/url') + + driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, + extensions=[CustomYourCommand]) + + # Then, the driver sends a post request to `session/$sessionId/path/to/your/custom/url` + # with `{'dummy_arg': 'as a value'}` JSON body. + driver.custom_method_name({'dummy_arg': 'as a value'}) + + + When you customize the URL dinamically with element id. + + class CustomURLCommand(ExtensionBase): + def method_name(self): + return 'custom_method_name' + + def custom_method_name(self, element_id): + return self.execute({'id': element_id})['value'] + + def add_command(self): + return ('GET', 'session/$sessionId/path/to/your/custom/$id/url') + + driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, + extensions=[CustomYourCommand]) + element = driver.find_element_by_accessibility_id('id') + + # Then, the driver calls a get request to `session/$sessionId/path/to/your/custom/$id/url` + # with replacing the `$id` with the given `element.id` + driver.custom_method_name(element.id) """ - def __init__(self, execute: Callable): + def __init__(self, execute: Callable[[str, Dict], Dict[str, Any]]): self._execute = execute def execute(self, parameters: Any = {}) -> Any: From 08eb385ecfce967a673bf0a014121e3ce23859b3 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sun, 11 Jul 2021 15:14:57 -0700 Subject: [PATCH 09/12] add types-python-dateutil as error message --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index b24c835b8..278a6fe95 100644 --- a/Pipfile +++ b/Pipfile @@ -18,6 +18,7 @@ tox = "~=3.23" httpretty = "~=1.0" python-dateutil = "~=2.8" +types-python-dateutil = "~=0.1" mock = "~=4.0" pylint = "~=2.8" From 5208468360321b8461b23eb7c23211fd044dbff8 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sun, 11 Jul 2021 15:18:22 -0700 Subject: [PATCH 10/12] add example more --- appium/webdriver/webdriver.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 5ea9a99e8..1f549f160 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -150,6 +150,12 @@ def add_command(self): # the `value` part of the response. driver.custom_method_name() + 4. Remove added commands (if needed) + # New commands are added by `setattr`. They remain in the module, + # so you should explicitly delete them to define the same name method + # with different arguments or process in the method. + driver.delete_extensions() + You can give arbitrary arguments for the command like the below. From 8bb587f03004da870ccff82b38d048f22895f85f Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 13 Jul 2021 22:56:56 -0700 Subject: [PATCH 11/12] tweak naming --- appium/webdriver/webdriver.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 1f549f160..70555d202 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -123,7 +123,7 @@ class ExtensionBase: `session/$sessionId/path/to/your/custom/url`. 1. Defines an extension as a subclass of `ExtensionBase` - class CustomYourCommand(ExtensionBase): + class YourCustomCommand(ExtensionBase): def method_name(self): return 'custom_method_name' @@ -141,7 +141,7 @@ def add_command(self): # Appium capabilities desired_caps = { ... } driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, - extensions=[CustomYourCommand]) + extensions=[YourCustomCommand]) 3. Calls the custom command # Then, the driver calls a get request against @@ -159,7 +159,7 @@ def add_command(self): You can give arbitrary arguments for the command like the below. - class CustomYourCommand(ExtensionBase): + class YourCustomCommand(ExtensionBase): def method_name(self): return 'custom_method_name' @@ -170,7 +170,7 @@ def add_command(self): return ('post', 'session/$sessionId/path/to/your/custom/url') driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, - extensions=[CustomYourCommand]) + extensions=[YourCustomCommand]) # Then, the driver sends a post request to `session/$sessionId/path/to/your/custom/url` # with `{'dummy_arg': 'as a value'}` JSON body. @@ -190,7 +190,7 @@ def add_command(self): return ('GET', 'session/$sessionId/path/to/your/custom/$id/url') driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, - extensions=[CustomYourCommand]) + extensions=[YourCustomCommand]) element = driver.find_element_by_accessibility_id('id') # Then, the driver calls a get request to `session/$sessionId/path/to/your/custom/$id/url` @@ -287,20 +287,22 @@ def __init__( self._extensions = extensions for extension in self._extensions: instance = extension(self.execute) - if hasattr(WebDriver, instance.method_name()): - raise ValueError(f'{instance.method_name()} is already defined.') + method_name = instance.method_name() + if hasattr(WebDriver, method_name): + raise ValueError(f'{method_name} is already defined.') # add a new method named 'instance.method_name()' and call it - setattr(WebDriver, instance.method_name(), getattr(instance, instance.method_name())) + setattr(WebDriver, method_name, getattr(instance, method_name)) method, url_cmd = instance.add_command() - self.command_executor._commands[instance.method_name()] = (method.upper(), url_cmd) # type: ignore + self.command_executor._commands[method_name] = (method.upper(), url_cmd) # type: ignore def delete_extensions(self) -> None: """Delete extensions added in the class with 'setattr'""" for extension in self._extensions: instance = extension(self.execute) - if hasattr(WebDriver, instance.method_name()): - delattr(WebDriver, instance.method_name()) + method_name = instance.method_name() + if hasattr(WebDriver, method_name): + delattr(WebDriver, method_name) def _update_command_executor(self, keep_alive: bool) -> None: """Update command executor following directConnect feature""" From b5615ff92632659f3e31a59b106f3b2896d2ec49 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 13 Jul 2021 23:07:42 -0700 Subject: [PATCH 12/12] Explicit Dict --- appium/webdriver/webdriver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 70555d202..733054364 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -201,8 +201,11 @@ def add_command(self): def __init__(self, execute: Callable[[str, Dict], Dict[str, Any]]): self._execute = execute - def execute(self, parameters: Any = {}) -> Any: - return self._execute(self.method_name(), parameters) + def execute(self, parameters: Dict[str, Any] = None) -> Any: + param = {} + if parameters: + param = parameters + return self._execute(self.method_name(), param) def method_name(self) -> str: """