diff --git a/Pipfile b/Pipfile index aca7dc7f..d9795ba9 100644 --- a/Pipfile +++ b/Pipfile @@ -24,3 +24,5 @@ mock = "~=3.0" pylint = "~=2.4" astroid = "~=2.3" isort = "~=4.3" + +mypy = "==0.761" diff --git a/README.md b/README.md index 9cb50efc..50834ce6 100644 --- a/README.md +++ b/README.md @@ -45,14 +45,18 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz). - Style Guide: https://www.python.org/dev/peps/pep-0008/ - `autopep8` helps to format code automatically - ``` + ```shell $ python -m autopep8 -r --global-config .config-pep8 -i . ``` - `isort` helps to order imports automatically - ``` + ```shell $ python -m isort -rc . ``` - When you use newly 3rd party modules, add it to [.isort.cfg](.isort.cfg) to keep import order correct + - `mypy` helps to check explicit type declarations + ```shell + $ python -m mypy appium + ``` - Docstring style: Google Style - Refer [link](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) - You can customise `CHANGELOG.rst` with commit messages following [.gitchangelog.rc](.gitchangelog.rc) diff --git a/appium/common/helper.py b/appium/common/helper.py index b694b9fa..d34417fb 100644 --- a/appium/common/helper.py +++ b/appium/common/helper.py @@ -12,44 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import OrderedDict +from typing import Dict from appium import version as appium_version -def appium_bytes(value, encoding): - """Return a bytes-like object - - Has _appium_ prefix to avoid overriding built-in bytes. - - Args: - value (str): A value to convert - encoding (str): A encoding which will convert to - - Returns: - str: A bytes-like object - """ - - try: - return bytes(value, encoding) # Python 3 - except TypeError: - return value # Python 2 - - -def extract_const_attributes(cls): +def extract_const_attributes(cls: type) -> Dict: """Return dict with constants attributes and values in the class(e.g. {'VAL1': 1, 'VAL2': 2}) Args: cls (type): Class to be extracted constants Returns: - OrderedDict: dict with constants attributes and values in the class + dict: dict with constants attributes and values in the class """ - return OrderedDict( - [(attr, value) for attr, value in vars(cls).items() if not callable(getattr(cls, attr)) and attr.isupper()]) + return dict([(attr, value) for attr, value in vars(cls).items() + if not callable(getattr(cls, attr)) and attr.isupper()]) -def library_version(): +def library_version() -> str: """Return a version of this python library """ diff --git a/appium/common/logger.py b/appium/common/logger.py index 372d7fa8..014b69e4 100644 --- a/appium/common/logger.py +++ b/appium/common/logger.py @@ -16,7 +16,7 @@ import sys -def setup_logger(level=logging.NOTSET): +def setup_logger(level: int = logging.NOTSET) -> None: logger.propagate = False logger.setLevel(level) handler = logging.StreamHandler(stream=sys.stderr) diff --git a/appium/saucetestcase.py b/appium/saucetestcase.py index 8192f504..a2e94aba 100644 --- a/appium/saucetestcase.py +++ b/appium/saucetestcase.py @@ -19,6 +19,7 @@ import os import sys import unittest +from typing import Any, Callable, List from sauceclient import SauceClient @@ -29,8 +30,8 @@ sauce = SauceClient(SAUCE_USERNAME, SAUCE_ACCESS_KEY) -def on_platforms(platforms): - def decorator(base_class): +def on_platforms(platforms: List[str]) -> Callable[[type], None]: + def decorator(base_class: type) -> None: module = sys.modules[base_class.__module__].__dict__ for i, platform in enumerate(platforms): name = "%s_%s" % (base_class.__name__, i + 1) @@ -40,16 +41,16 @@ def decorator(base_class): class SauceTestCase(unittest.TestCase): - def setUp(self): - self.desired_capabilities['name'] = self.id() + def setUp(self) -> None: + self.desired_capabilities['name'] = self.id() # type: ignore sauce_url = "http://%s:%s@ondemand.saucelabs.com:80/wd/hub" self.driver = webdriver.Remote( - desired_capabilities=self.desired_capabilities, + desired_capabilities=self.desired_capabilities, # type: ignore command_executor=sauce_url % (SAUCE_USERNAME, SAUCE_ACCESS_KEY) ) self.driver.implicitly_wait(30) - def tearDown(self): + def tearDown(self) -> None: print("Link to your job: https://saucelabs.com/jobs/%s" % self.driver.session_id) try: if sys.exc_info() == (None, None, None): diff --git a/appium/webdriver/appium_connection.py b/appium/webdriver/appium_connection.py index 719923fd..f89998af 100644 --- a/appium/webdriver/appium_connection.py +++ b/appium/webdriver/appium_connection.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict + from selenium.webdriver.remote.remote_connection import RemoteConnection from appium.common.helper import library_version @@ -20,7 +22,7 @@ class AppiumConnection(RemoteConnection): @classmethod - def get_remote_connection_headers(cls, parsed_url, keep_alive=True): + def get_remote_connection_headers(cls, parsed_url: str, 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) headers['User-Agent'] = 'appium/python {} ({})'.format(library_version(), headers['User-Agent']) diff --git a/appium/webdriver/appium_service.py b/appium/webdriver/appium_service.py index 42527c24..7683782a 100644 --- a/appium/webdriver/appium_service.py +++ b/appium/webdriver/appium_service.py @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. - import os -import subprocess +import subprocess as sp import sys import time +from typing import Any, List, Optional, TypeVar, Union import urllib3 @@ -27,7 +27,7 @@ STATUS_URL = '/wd/hub/status' -def find_executable(executable): +def find_executable(executable: str) -> Optional[str]: path = os.environ['PATH'] paths = path.split(os.pathsep) base, ext = os.path.splitext(executable) @@ -45,7 +45,7 @@ def find_executable(executable): return None -def poll_url(host, port, path, timeout_ms): +def poll_url(host: str, port: int, path: str, timeout_ms: int) -> bool: time_started_sec = time.time() while time.time() < time_started_sec + timeout_ms / 1000.0: try: @@ -64,12 +64,15 @@ class AppiumServiceError(RuntimeError): pass +T = TypeVar('T', bound='AppiumService') + + class AppiumService(object): - def __init__(self): - self._process = None - self._cmd = None + def __init__(self) -> None: + self._process: Optional[sp.Popen] = None + self._cmd: Optional[List] = None - def _get_node(self): + def _get_node(self) -> str: if not hasattr(self, '_node_executable'): self._node_executable = find_executable('node') if self._node_executable is None: @@ -77,7 +80,7 @@ def _get_node(self): 'Make sure it is installed and present in PATH') return self._node_executable - def _get_npm(self): + def _get_npm(self) -> str: if not hasattr(self, '_npm_executable'): self._npm_executable = find_executable('npm.cmd' if sys.platform == 'win32' else 'npm') if self._npm_executable is None: @@ -85,41 +88,41 @@ def _get_npm(self): 'Make sure it is installed and present in PATH') return self._npm_executable - def _get_main_script(self): + def _get_main_script(self) -> Union[str, bytes]: if not hasattr(self, '_main_script'): for args in [['root', '-g'], ['root']]: try: - modules_root = subprocess.check_output([self._get_npm()] + args).strip().decode('utf-8') + modules_root = sp.check_output([self._get_npm()] + args).strip().decode('utf-8') if os.path.exists(os.path.join(modules_root, MAIN_SCRIPT_PATH)): - self._main_script = os.path.join(modules_root, MAIN_SCRIPT_PATH) + self._main_script: Union[str, bytes] = os.path.join(modules_root, MAIN_SCRIPT_PATH) break - except subprocess.CalledProcessError: + except sp.CalledProcessError: continue if not hasattr(self, '_main_script'): try: - self._main_script = subprocess.check_output( + self._main_script = sp.check_output( [self._get_node(), '-e', 'console.log(require.resolve("{}"))'.format(MAIN_SCRIPT_PATH)]).strip() - except subprocess.CalledProcessError as e: + except sp.CalledProcessError as e: raise AppiumServiceError(e.output) return self._main_script @staticmethod - def _parse_port(args): + def _parse_port(args: List[str]) -> int: for idx, arg in enumerate(args or []): if arg in ('--port', '-p') and idx < len(args) - 1: return int(args[idx + 1]) return DEFAULT_PORT @staticmethod - def _parse_host(args): + def _parse_host(args: List[str]) -> str: for idx, arg in enumerate(args or []): if arg in ('--address', '-a') and idx < len(args) - 1: return args[idx + 1] return DEFAULT_HOST - def start(self, **kwargs): + def start(self, **kwargs: Any) -> sp.Popen: """Starts Appium service with given arguments. The service will be forcefully restarted if it is already running. @@ -153,31 +156,31 @@ def start(self, **kwargs): env = kwargs['env'] if 'env' in kwargs else None node = kwargs['node'] if 'node' in kwargs else self._get_node() - stdout = kwargs['stdout'] if 'stdout' in kwargs else subprocess.PIPE - stderr = kwargs['stderr'] if 'stderr' in kwargs else subprocess.PIPE + stdout = kwargs['stdout'] if 'stdout' in kwargs else sp.PIPE + stderr = kwargs['stderr'] if 'stderr' in kwargs else sp.PIPE timeout_ms = int(kwargs['timeout_ms']) if 'timeout_ms' in kwargs else STARTUP_TIMEOUT_MS main_script = kwargs['main_script'] if 'main_script' in kwargs else self._get_main_script() args = [node, main_script] if 'args' in kwargs: args.extend(kwargs['args']) self._cmd = args - self._process = subprocess.Popen(args=args, stdout=stdout, stderr=stderr, env=env) + self._process = sp.Popen(args=args, stdout=stdout, stderr=stderr, env=env) host = self._parse_host(args) port = self._parse_port(args) - error_msg = None + error_msg: Optional[str] = None if not self.is_running or (timeout_ms > 0 and not poll_url(host, port, STATUS_URL, timeout_ms)): error_msg = 'Appium has failed to start on {}:{} within {}ms timeout'\ .format(host, port, timeout_ms) if error_msg is not None: - if stderr == subprocess.PIPE: + if stderr == sp.PIPE: err_output = self._process.stderr.read() if err_output: - error_msg += '\nOriginal error: {}'.format(err_output) + error_msg += '\nOriginal error: {}'.format(str(err_output)) self.stop() raise AppiumServiceError(error_msg) return self._process - def stop(self): + def stop(self) -> bool: """Stops Appium service if it is running. The call will be ignored if the service is not running @@ -188,14 +191,14 @@ def stop(self): """ is_terminated = False if self.is_running: - self._process.terminate() + self._process.terminate() # type: ignore is_terminated = True self._process = None self._cmd = None return is_terminated @property - def is_running(self): + def is_running(self) -> bool: """Check if the service is running. Returns: @@ -204,7 +207,7 @@ def is_running(self): return self._process is not None and self._process.poll() is None @property - def is_listening(self): + def is_listening(self) -> bool: """Check if the service is listening on the given/default host/port. The fact, that the service is running, does not always mean it is listening. diff --git a/appium/webdriver/common/multi_action.py b/appium/webdriver/common/multi_action.py index f80a111f..f3326534 100644 --- a/appium/webdriver/common/multi_action.py +++ b/appium/webdriver/common/multi_action.py @@ -19,17 +19,25 @@ # chaining as the spec requires. import copy +from typing import TYPE_CHECKING, Dict, List, Optional, TypeVar, Union from appium.webdriver.mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + from appium.webdriver.webelement import WebElement + from appium.webdriver.common.touch_action import TouchAction + +T = TypeVar('T', bound='MultiAction') + class MultiAction(object): - def __init__(self, driver, element=None): + def __init__(self, driver: 'WebDriver', element: Optional['WebElement'] = None) -> None: self._driver = driver self._element = element - self._touch_actions = [] + self._touch_actions: List['TouchAction'] = [] - def add(self, *touch_actions): + def add(self, *touch_actions: 'TouchAction') -> None: """Add TouchAction objects to the MultiAction, to be performed later. Args: @@ -49,7 +57,7 @@ def add(self, *touch_actions): self._touch_actions.append(copy.copy(touch_action)) - def perform(self): + def perform(self: T) -> T: """Perform the actions stored in the object. Usage: @@ -68,7 +76,7 @@ def perform(self): return self @property - def json_wire_gestures(self): + def json_wire_gestures(self) -> Dict[str, Union[List, str]]: actions = [] for action in self._touch_actions: actions.append(action.json_wire_gestures) diff --git a/appium/webdriver/common/touch_action.py b/appium/webdriver/common/touch_action.py index 84668e17..a8fb299d 100644 --- a/appium/webdriver/common/touch_action.py +++ b/appium/webdriver/common/touch_action.py @@ -24,16 +24,25 @@ # pylint: disable=no-self-use import copy +from typing import TYPE_CHECKING, Dict, List, Optional, TypeVar, Union from appium.webdriver.mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + from appium.webdriver.webdriver import WebDriver + +T = TypeVar('T', bound='TouchAction') + class TouchAction(object): - def __init__(self, driver=None): + + def __init__(self, driver: Optional['WebDriver'] = None): self._driver = driver - self._actions = [] + self._actions: List = [] - def tap(self, element=None, x=None, y=None, count=1): + def tap(self: T, element: Optional['WebElement'] = None, x: Optional[int] + = None, y: Optional[int] = None, count: int = 1) -> T: """Perform a tap action on the element Args: @@ -50,7 +59,8 @@ def tap(self, element=None, x=None, y=None, count=1): return self - def press(self, el=None, x=None, y=None, pressure=None): + def press(self: T, el: Optional['WebElement'] = None, x: Optional[int] = None, + y: Optional[int] = None, pressure: Optional[float] = None) -> T: """Begin a chain with a press down action at a particular element or point Args: @@ -67,7 +77,8 @@ def press(self, el=None, x=None, y=None, pressure=None): return self - def long_press(self, el=None, x=None, y=None, duration=1000): + def long_press(self: T, el: Optional['WebElement'] = None, x: Optional[int] + = None, y: Optional[int] = None, duration: int = 1000) -> T: """Begin a chain with a press down that lasts `duration` milliseconds Args: @@ -83,7 +94,7 @@ def long_press(self, el=None, x=None, y=None, duration=1000): return self - def wait(self, ms=0): + def wait(self: T, ms: int = 0) -> T: """Pause for `ms` milliseconds. Args: @@ -101,7 +112,7 @@ def wait(self, ms=0): return self - def move_to(self, el=None, x=None, y=None): + def move_to(self: T, el: Optional['WebElement'] = None, x: Optional[int] = None, y: Optional[int] = None) -> T: """Move the pointer from the previous point to the element or point specified Args: @@ -116,7 +127,7 @@ def move_to(self, el=None, x=None, y=None): return self - def release(self): + def release(self: T) -> T: """End the action by lifting the pointer off the screen Returns: @@ -126,12 +137,14 @@ def release(self): return self - def perform(self): + def perform(self: T) -> T: """Perform the action by sending the commands to the server to be operated upon Returns: `TouchAction`: self instance """ + if self._driver is None: + raise TypeError('Set driver to constructor as a argument when to create the instance.') params = {'actions': self._actions} self._driver.execute(Command.TOUCH_ACTION, params) @@ -141,23 +154,24 @@ def perform(self): return self @property - def json_wire_gestures(self): + def json_wire_gestures(self) -> List[Dict]: gestures = [] for action in self._actions: gestures.append(copy.deepcopy(action)) return gestures - def _add_action(self, action, options): + def _add_action(self, action: str, options: Dict) -> None: gesture = { 'action': action, 'options': options, } self._actions.append(gesture) - def _get_opts(self, element, x, y, duration=None, pressure=None): + def _get_opts(self, el: Optional['WebElement'] = None, x: Optional[int] = None, y: Optional[int] = None, + duration: Optional[int] = None, pressure: Optional[float] = None) -> Dict[str, Union[int, float]]: opts = {} - if element is not None: - opts['element'] = element.id + if el is not None: + opts['element'] = el.id # it makes no sense to have x but no y, or vice versa. if x is not None and y is not None: diff --git a/appium/webdriver/errorhandler.py b/appium/webdriver/errorhandler.py index 1fa793ba..38426992 100644 --- a/appium/webdriver/errorhandler.py +++ b/appium/webdriver/errorhandler.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict + from selenium.common.exceptions import WebDriverException from selenium.webdriver.remote import errorhandler @@ -19,7 +21,7 @@ class MobileErrorHandler(errorhandler.ErrorHandler): - def check_response(self, response): + def check_response(self, response: Dict) -> None: try: super(MobileErrorHandler, self).check_response(response) except WebDriverException as wde: diff --git a/appium/webdriver/extensions/action_helpers.py b/appium/webdriver/extensions/action_helpers.py index 108fcfd5..98ddbf67 100644 --- a/appium/webdriver/extensions/action_helpers.py +++ b/appium/webdriver/extensions/action_helpers.py @@ -12,15 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import List, Optional, Tuple, TypeVar + from selenium import webdriver from appium.webdriver.common.multi_action import MultiAction from appium.webdriver.common.touch_action import TouchAction +from appium.webdriver.webelement import WebElement + +T = TypeVar('T', bound='ActionHelpers') class ActionHelpers(webdriver.Remote): - def scroll(self, origin_el, destination_el, duration=None): + def scroll(self, origin_el: WebElement, destination_el: WebElement, duration: Optional[int] = None) -> T: """Scrolls from one element to another Args: @@ -47,7 +52,7 @@ def scroll(self, origin_el, destination_el, duration=None): action.press(origin_el).wait(duration).move_to(destination_el).release().perform() return self - def drag_and_drop(self, origin_el, destination_el): + def drag_and_drop(self, origin_el: WebElement, destination_el: WebElement) -> T: """Drag the origin element to the destination element Args: @@ -61,7 +66,7 @@ def drag_and_drop(self, origin_el, destination_el): action.long_press(origin_el).move_to(destination_el).release().perform() return self - def tap(self, positions, duration=None): + def tap(self, positions: List[Tuple], duration: Optional[int] = None) -> T: """Taps on an particular place with up to five fingers, holding for a certain time @@ -100,7 +105,7 @@ def tap(self, positions, duration=None): ma.perform() return self - def swipe(self, start_x, start_y, end_x, end_y, duration=None): + def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: int = 0) -> T: """Swipe from one point to another point, for an optional duration. Args: @@ -127,7 +132,7 @@ def swipe(self, start_x, start_y, end_x, end_y, duration=None): action.perform() return self - def flick(self, start_x, start_y, end_x, end_y): + def flick(self, start_x: int, start_y: int, end_x: int, end_y: int) -> T: """Flick from one point to another point. Args: diff --git a/appium/webdriver/extensions/android/activities.py b/appium/webdriver/extensions/android/activities.py index 236252c3..f80805a0 100644 --- a/appium/webdriver/extensions/android/activities.py +++ b/appium/webdriver/extensions/android/activities.py @@ -12,15 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TypeVar + from selenium import webdriver from selenium.common.exceptions import TimeoutException from selenium.webdriver.support.ui import WebDriverWait from appium.webdriver.mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='Activities') + class Activities(webdriver.Remote): - def start_activity(self, app_package, app_activity, **opts): + def start_activity(self, app_package: str, app_activity: str, **opts: str) -> T: """Opens an arbitrary activity during a test. If the activity belongs to another application, that application is started and the activity is opened. @@ -59,7 +63,7 @@ def start_activity(self, app_package, app_activity, **opts): return self @property - def current_activity(self): + def current_activity(self) -> str: """Retrieves the current activity running on the device. Returns: @@ -67,7 +71,7 @@ def current_activity(self): """ return self.execute(Command.GET_CURRENT_ACTIVITY)['value'] - def wait_activity(self, activity, timeout, interval=1): + def wait_activity(self, activity: str, timeout: int, interval: int = 1) -> bool: """Wait for an activity: block until target activity presents or time out. This is an Android-only method. @@ -76,6 +80,9 @@ def wait_activity(self, activity, timeout, interval=1): activity (str): target activity timeout (int): max wait time, in seconds interval (int): sleep interval between retries, in seconds + + Returns: + bool: `True` if the target activity is shown """ try: WebDriverWait(self, timeout, interval).until( @@ -86,7 +93,7 @@ def wait_activity(self, activity, timeout, interval=1): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_CURRENT_ACTIVITY] = \ ('GET', '/session/$sessionId/appium/device/current_activity') self.command_executor._commands[Command.START_ACTIVITY] = \ diff --git a/appium/webdriver/extensions/android/common.py b/appium/webdriver/extensions/android/common.py index 4fa3a00f..65da6733 100644 --- a/appium/webdriver/extensions/android/common.py +++ b/appium/webdriver/extensions/android/common.py @@ -12,14 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, TypeVar + from selenium import webdriver from appium.webdriver.mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='Common') + class Common(webdriver.Remote): - def end_test_coverage(self, intent, path): + def end_test_coverage(self, intent: str, path: str) -> Any: # TODO Check return type """Ends the coverage collection and pull the coverage.ec file from the device. Android only. @@ -38,7 +42,7 @@ def end_test_coverage(self, intent, path): } return self.execute(Command.END_TEST_COVERAGE, data)['value'] - def open_notifications(self): + def open_notifications(self) -> T: """Open notification shade in Android (API Level 18 and above) Returns: @@ -48,12 +52,12 @@ def open_notifications(self): return self @property - def current_package(self): + def current_package(self) -> str: """Retrieves the current package running on the device. """ return self.execute(Command.GET_CURRENT_PACKAGE)['value'] - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_CURRENT_PACKAGE] = \ ('GET', '/session/$sessionId/appium/device/current_package') self.command_executor._commands[Command.END_TEST_COVERAGE] = \ diff --git a/appium/webdriver/extensions/android/display.py b/appium/webdriver/extensions/android/display.py index c05e1a5d..73ac1ccb 100644 --- a/appium/webdriver/extensions/android/display.py +++ b/appium/webdriver/extensions/android/display.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TypeVar + from selenium import webdriver from appium.webdriver.mobilecommand import MobileCommand as Command @@ -19,7 +21,7 @@ class Display(webdriver.Remote): - def get_display_density(self): + def get_display_density(self) -> int: """Get the display density, Android only Returns: @@ -32,6 +34,6 @@ def get_display_density(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_DISPLAY_DENSITY] = \ ('GET', '/session/$sessionId/appium/device/display_density') diff --git a/appium/webdriver/extensions/android/gsm.py b/appium/webdriver/extensions/android/gsm.py index 4aaf4fc8..df4ea95e 100644 --- a/appium/webdriver/extensions/android/gsm.py +++ b/appium/webdriver/extensions/android/gsm.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TypeVar + from selenium import webdriver from appium.common.helper import extract_const_attributes @@ -44,9 +46,12 @@ class GsmVoiceState(object): ON = 'on' +T = TypeVar('T', bound='Gsm') + + class Gsm(webdriver.Remote): - def make_gsm_call(self, phone_number, action): + def make_gsm_call(self, phone_number: str, action: str) -> T: """Make GSM call (Emulator only) Android only. @@ -66,7 +71,7 @@ def make_gsm_call(self, phone_number, action): self.execute(Command.MAKE_GSM_CALL, {'phoneNumber': phone_number, 'action': action}) return self - def set_gsm_signal(self, strength): + def set_gsm_signal(self, strength: int) -> T: """Set GSM signal strength (Emulator only) Android only. @@ -85,7 +90,7 @@ def set_gsm_signal(self, strength): self.execute(Command.SET_GSM_SIGNAL, {'signalStrength': strength, 'signalStrengh': strength}) return self - def set_gsm_voice(self, state): + def set_gsm_voice(self, state: str) -> T: """Set GSM voice state (Emulator only) Android only. @@ -106,7 +111,7 @@ def set_gsm_voice(self, state): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.MAKE_GSM_CALL] = \ ('POST', '/session/$sessionId/appium/device/gsm_call') self.command_executor._commands[Command.SET_GSM_SIGNAL] = \ diff --git a/appium/webdriver/extensions/android/nativekey.py b/appium/webdriver/extensions/android/nativekey.py index aca374eb..79973dba 100644 --- a/appium/webdriver/extensions/android/nativekey.py +++ b/appium/webdriver/extensions/android/nativekey.py @@ -1,3 +1,18 @@ +#!/usr/bin/env python + +# 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. + + class AndroidKey: # Key code constant: Unknown key code. UNKNOWN = 0 @@ -1005,14 +1020,14 @@ class AndroidKey: BUTTON_15, BUTTON_16] @staticmethod - def is_gamepad_button(code): + def is_gamepad_button(code: int) -> bool: """Returns true if the specified nativekey is a gamepad button.""" return code in AndroidKey.gamepad_buttons confirm_buttons = [DPAD_CENTER, ENTER, SPACE, NUMPAD_ENTER] @staticmethod - def is_confirm_key(code): + def is_confirm_key(code: int) -> bool: """Returns true if the key will, by default, trigger a click on the focused view.""" return code in AndroidKey.confirm_buttons @@ -1021,7 +1036,7 @@ def is_confirm_key(code): MEDIA_REWIND, MEDIA_RECORD, MEDIA_FAST_FORWARD] @staticmethod - def is_media_key(code): + def is_media_key(code: int) -> bool: """Returns true if this key is a media key, which can be send to apps that are interested in media key events.""" return code in AndroidKey.media_buttons @@ -1035,13 +1050,13 @@ def is_media_key(code): BRIGHTNESS_DOWN, BRIGHTNESS_UP, MEDIA_AUDIO_TRACK] @staticmethod - def is_system_key(code): + def is_system_key(code: int) -> bool: """Returns true if the key is a system key, System keys can not be used for menu shortcuts.""" return code in AndroidKey.system_buttons wake_buttons = [BACK, MENU, WAKEUP, PAIRING, STEM_1, STEM_2, STEM_3] @staticmethod - def is_wake_key(code): + def is_wake_key(code: int) -> bool: """Returns true if the key is a wake key.""" return code in AndroidKey.wake_buttons diff --git a/appium/webdriver/extensions/android/network.py b/appium/webdriver/extensions/android/network.py index 3a648154..99c71763 100644 --- a/appium/webdriver/extensions/android/network.py +++ b/appium/webdriver/extensions/android/network.py @@ -12,12 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, TypeVar + from selenium import webdriver from appium.common.helper import extract_const_attributes from appium.common.logger import logger from appium.webdriver.mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='Network') + class NetSpeed(object): GSM = 'gsm' # GSM/CSD (up: 14.4(kbps), down: 14.4(kbps)) @@ -34,7 +38,7 @@ class NetSpeed(object): class Network(webdriver.Remote): @property - def network_connection(self): + def network_connection(self) -> int: """Returns an integer bitmask specifying the network connection type. Android only. @@ -42,7 +46,7 @@ def network_connection(self): """ return self.execute(Command.GET_NETWORK_CONNECTION, {})['value'] - def set_network_connection(self, connection_type): + def set_network_connection(self, connection_type: int) -> int: """Sets the network connection type. Android only. Possible values: @@ -58,8 +62,8 @@ def set_network_connection(self, connection_type): Args: connection_type (int): a member of the enum appium.webdriver.ConnectionType - Returns: - `appium.webdriver.webdriver.WebDriver` + Return: + int: Set network connection type """ data = { 'parameters': { @@ -68,7 +72,7 @@ def set_network_connection(self, connection_type): } return self.execute(Command.SET_NETWORK_CONNECTION, data)['value'] - def toggle_wifi(self): + def toggle_wifi(self) -> T: """Toggle the wifi on the device, Android only. Returns: @@ -77,7 +81,7 @@ def toggle_wifi(self): self.execute(Command.TOGGLE_WIFI, {}) return self - def set_network_speed(self, speed_type): + def set_network_speed(self, speed_type: str) -> T: """Set the network speed emulation. Android Emulator only. @@ -102,7 +106,7 @@ def set_network_speed(self, speed_type): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.TOGGLE_WIFI] = \ ('POST', '/session/$sessionId/appium/device/toggle_wifi') self.command_executor._commands[Command.GET_NETWORK_CONNECTION] = \ diff --git a/appium/webdriver/extensions/android/performance.py b/appium/webdriver/extensions/android/performance.py index 7b77b74e..3d661d77 100644 --- a/appium/webdriver/extensions/android/performance.py +++ b/appium/webdriver/extensions/android/performance.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, List, Union + from selenium import webdriver from appium.webdriver.mobilecommand import MobileCommand as Command @@ -19,7 +21,7 @@ class Performance(webdriver.Remote): - def get_performance_data(self, package_name, data_type, data_read_timeout=None): + def get_performance_data(self, package_name: str, data_type: str, data_read_timeout: int = None) -> List[List[str]]: """Returns the information of the system state which is supported to read as like cpu, memory, network traffic, and battery. @@ -36,14 +38,14 @@ def get_performance_data(self, package_name, data_type, data_read_timeout=None): self.driver.get_performance_data('my.app.package', 'cpuinfo', 5) Returns: - dict: The data along to `data_type` + list: The data along to `data_type` """ - data = {'packageName': package_name, 'dataType': data_type} + data: Dict[str, Union[str, int]] = {'packageName': package_name, 'dataType': data_type} if data_read_timeout is not None: data['dataReadTimeout'] = data_read_timeout return self.execute(Command.GET_PERFORMANCE_DATA, data)['value'] - def get_performance_data_types(self): + def get_performance_data_types(self) -> List: """Returns the information types of the system state which is supported to read as like cpu, memory, network traffic, and battery. Android only. @@ -58,7 +60,7 @@ def get_performance_data_types(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_PERFORMANCE_DATA] = \ ('POST', '/session/$sessionId/appium/getPerformanceData') self.command_executor._commands[Command.GET_PERFORMANCE_DATA_TYPES] = \ diff --git a/appium/webdriver/extensions/android/power.py b/appium/webdriver/extensions/android/power.py index bfa6a824..827324e8 100644 --- a/appium/webdriver/extensions/android/power.py +++ b/appium/webdriver/extensions/android/power.py @@ -12,16 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TypeVar + from selenium import webdriver from appium.webdriver.mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='Power') + class Power(webdriver.Remote): AC_OFF, AC_ON = 'off', 'on' - def set_power_capacity(self, percent): + def set_power_capacity(self, percent: int) -> T: """Emulate power capacity change on the connected emulator. Android only. @@ -38,7 +42,7 @@ def set_power_capacity(self, percent): self.execute(Command.SET_POWER_CAPACITY, {'percent': percent}) return self - def set_power_ac(self, ac_state): + def set_power_ac(self, ac_state: str) -> T: """Emulate power state change on the connected emulator. Android only. @@ -58,7 +62,7 @@ def set_power_ac(self, ac_state): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.SET_POWER_CAPACITY] = \ ('POST', '/session/$sessionId/appium/device/power_capacity') self.command_executor._commands[Command.SET_POWER_AC] = \ diff --git a/appium/webdriver/extensions/android/sms.py b/appium/webdriver/extensions/android/sms.py index 4c0675a0..8f7773cf 100644 --- a/appium/webdriver/extensions/android/sms.py +++ b/appium/webdriver/extensions/android/sms.py @@ -12,14 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TypeVar + from selenium import webdriver from appium.webdriver.mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='Sms') + class Sms(webdriver.Remote): - def send_sms(self, phone_number, message): + def send_sms(self, phone_number: str, message: str) -> T: """Emulate send SMS event on the connected emulator. Android only. @@ -39,6 +43,6 @@ def send_sms(self, phone_number, message): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.SEND_SMS] = \ ('POST', '/session/$sessionId/appium/device/send_sms') diff --git a/appium/webdriver/extensions/android/system_bars.py b/appium/webdriver/extensions/android/system_bars.py index edc98a5e..085296d9 100644 --- a/appium/webdriver/extensions/android/system_bars.py +++ b/appium/webdriver/extensions/android/system_bars.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, Union + from selenium import webdriver from appium.webdriver.mobilecommand import MobileCommand as Command @@ -19,7 +21,7 @@ class SystemBars(webdriver.Remote): - def get_system_bars(self): + def get_system_bars(self) -> Dict[str, Dict[str, Union[int, bool]]]: """Retrieve visibility and bounds information of the status and navigation bars. Android only. @@ -43,6 +45,6 @@ def get_system_bars(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_SYSTEM_BARS] = \ ('GET', '/session/$sessionId/appium/device/system_bars') diff --git a/appium/webdriver/extensions/applications.py b/appium/webdriver/extensions/applications.py index e89b097d..9ab8a295 100644 --- a/appium/webdriver/extensions/applications.py +++ b/appium/webdriver/extensions/applications.py @@ -12,13 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict, TypeVar + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='Applications') + class Applications(webdriver.Remote): - def background_app(self, seconds): + def background_app(self, seconds: int) -> T: """Puts the application in the background on the device for a certain duration. Args: @@ -33,7 +37,7 @@ def background_app(self, seconds): self.execute(Command.BACKGROUND, data) return self - def is_app_installed(self, bundle_id): + def is_app_installed(self, bundle_id: str) -> bool: """Checks whether the application specified by `bundle_id` is installed on the device. Args: @@ -47,7 +51,7 @@ def is_app_installed(self, bundle_id): } return self.execute(Command.IS_APP_INSTALLED, data)['value'] - def install_app(self, app_path, **options): + def install_app(self, app_path: str, **options: Any) -> T: """Install the application found at `app_path` on the device. Args: @@ -67,7 +71,7 @@ def install_app(self, app_path, **options): Returns: `appium.webdriver.webdriver.WebDriver` """ - data = { + data: Dict[str, Any] = { 'appPath': app_path, } if options: @@ -75,7 +79,7 @@ def install_app(self, app_path, **options): self.execute(Command.INSTALL_APP, data) return self - def remove_app(self, app_id, **options): + def remove_app(self, app_id: str, **options: Any) -> T: """Remove the specified application from the device. Args: @@ -90,7 +94,7 @@ def remove_app(self, app_id, **options): Returns: `appium.webdriver.webdriver.WebDriver` """ - data = { + data: Dict[str, Any] = { 'appId': app_id, } if options: @@ -98,7 +102,7 @@ def remove_app(self, app_id, **options): self.execute(Command.REMOVE_APP, data) return self - def launch_app(self): + def launch_app(self) -> T: """Start on the device the application specified in the desired capabilities. Returns: @@ -107,7 +111,7 @@ def launch_app(self): self.execute(Command.LAUNCH_APP) return self - def close_app(self): + def close_app(self) -> T: """Stop the running application, specified in the desired capabilities, on the device. @@ -117,7 +121,7 @@ def close_app(self): self.execute(Command.CLOSE_APP) return self - def terminate_app(self, app_id, **options): + def terminate_app(self, app_id: str, **options: Any) -> bool: """Terminates the application if it is running. Args: @@ -130,14 +134,14 @@ def terminate_app(self, app_id, **options): Returns: bool: True if the app has been successfully terminated """ - data = { + data: Dict[str, Any] = { 'appId': app_id, } if options: data.update({'options': options}) return self.execute(Command.TERMINATE_APP, data)['value'] - def activate_app(self, app_id): + def activate_app(self, app_id: str) -> T: """Activates the application if it is not running or is running in the background. @@ -153,7 +157,7 @@ def activate_app(self, app_id): self.execute(Command.ACTIVATE_APP, data) return self - def query_app_state(self, app_id): + def query_app_state(self, app_id: str) -> int: """Queries the state of the application. Args: @@ -168,7 +172,7 @@ class for more details. } return self.execute(Command.QUERY_APP_STATE, data)['value'] - def app_strings(self, language=None, string_file=None): + def app_strings(self, language: str = None, string_file: str = None) -> str: """Returns the application strings from the device for the specified language. @@ -183,7 +187,7 @@ def app_strings(self, language=None, string_file=None): data['stringFile'] = string_file return self.execute(Command.GET_APP_STRINGS, data)['value'] - def reset(self): + def reset(self) -> T: """Resets the current application on the device. """ self.execute(Command.RESET) @@ -191,7 +195,7 @@ def reset(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.BACKGROUND] = \ ('POST', '/session/$sessionId/appium/app/background') self.command_executor._commands[Command.IS_APP_INSTALLED] = \ diff --git a/appium/webdriver/extensions/clipboard.py b/appium/webdriver/extensions/clipboard.py index e05e6b86..e4f7bc71 100644 --- a/appium/webdriver/extensions/clipboard.py +++ b/appium/webdriver/extensions/clipboard.py @@ -13,25 +13,31 @@ # limitations under the License. import base64 +from typing import Any, Dict, Optional, TypeVar from selenium import webdriver -from appium.common.helper import appium_bytes from appium.webdriver.clipboard_content_type import ClipboardContentType from ..mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='Clipboard') + class Clipboard(webdriver.Remote): - def set_clipboard(self, content, content_type=ClipboardContentType.PLAINTEXT, label=None): + def set_clipboard(self, content: bytes, content_type: str = ClipboardContentType.PLAINTEXT, + label: Optional[str] = None) -> T: """Set the content of the system clipboard Args: - content (str): The content to be set as bytearray string + content (bytes): The content to be set as bytearray string content_type (str): One of ClipboardContentType items. Only ClipboardContentType.PLAINTEXT is supported on Android label (:obj:`str`, optional): label argument, which only works for Android + + Returns: + `appium.webdriver.webdriver.WebDriver` """ options = { 'content': base64.b64encode(content).decode('UTF-8'), @@ -40,18 +46,23 @@ def set_clipboard(self, content, content_type=ClipboardContentType.PLAINTEXT, la if label: options['label'] = label self.execute(Command.SET_CLIPBOARD, options) + return self - def set_clipboard_text(self, text, label=None): + def set_clipboard_text(self, text: str, label: Optional[str] = None) -> T: """Copies the given text to the system clipboard Args: text (str): The text to be set - label (:obj:`int`, optional):label argument, which only works for Android + label (:obj:`str`, optional):label argument, which only works for Android + + Returns: + `appium.webdriver.webdriver.WebDriver` """ - self.set_clipboard(appium_bytes(str(text), 'UTF-8'), ClipboardContentType.PLAINTEXT, label) + self.set_clipboard(bytes(str(text), 'UTF-8'), ClipboardContentType.PLAINTEXT, label) + return self - def get_clipboard(self, content_type=ClipboardContentType.PLAINTEXT): + def get_clipboard(self, content_type: str = ClipboardContentType.PLAINTEXT) -> bytes: """Receives the content of the system clipboard Args: @@ -66,7 +77,7 @@ def get_clipboard(self, content_type=ClipboardContentType.PLAINTEXT): })['value'] return base64.b64decode(base64_str) - def get_clipboard_text(self): + def get_clipboard_text(self) -> str: """Receives the text of the system clipboard Return: @@ -76,7 +87,7 @@ def get_clipboard_text(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.SET_CLIPBOARD] = \ ('POST', '/session/$sessionId/appium/device/set_clipboard') self.command_executor._commands[Command.GET_CLIPBOARD] = \ diff --git a/appium/webdriver/extensions/context.py b/appium/webdriver/extensions/context.py index 8581cc3f..9ff5ec62 100644 --- a/appium/webdriver/extensions/context.py +++ b/appium/webdriver/extensions/context.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import List + from selenium import webdriver from ..mobilecommand import MobileCommand as Command @@ -19,7 +21,7 @@ class Context(webdriver.Remote): @property - def contexts(self): + def contexts(self) -> List[str]: """Returns the contexts within the current session. Usage: @@ -32,7 +34,7 @@ def contexts(self): return self.execute(Command.CONTEXTS)['value'] @property - def current_context(self): + def current_context(self) -> str: """Returns the current context of the current session. Usage: @@ -44,7 +46,7 @@ def current_context(self): return self.execute(Command.GET_CURRENT_CONTEXT)['value'] @property - def context(self): + def context(self) -> str: """Returns the current context of the current session. Usage: @@ -57,7 +59,7 @@ def context(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.CONTEXTS] = \ ('GET', '/session/$sessionId/contexts') self.command_executor._commands[Command.GET_CURRENT_CONTEXT] = \ diff --git a/appium/webdriver/extensions/device_time.py b/appium/webdriver/extensions/device_time.py index e40b36d2..d16f56b3 100644 --- a/appium/webdriver/extensions/device_time.py +++ b/appium/webdriver/extensions/device_time.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional + from selenium import webdriver from ..mobilecommand import MobileCommand as Command @@ -20,7 +22,7 @@ class DeviceTime(webdriver.Remote): @property - def device_time(self): + def device_time(self) -> str: """Returns the date and time from the device. Return: @@ -28,11 +30,11 @@ def device_time(self): """ return self.execute(Command.GET_DEVICE_TIME_GET, {})['value'] - def get_device_time(self, format=None): + def get_device_time(self, format: Optional[str] = None) -> str: """Returns the date and time from the device. Args: - format (optional): The set of format specifiers. Read https://momentjs.com/docs/ + format (:obj:`str`, optional): The set of format specifiers. Read https://momentjs.com/docs/ to get the full list of supported datetime format specifiers. If unset, return :func:`.device_time` as default format is `YYYY-MM-DDTHH:mm:ssZ`, which complies to ISO-8601 @@ -50,7 +52,7 @@ def get_device_time(self, format=None): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_DEVICE_TIME_GET] = \ ('GET', '/session/$sessionId/appium/device/system_time') self.command_executor._commands[Command.GET_DEVICE_TIME_POST] = \ diff --git a/appium/webdriver/extensions/execute_driver.py b/appium/webdriver/extensions/execute_driver.py index 5c866a39..f6c00fb0 100644 --- a/appium/webdriver/extensions/execute_driver.py +++ b/appium/webdriver/extensions/execute_driver.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict, Optional, Union + from selenium import webdriver from ..mobilecommand import MobileCommand as Command @@ -19,15 +21,16 @@ class ExecuteDriver(webdriver.Remote): - def execute_driver(self, script, script_type='webdriverio', timeout_ms=None): + # TODO Inner class case + def execute_driver(self, script: str, script_type: str = 'webdriverio', timeout_ms: Optional[int] = None) -> Any: """Run a set of script against the current session, allowing execution of many commands in one Appium request. Please read http://appium.io/docs/en/commands/session/execute-driver for more details about the acceptable scripts and the output format. Args: - script (string): The string consisting of the script itself - script_type (string): The name of the script type. Defaults to 'webdriverio'. - timeout_ms (optional): The number of `ms` Appium should wait for the script to finish before killing it due to timeout_ms. + script (str): The string consisting of the script itself + script_type (str): The name of the script type. Defaults to 'webdriverio'. + timeout_ms (:obj:`int`, optional): The number of `ms` Appium should wait for the script to finish before killing it due to timeout_ms. Usage: self.driver.execute_driver(script='return [];') @@ -43,11 +46,11 @@ def execute_driver(self, script, script_type='webdriverio', timeout_ms=None): class Result(object): - def __init__(self, response): + def __init__(self, response: Dict): self.result = response['result'] self.logs = response['logs'] - option = {'script': script, 'type': script_type} + option: Dict[str, Union[str, int]] = {'script': script, 'type': script_type} if timeout_ms is not None: option['timeout'] = timeout_ms @@ -56,6 +59,6 @@ def __init__(self, response): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.EXECUTE_DRIVER] = \ ('POST', '/session/$sessionId/appium/execute_driver') diff --git a/appium/webdriver/extensions/execute_mobile_command.py b/appium/webdriver/extensions/execute_mobile_command.py index 789dcfef..9866df7f 100644 --- a/appium/webdriver/extensions/execute_mobile_command.py +++ b/appium/webdriver/extensions/execute_mobile_command.py @@ -12,12 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict, TypeVar + from selenium import webdriver +T = TypeVar('T', bound='ExecuteMobileCommand') + class ExecuteMobileCommand(webdriver.Remote): - def press_button(self, button_name): + def press_button(self, button_name: str) -> T: """Sends a physical button name to the device to simulate the user pressing. iOS only. @@ -38,7 +42,7 @@ def press_button(self, button_name): return self @property - def battery_info(self): + def battery_info(self) -> Dict[str, Any]: """Retrieves battery information for the device under test. Returns: diff --git a/appium/webdriver/extensions/hw_actions.py b/appium/webdriver/extensions/hw_actions.py index a3d6babb..c27c7382 100644 --- a/appium/webdriver/extensions/hw_actions.py +++ b/appium/webdriver/extensions/hw_actions.py @@ -12,18 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Optional, TypeVar + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='HardwareActions') + class HardwareActions(webdriver.Remote): - def lock(self, seconds=None): + def lock(self, seconds: Optional[int] = None) -> T: """Lock the device. No changes are made if the device is already unlocked. Args: - seconds (optional): The duration to lock the device, in seconds. + seconds (:obj:`int`, optional): The duration to lock the device, in seconds. The device is going to be locked forever until `unlock` is called if it equals or is less than zero, otherwise this call blocks until the timeout expires and unlocks the screen automatically. @@ -38,7 +42,7 @@ def lock(self, seconds=None): return self - def unlock(self): + def unlock(self) -> T: """Unlock the device. No changes are made if the device is already locked. Returns: @@ -47,7 +51,7 @@ def unlock(self): self.execute(Command.UNLOCK) return self - def is_locked(self): + def is_locked(self) -> bool: """Checks whether the device is locked. Returns: @@ -55,7 +59,7 @@ def is_locked(self): """ return self.execute(Command.IS_LOCKED)['value'] - def shake(self): + def shake(self) -> T: """Shake the device. Returns: @@ -64,7 +68,7 @@ def shake(self): self.execute(Command.SHAKE) return self - def touch_id(self, match): + def touch_id(self, match: bool) -> T: """Simulate touchId on iOS Simulator Args: @@ -79,7 +83,7 @@ def touch_id(self, match): self.execute(Command.TOUCH_ID, data) return self - def toggle_touch_id_enrollment(self): + def toggle_touch_id_enrollment(self) -> T: """Toggle enroll touchId on iOS Simulator Returns: @@ -88,7 +92,7 @@ def toggle_touch_id_enrollment(self): self.execute(Command.TOGGLE_TOUCH_ID_ENROLLMENT) return self - def finger_print(self, finger_id): + def finger_print(self, finger_id: int) -> Any: """Authenticate users by using their finger print scans on supported Android emulators. Args: @@ -101,7 +105,7 @@ def finger_print(self, finger_id): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.LOCK] = \ ('POST', '/session/$sessionId/appium/device/lock') self.command_executor._commands[Command.UNLOCK] = \ diff --git a/appium/webdriver/extensions/images_comparison.py b/appium/webdriver/extensions/images_comparison.py index c0f16a90..77289fe8 100644 --- a/appium/webdriver/extensions/images_comparison.py +++ b/appium/webdriver/extensions/images_comparison.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict, Union + from selenium import webdriver from ..mobilecommand import MobileCommand as Command @@ -19,7 +21,7 @@ class ImagesComparison(webdriver.Remote): - def match_images_features(self, base64_image1, base64_image2, **opts): + def match_images_features(self, base64_image1: bytes, base64_image2: bytes, **opts: Any) -> Dict[str, Any]: """Performs images matching by features. Read @@ -73,7 +75,8 @@ def match_images_features(self, base64_image1, base64_image2, **opts): } return self.execute(Command.COMPARE_IMAGES, options)['value'] - def find_image_occurrence(self, base64_full_image, base64_partial_image, **opts): + def find_image_occurrence(self, base64_full_image: bytes, base64_partial_image: bytes, + **opts: Any) -> Dict[str, Union[bytes, Dict]]: """Performs images matching by template to find possible occurrence of the partial image in the full image. @@ -91,10 +94,11 @@ def find_image_occurrence(self, base64_full_image, base64_partial_image, **opts) False by default Returns: - visualization (bytes): base64-encoded content of PNG visualization of the current comparison - operation. This entry is only present if `visualize` option is enabled - rect (dict): The region of the partial image occurrence on the full image. - The rect is represented by a dictionary with 'x', 'y', 'width' and 'height' keys + The dictionary containing the following entries: + visualization (bytes): base64-encoded content of PNG visualization of the current comparison + operation. This entry is only present if `visualize` option is enabled + rect (dict): The region of the partial image occurrence on the full image. + The rect is represented by a dictionary with 'x', 'y', 'width' and 'height' keys """ options = { 'mode': 'matchTemplate', @@ -104,7 +108,8 @@ def find_image_occurrence(self, base64_full_image, base64_partial_image, **opts) } return self.execute(Command.COMPARE_IMAGES, options)['value'] - def get_images_similarity(self, base64_image1, base64_image2, **opts): + def get_images_similarity(self, base64_image1: bytes, base64_image2: bytes, + **opts: Any) -> Dict[str, Union[bytes, Dict]]: """Performs images matching to calculate the similarity score between them. The flow there is similar to the one used in @@ -120,10 +125,11 @@ def get_images_similarity(self, base64_image1, base64_image2, **opts): False by default Returns: - visualization (bytes): base64-encoded content of PNG visualization of the current comparison - operation. This entry is only present if `visualize` option is enabled - score (float): The similarity score as a float number in range [0.0, 1.0]. - 1.0 is the highest score (means both images are totally equal). + The dictionary containing the following entries: + visualization (bytes): base64-encoded content of PNG visualization of the current comparison + operation. This entry is only present if `visualize` option is enabled + score (float): The similarity score as a float number in range [0.0, 1.0]. + 1.0 is the highest score (means both images are totally equal). """ options = { 'mode': 'getSimilarity', @@ -135,6 +141,6 @@ def get_images_similarity(self, base64_image1, base64_image2, **opts): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.COMPARE_IMAGES] = \ ('POST', '/session/$sessionId/appium/compare_images') diff --git a/appium/webdriver/extensions/ime.py b/appium/webdriver/extensions/ime.py index 9c3230e3..701c4f5c 100644 --- a/appium/webdriver/extensions/ime.py +++ b/appium/webdriver/extensions/ime.py @@ -12,15 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import List, TypeVar + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='IME') + class IME(webdriver.Remote): @property - def available_ime_engines(self): + def available_ime_engines(self) -> List[str]: """Get the available input methods for an Android device. Package and activity are returned (e.g., ['com.android.inputmethod.latin/.LatinIME']) @@ -31,7 +35,7 @@ def available_ime_engines(self): """ return self.execute(Command.GET_AVAILABLE_IME_ENGINES, {})['value'] - def is_ime_active(self): + def is_ime_active(self) -> bool: """Checks whether the device has IME service active. Android only. @@ -40,7 +44,7 @@ def is_ime_active(self): """ return self.execute(Command.IS_IME_ACTIVE, {})['value'] - def activate_ime_engine(self, engine): + def activate_ime_engine(self, engine: str) -> T: """Activates the given IME engine on the device. Android only. @@ -58,7 +62,7 @@ def activate_ime_engine(self, engine): self.execute(Command.ACTIVATE_IME_ENGINE, data) return self - def deactivate_ime_engine(self): + def deactivate_ime_engine(self) -> T: """Deactivates the currently active IME engine on the device. Android only. @@ -70,7 +74,7 @@ def deactivate_ime_engine(self): return self @property - def active_ime_engine(self): + def active_ime_engine(self) -> str: """Returns the activity and package of the currently active IME engine(e.g., 'com.android.inputmethod.latin/.LatinIME'). Android only. @@ -82,7 +86,7 @@ def active_ime_engine(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_AVAILABLE_IME_ENGINES] = \ ('GET', '/session/$sessionId/ime/available_engines') self.command_executor._commands[Command.IS_IME_ACTIVE] = \ diff --git a/appium/webdriver/extensions/keyboard.py b/appium/webdriver/extensions/keyboard.py index 612a99b6..029996fc 100644 --- a/appium/webdriver/extensions/keyboard.py +++ b/appium/webdriver/extensions/keyboard.py @@ -12,14 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, Optional, TypeVar + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='Keyboard') + class Keyboard(webdriver.Remote): - def hide_keyboard(self, key_name=None, key=None, strategy=None): + def hide_keyboard(self, key_name: Optional[str] = None, key: Optional[str] + = None, strategy: Optional[str] = None) -> T: """Hides the software keyboard on the device. In iOS, use `key_name` to press @@ -30,7 +35,7 @@ def hide_keyboard(self, key_name=None, key=None, strategy=None): key (:obj:`str`, optional): strategy (:obj:`str`, optional): strategy for closing the keyboard (e.g., `tapOutside`) """ - data = {} + data: Dict[str, Optional[str]] = {} if key_name is not None: data['keyName'] = key_name elif key is not None: @@ -41,7 +46,7 @@ def hide_keyboard(self, key_name=None, key=None, strategy=None): self.execute(Command.HIDE_KEYBOARD, data) return self - def is_keyboard_shown(self): + def is_keyboard_shown(self) -> bool: """Attempts to detect whether a software keyboard is present Returns: @@ -49,7 +54,7 @@ def is_keyboard_shown(self): """ return self.execute(Command.IS_KEYBOARD_SHOWN)['value'] - def keyevent(self, keycode, metastate=None): + def keyevent(self, keycode: int, metastate: Optional[int] = None) -> T: """Sends a keycode to the device. Android only. @@ -70,7 +75,7 @@ def keyevent(self, keycode, metastate=None): self.execute(Command.KEY_EVENT, data) return self - def press_keycode(self, keycode, metastate=None, flags=None): + def press_keycode(self, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None) -> T: """Sends a keycode to the device. Android only. Possible keycodes can be found in http://developer.android.com/reference/android/view/KeyEvent.html. @@ -93,7 +98,7 @@ def press_keycode(self, keycode, metastate=None, flags=None): self.execute(Command.PRESS_KEYCODE, data) return self - def long_press_keycode(self, keycode, metastate=None, flags=None): + def long_press_keycode(self, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None) -> T: """Sends a long press of keycode to the device. Android only. Possible keycodes can be found in http://developer.android.com/reference/android/view/KeyEvent.html. @@ -118,7 +123,7 @@ def long_press_keycode(self, keycode, metastate=None, flags=None): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.HIDE_KEYBOARD] = \ ('POST', '/session/$sessionId/appium/device/hide_keyboard') self.command_executor._commands[Command.IS_KEYBOARD_SHOWN] = \ diff --git a/appium/webdriver/extensions/location.py b/appium/webdriver/extensions/location.py index 7dffcb0e..054cf44e 100644 --- a/appium/webdriver/extensions/location.py +++ b/appium/webdriver/extensions/location.py @@ -12,13 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, Optional, TypeVar, Union + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='Location') + class Location(webdriver.Remote): - def toggle_location_services(self): + def toggle_location_services(self) -> T: """Toggle the location services on the device. Android only. @@ -29,7 +33,10 @@ def toggle_location_services(self): self.execute(Command.TOGGLE_LOCATION_SERVICES, {}) return self - def set_location(self, latitude, longitude, altitude=None): + def set_location(self, + latitude: Union[float, str], + longitude: Union[float, str], + altitude: Union[float, str] = None) -> T: """Set the location of the device Args: @@ -52,7 +59,7 @@ def set_location(self, latitude, longitude, altitude=None): return self @property - def location(self): + def location(self) -> Dict[str, float]: """Retrieves the current location Returns: @@ -65,7 +72,7 @@ def location(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.TOGGLE_LOCATION_SERVICES] = \ ('POST', '/session/$sessionId/appium/device/toggle_location_services') self.command_executor._commands[Command.GET_LOCATION] = \ diff --git a/appium/webdriver/extensions/log_event.py b/appium/webdriver/extensions/log_event.py index 383664d6..b22c2416 100644 --- a/appium/webdriver/extensions/log_event.py +++ b/appium/webdriver/extensions/log_event.py @@ -12,14 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, List, TypeVar, Union + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='LogEvent') + class LogEvent(webdriver.Remote): - def get_events(self, type=None): + def get_events(self, type: List[str] = None) -> Dict[str, Union[str, int]]: """ Retrieves events information from the current session (Since Appium 1.16.0) @@ -35,14 +39,14 @@ def get_events(self, type=None): commands: (`list` of `dict`) List of dictionaries containing the following entries cmd: (str) The command name that has been sent to the appium server startTime: (int) Received time - endTime: (init) Response time + endTime: (int) Response time """ data = {} if type is not None: data['type'] = type return self.execute(Command.GET_EVENTS, data)['value'] - def log_event(self, vendor, event): + def log_event(self, vendor: str, event: str) -> T: """Log a custom event on the Appium server. (Since Appium 1.16.0) @@ -65,7 +69,7 @@ def log_event(self, vendor, event): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_EVENTS] = \ ('POST', '/session/$sessionId/appium/events') self.command_executor._commands[Command.LOG_EVENT] = \ diff --git a/appium/webdriver/extensions/remote_fs.py b/appium/webdriver/extensions/remote_fs.py index 10fbff23..dd256a7a 100644 --- a/appium/webdriver/extensions/remote_fs.py +++ b/appium/webdriver/extensions/remote_fs.py @@ -13,50 +13,54 @@ # limitations under the License. import base64 +from typing import Optional, TypeVar from selenium import webdriver from selenium.common.exceptions import InvalidArgumentException from ..mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='RemoteFS') + class RemoteFS(webdriver.Remote): - def pull_file(self, path): + def pull_file(self, path: str) -> str: """Retrieves the file at `path`. Args: path (str): the path to the file on the device Returns: - bytes: The file's contents as base64. + str: The file's contents encoded as Base64. """ data = { 'path': path, } return self.execute(Command.PULL_FILE, data)['value'] - def pull_folder(self, path): + def pull_folder(self, path: str) -> str: """Retrieves a folder at `path`. Args: path (str): the path to the folder on the device Returns: - bytes: The folder's contents zipped and encoded as Base64. + str: The folder's contents zipped and encoded as Base64. """ data = { 'path': path, } return self.execute(Command.PULL_FOLDER, data)['value'] - def push_file(self, destination_path, base64data=None, source_path=None): + def push_file(self, destination_path: str, + base64data: Optional[str] = None, source_path: Optional[str] = None) -> T: """Puts the data from the file at `source_path`, encoded as Base64, in the file specified as `path`. Specify either `base64data` or `source_path`, if both specified default to `source_path` Args: destination_path (str): the location on the device/simulator where the local file contents should be saved - base64data (:obj:`bytes`, optional): file contents, encoded as Base64, to be written to the file on the device/simulator + base64data (:obj:`str`, optional): file contents, encoded as Base64, to be written to the file on the device/simulator source_path (:obj:`str`, optional): local file path for the file to be loaded on device Returns: @@ -68,11 +72,11 @@ def push_file(self, destination_path, base64data=None, source_path=None): if source_path is not None: try: with open(source_path, 'rb') as f: - data = f.read() + file_data = f.read() except IOError: message = 'source_path {} could not be found. Are you sure the file exists?'.format(source_path) raise InvalidArgumentException(message) - base64data = base64.b64encode(data).decode('utf-8') + base64data = base64.b64encode(file_data).decode('utf-8') data = { 'path': destination_path, @@ -83,7 +87,7 @@ def push_file(self, destination_path, base64data=None, source_path=None): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.PULL_FILE] = \ ('POST', '/session/$sessionId/appium/device/pull_file') self.command_executor._commands[Command.PULL_FOLDER] = \ diff --git a/appium/webdriver/extensions/screen_record.py b/appium/webdriver/extensions/screen_record.py index 0b7f1bc7..8d2abbf0 100644 --- a/appium/webdriver/extensions/screen_record.py +++ b/appium/webdriver/extensions/screen_record.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Union + from selenium import webdriver from ..mobilecommand import MobileCommand as Command @@ -19,7 +21,7 @@ class ScreenRecord(webdriver.Remote): - def start_recording_screen(self, **options): + def start_recording_screen(self, **options: Any) -> Union[bytes, str]: """Start asynchronous screen recording process. Keyword Args: @@ -82,7 +84,7 @@ def start_recording_screen(self, **options): del options['password'] return self.execute(Command.START_RECORDING_SCREEN, {'options': options})['value'] - def stop_recording_screen(self, **options): + def stop_recording_screen(self, **options: Any) -> bytes: """Gather the output from the previously started screen recording to a media file. Keyword Args: @@ -112,7 +114,7 @@ def stop_recording_screen(self, **options): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.START_RECORDING_SCREEN] = \ ('POST', '/session/$sessionId/appium/start_recording_screen') self.command_executor._commands[Command.STOP_RECORDING_SCREEN] = \ diff --git a/appium/webdriver/extensions/search_context/android.py b/appium/webdriver/extensions/search_context/android.py index 7c6f127a..4197eb99 100644 --- a/appium/webdriver/extensions/search_context/android.py +++ b/appium/webdriver/extensions/search_context/android.py @@ -15,16 +15,21 @@ # pylint: disable=abstract-method import json +from typing import TYPE_CHECKING, List, Optional from appium.webdriver.common.mobileby import MobileBy from .base_search_context import BaseSearchContext +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + class AndroidSearchContext(BaseSearchContext): """Define search context for Android""" - def find_element_by_android_data_matcher(self, name=None, args=None, className=None): + def find_element_by_android_data_matcher( + self, name: Optional[str] = None, args: Optional[str] = None, className: Optional[str] = None) -> 'WebElement': """Finds element by [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). @@ -56,7 +61,8 @@ def find_element_by_android_data_matcher(self, name=None, args=None, className=N value=self._build_data_matcher(name=name, args=args, className=className) ) - def find_elements_by_android_data_matcher(self, name=None, args=None, className=None): + def find_elements_by_android_data_matcher( + self, name: Optional[str] = None, args: Optional[str] = None, className: Optional[str] = None) -> List['WebElement']: """Finds elements by [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). @@ -83,7 +89,8 @@ def find_elements_by_android_data_matcher(self, name=None, args=None, className= value=self._build_data_matcher(name=name, args=args, className=className) ) - def _build_data_matcher(self, name=None, args=None, className=None): + def _build_data_matcher(self, name: Optional[str] = None, args: Optional[str] + = None, className: Optional[str] = None) -> str: result = {} for key, value in {'name': name, 'args': args, 'class': className}.items(): @@ -92,7 +99,7 @@ def _build_data_matcher(self, name=None, args=None, className=None): return json.dumps(result) - def find_element_by_android_uiautomator(self, uia_string): + def find_element_by_android_uiautomator(self, uia_string: str) -> 'WebElement': """Finds element by uiautomator in Android. Args: @@ -108,7 +115,7 @@ def find_element_by_android_uiautomator(self, uia_string): """ return self.find_element(by=MobileBy.ANDROID_UIAUTOMATOR, value=uia_string) - def find_elements_by_android_uiautomator(self, uia_string): + def find_elements_by_android_uiautomator(self, uia_string: str) -> List['WebElement']: """Finds elements by uiautomator in Android. Args: @@ -124,7 +131,7 @@ def find_elements_by_android_uiautomator(self, uia_string): """ return self.find_elements(by=MobileBy.ANDROID_UIAUTOMATOR, value=uia_string) - def find_element_by_android_viewtag(self, tag): + def find_element_by_android_viewtag(self, tag: str) -> 'WebElement': """Finds element by [View#tags](https://developer.android.com/reference/android/view/View#tags) in Android. It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). @@ -142,7 +149,7 @@ def find_element_by_android_viewtag(self, tag): """ return self.find_element(by=MobileBy.ANDROID_VIEWTAG, value=tag) - def find_elements_by_android_viewtag(self, tag): + def find_elements_by_android_viewtag(self, tag: str) -> List['WebElement']: """Finds element by [View#tags](https://developer.android.com/reference/android/view/View#tags) in Android. It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). diff --git a/appium/webdriver/extensions/search_context/base_search_context.py b/appium/webdriver/extensions/search_context/base_search_context.py index 500a6e3a..404aa62a 100644 --- a/appium/webdriver/extensions/search_context/base_search_context.py +++ b/appium/webdriver/extensions/search_context/base_search_context.py @@ -14,12 +14,17 @@ # pylint: disable=abstract-method +from typing import TYPE_CHECKING, Dict, List, Optional, Union + +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + class BaseSearchContext(object): """Used by each search context. Dummy find_element/s are for preventing pylint error""" - def find_element(self, by=None, value=None): + def find_element(self, by: str, value: Union[str, Dict] = None) -> 'WebElement': raise NotImplementedError - def find_elements(self, by=None, value=None): + def find_elements(self, by: str, value: Union[str, Dict] = None) -> List['WebElement']: raise NotImplementedError diff --git a/appium/webdriver/extensions/search_context/custom.py b/appium/webdriver/extensions/search_context/custom.py index dabcc6fd..a790016b 100644 --- a/appium/webdriver/extensions/search_context/custom.py +++ b/appium/webdriver/extensions/search_context/custom.py @@ -14,15 +14,20 @@ # pylint: disable=abstract-method +from typing import TYPE_CHECKING, List + from appium.webdriver.common.mobileby import MobileBy from .base_search_context import BaseSearchContext +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + class CustomSearchContext(BaseSearchContext): """Define search context for custom plugin""" - def find_element_by_custom(self, selector): + def find_element_by_custom(self, selector: str) -> 'WebElement': """Finds an element in conjunction with a custom element finding plugin Args: @@ -42,7 +47,7 @@ def find_element_by_custom(self, selector): """ return self.find_element(by=MobileBy.CUSTOM, value=selector) - def find_elements_by_custom(self, selector): + def find_elements_by_custom(self, selector: str) -> List['WebElement']: """Finds elements in conjunction with a custom element finding plugin Args: diff --git a/appium/webdriver/extensions/search_context/ios.py b/appium/webdriver/extensions/search_context/ios.py index ad42ce91..3066ad9e 100644 --- a/appium/webdriver/extensions/search_context/ios.py +++ b/appium/webdriver/extensions/search_context/ios.py @@ -14,15 +14,20 @@ # pylint: disable=abstract-method +from typing import TYPE_CHECKING, List + from appium.webdriver.common.mobileby import MobileBy from .base_search_context import BaseSearchContext +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + class iOSSearchContext(BaseSearchContext): """Define search context for iOS""" - def find_element_by_ios_uiautomation(self, uia_string): + def find_element_by_ios_uiautomation(self, uia_string: str) -> 'WebElement': """Finds an element by uiautomation in iOS. Args: @@ -39,7 +44,7 @@ def find_element_by_ios_uiautomation(self, uia_string): """ return self.find_element(by=MobileBy.IOS_UIAUTOMATION, value=uia_string) - def find_elements_by_ios_uiautomation(self, uia_string): + def find_elements_by_ios_uiautomation(self, uia_string: str) -> List['WebElement']: """Finds elements by uiautomation in iOS. Args: @@ -55,7 +60,7 @@ def find_elements_by_ios_uiautomation(self, uia_string): """ return self.find_elements(by=MobileBy.IOS_UIAUTOMATION, value=uia_string) - def find_element_by_ios_predicate(self, predicate_string): + def find_element_by_ios_predicate(self, predicate_string: str) -> 'WebElement': """Find an element by ios predicate string. Args: @@ -71,7 +76,7 @@ def find_element_by_ios_predicate(self, predicate_string): """ return self.find_element(by=MobileBy.IOS_PREDICATE, value=predicate_string) - def find_elements_by_ios_predicate(self, predicate_string): + def find_elements_by_ios_predicate(self, predicate_string: str) -> List['WebElement']: """Finds elements by ios predicate string. Args: @@ -87,7 +92,7 @@ def find_elements_by_ios_predicate(self, predicate_string): """ return self.find_elements(by=MobileBy.IOS_PREDICATE, value=predicate_string) - def find_element_by_ios_class_chain(self, class_chain_string): + def find_element_by_ios_class_chain(self, class_chain_string: str) -> 'WebElement': """Find an element by ios class chain string. Args: @@ -103,7 +108,7 @@ def find_element_by_ios_class_chain(self, class_chain_string): """ return self.find_element(by=MobileBy.IOS_CLASS_CHAIN, value=class_chain_string) - def find_elements_by_ios_class_chain(self, class_chain_string): + def find_elements_by_ios_class_chain(self, class_chain_string: str) -> List['WebElement']: """Finds elements by ios class chain string. Args: diff --git a/appium/webdriver/extensions/search_context/mobile.py b/appium/webdriver/extensions/search_context/mobile.py index c9d68632..a58f4d23 100644 --- a/appium/webdriver/extensions/search_context/mobile.py +++ b/appium/webdriver/extensions/search_context/mobile.py @@ -15,16 +15,20 @@ # pylint: disable=abstract-method import base64 +from typing import TYPE_CHECKING, List from appium.webdriver.common.mobileby import MobileBy from .base_search_context import BaseSearchContext +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + class MobileSearchContext(BaseSearchContext): """Define search context for Mobile(Android, iOS)""" - def find_element_by_accessibility_id(self, accessibility_id): + def find_element_by_accessibility_id(self, accessibility_id: str) -> 'WebElement': """Finds an element by accessibility id. Args: @@ -42,7 +46,7 @@ def find_element_by_accessibility_id(self, accessibility_id): """ return self.find_element(by=MobileBy.ACCESSIBILITY_ID, value=accessibility_id) - def find_elements_by_accessibility_id(self, accessibility_id): + def find_elements_by_accessibility_id(self, accessibility_id: str) -> List['WebElement']: """Finds elements by accessibility id. Args: @@ -59,7 +63,7 @@ def find_elements_by_accessibility_id(self, accessibility_id): """ return self.find_elements(by=MobileBy.ACCESSIBILITY_ID, value=accessibility_id) - def find_element_by_image(self, img_path): + def find_element_by_image(self, img_path: str) -> 'WebElement': """Finds a portion of a screenshot by an image. Uses driver.find_image_occurrence under the hood. @@ -77,7 +81,7 @@ def find_element_by_image(self, img_path): return self.find_element(by=MobileBy.IMAGE, value=b64_data) - def find_elements_by_image(self, img_path): + def find_elements_by_image(self, img_path: str) -> List['WebElement']: """Finds a portion of a screenshot by an image. Uses driver.find_image_occurrence under the hood. Note that this will diff --git a/appium/webdriver/extensions/search_context/windows.py b/appium/webdriver/extensions/search_context/windows.py index 66ca23a2..692aebce 100644 --- a/appium/webdriver/extensions/search_context/windows.py +++ b/appium/webdriver/extensions/search_context/windows.py @@ -14,15 +14,20 @@ # pylint: disable=abstract-method +from typing import TYPE_CHECKING, List + from appium.webdriver.common.mobileby import MobileBy from .base_search_context import BaseSearchContext +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + class WindowsSearchContext(BaseSearchContext): """Define search context for Windows""" - def find_element_by_windows_uiautomation(self, win_uiautomation): + def find_element_by_windows_uiautomation(self, win_uiautomation: str) -> 'WebElement': """Finds an element by windows uiautomation Args: @@ -39,7 +44,7 @@ def find_element_by_windows_uiautomation(self, win_uiautomation): """ return self.find_element(by=MobileBy.WINDOWS_UI_AUTOMATION, value=win_uiautomation) - def find_elements_by_windows_uiautomation(self, win_uiautomation): + def find_elements_by_windows_uiautomation(self, win_uiautomation: str) -> List['WebElement']: """Finds elements by windows uiautomation Args: diff --git a/appium/webdriver/extensions/session.py b/appium/webdriver/extensions/session.py index 64d49370..a8c097a6 100644 --- a/appium/webdriver/extensions/session.py +++ b/appium/webdriver/extensions/session.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict, List + from selenium import webdriver from appium.common.logger import logger @@ -21,7 +23,7 @@ class Session(webdriver.Remote): @property - def session(self): + def session(self) -> Dict[str, Any]: """ Retrieves session information from the current session Usage: @@ -33,19 +35,19 @@ def session(self): return self.execute(Command.GET_SESSION)['value'] @property - def all_sessions(self): + def all_sessions(self) -> List[Dict[str, Any]]: """ Retrieves all sessions that are open Usage: sessions = driver.all_sessions Returns: - `dict`: containing all open sessions + :obj:`list` of :obj:`dict`: containing all open sessions """ return self.execute(Command.GET_ALL_SESSIONS)['value'] @property - def events(self): + def events(self) -> Dict: """ Retrieves events information from the current session Usage: @@ -63,7 +65,7 @@ def events(self): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_SESSION] = \ ('GET', '/session/$sessionId') self.command_executor._commands[Command.GET_ALL_SESSIONS] = \ diff --git a/appium/webdriver/extensions/settings.py b/appium/webdriver/extensions/settings.py index 2a0f4235..b5514621 100644 --- a/appium/webdriver/extensions/settings.py +++ b/appium/webdriver/extensions/settings.py @@ -12,13 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict, TypeVar + from selenium import webdriver from ..mobilecommand import MobileCommand as Command +T = TypeVar('T', bound='Settings') + class Settings(webdriver.Remote): - def get_settings(self): + def get_settings(self) -> Dict[str, Any]: """Returns the appium server Settings for the current session. Do not get Settings confused with Desired Capabilities, they are @@ -29,7 +33,7 @@ def get_settings(self): """ return self.execute(Command.GET_SETTINGS, {})['value'] - def update_settings(self, settings): + def update_settings(self, settings: Dict[str, Any]) -> T: """Set settings for the current session. For more on settings, see: https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md @@ -44,7 +48,7 @@ def update_settings(self, settings): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: self.command_executor._commands[Command.GET_SETTINGS] = \ ('GET', '/session/$sessionId/appium/settings') self.command_executor._commands[Command.UPDATE_SETTINGS] = \ diff --git a/appium/webdriver/switch_to.py b/appium/webdriver/switch_to.py index d496b7a3..e0266510 100644 --- a/appium/webdriver/switch_to.py +++ b/appium/webdriver/switch_to.py @@ -12,13 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import TypeVar + from selenium.webdriver.remote.switch_to import SwitchTo from .mobilecommand import MobileCommand +T = TypeVar('T', bound='MobileSwitchTo') + class MobileSwitchTo(SwitchTo): - def context(self, context_name): + def context(self, context_name: str) -> T: """Sets the context for the current session. Args: @@ -28,3 +32,4 @@ def context(self, context_name): driver.switch_to.context('WEBVIEW_1') """ self._driver.execute(MobileCommand.SWITCH_TO_CONTEXT, {'name': context_name}) + return self diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 03e7e3fd..13e9619f 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 +from typing import Any, Dict, List, Optional, TypeVar, Union from selenium.common.exceptions import InvalidArgumentException from selenium.webdriver.common.by import By @@ -84,7 +85,7 @@ # Add appium prefix for the non-W3C capabilities -def _make_w3c_caps(caps): +def _make_w3c_caps(caps: Dict) -> Dict[str, List[Dict[str, Any]]]: appium_prefix = 'appium:' caps = copy.deepcopy(caps) @@ -111,6 +112,9 @@ def _make_w3c_caps(caps): return {'firstMatch': [first_match]} +T = TypeVar('T', bound='WebDriver') + + class WebDriver( AppiumSearchContext, ActionHelpers, @@ -141,8 +145,8 @@ class WebDriver( SystemBars ): - def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', - desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=True, direct_connection=False): + def __init__(self, command_executor: str = 'http://127.0.0.1:4444/wd/hub', + desired_capabilities: Optional[Dict] = None, browser_profile: str = None, proxy: str = None, keep_alive: bool = True, direct_connection: bool = False): super(WebDriver, self).__init__( AppiumConnection(command_executor, keep_alive=keep_alive), @@ -171,7 +175,7 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', By.IMAGE = MobileBy.IMAGE By.CUSTOM = MobileBy.CUSTOM - def _update_command_executor(self, keep_alive): + def _update_command_executor(self, keep_alive: bool) -> None: """Update command executor following directConnect feature""" direct_protocol = 'directConnectProtocol' direct_host = 'directConnectHost' @@ -201,7 +205,7 @@ def _update_command_executor(self, keep_alive): self.command_executor = RemoteConnection(executor, keep_alive=keep_alive) self._addCommands() - def start_session(self, capabilities, browser_profile=None): + def start_session(self, capabilities: Dict, browser_profile: Optional[str] = None) -> None: """Creates a new session with the desired capabilities. Override for Appium @@ -218,9 +222,11 @@ def start_session(self, capabilities, browser_profile=None): raise InvalidArgumentException('Capabilities must be a dictionary') if browser_profile: if 'moz:firefoxOptions' in capabilities: - capabilities['moz:firefoxOptions']['profile'] = browser_profile.encoded + # encoded is defined in selenium's original codes + capabilities['moz:firefoxOptions']['profile'] = browser_profile.encoded # type: ignore else: - capabilities.update({'firefox_profile': browser_profile.encoded}) + # encoded is defined in selenium's original codes + capabilities.update({'firefox_profile': browser_profile.encoded}) # type: ignore parameters = self._merge_capabilities(capabilities) @@ -239,7 +245,7 @@ def start_session(self, capabilities, browser_profile=None): self.w3c = response.get('status') is None self.command_executor.w3c = self.w3c - def _merge_capabilities(self, capabilities): + def _merge_capabilities(self, capabilities: Dict) -> Dict[str, Any]: """Manage capabilities whether W3C format or MJSONWP format """ if _FORCE_MJSONWP in capabilities: @@ -252,7 +258,7 @@ def _merge_capabilities(self, capabilities): w3c_caps = _make_w3c_caps(capabilities) return {'capabilities': w3c_caps, 'desiredCapabilities': capabilities} - def find_element(self, by=By.ID, value=None): + def find_element(self, by: str = By.ID, value: Union[str, Dict] = None) -> MobileWebElement: """'Private' method used by the find_element_by_* methods. Override for Appium @@ -283,7 +289,8 @@ def find_element(self, by=By.ID, value=None): 'using': by, 'value': value})['value'] - def find_elements(self, by=By.ID, value=None): + def find_elements(self, by: str = By.ID, value: Union[str, Dict] + = None) -> Union[List[MobileWebElement], List]: """'Private' method used by the find_elements_by_* methods. Override for Appium @@ -317,7 +324,7 @@ def find_elements(self, by=By.ID, value=None): 'using': by, 'value': value})['value'] or [] - def create_web_element(self, element_id, w3c=False): + def create_web_element(self, element_id: int, w3c: bool = False) -> MobileWebElement: """Creates a web element with the specified element_id. Overrides method in Selenium WebDriver in order to always give them @@ -332,7 +339,7 @@ def create_web_element(self, element_id, w3c=False): """ return MobileWebElement(self, element_id, w3c) - def set_value(self, element, value): + def set_value(self, element: MobileWebElement, value: str) -> T: """Set the value on an element in the application. Args: @@ -351,7 +358,7 @@ def set_value(self, element, value): # pylint: disable=protected-access - def _addCommands(self): + def _addCommands(self) -> None: # call the overridden command binders from all mixin classes except for # appium.webdriver.webdriver.WebDriver and its sub-classes # https://github.com/appium/python-client/issues/342 diff --git a/appium/webdriver/webelement.py b/appium/webdriver/webelement.py index bf8efc5b..ce9e8f80 100644 --- a/appium/webdriver/webelement.py +++ b/appium/webdriver/webelement.py @@ -12,21 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, List, Optional, TypeVar, Union + from selenium.webdriver.common.by import By from selenium.webdriver.remote.command import Command as RemoteCommand from .extensions.search_context import AppiumWebElementSearchContext from .mobilecommand import MobileCommand as Command -# Python 3 imports -try: - str = basestring -except NameError: - pass +T = TypeVar('T', bound='WebElement') class WebElement(AppiumWebElementSearchContext): - def get_attribute(self, name): + def get_attribute(self, name: str) -> Optional[str]: """Gets the given attribute or property of the element. Override for Appium @@ -58,22 +56,23 @@ def get_attribute(self, name): if attributeValue is None: return None + # Convert to str along to the spec if not isinstance(attributeValue, str): - attributeValue = unicode(attributeValue) + attributeValue = str(attributeValue) if name != 'value' and attributeValue.lower() in ('true', 'false'): return attributeValue.lower() return attributeValue - def is_displayed(self): + def is_displayed(self) -> bool: """Whether the element is visible to a user. Override for Appium """ return self._execute(RemoteCommand.IS_ELEMENT_DISPLAYED)['value'] - def find_element(self, by=By.ID, value=None): + def find_element(self, by: str = By.ID, value: Union[str, Dict] = None) -> T: """Find an element given a By strategy and locator Override for Appium @@ -107,7 +106,7 @@ def find_element(self, by=By.ID, value=None): return self._execute(RemoteCommand.FIND_CHILD_ELEMENT, {"using": by, "value": value})['value'] - def find_elements(self, by=By.ID, value=None): + def find_elements(self, by: str = By.ID, value: Union[str, Dict] = None) -> List[T]: """Find elements given a By strategy and locator Override for Appium @@ -141,7 +140,7 @@ def find_elements(self, by=By.ID, value=None): return self._execute(RemoteCommand.FIND_CHILD_ELEMENTS, {"using": by, "value": value})['value'] - def clear(self): + def clear(self) -> T: """Clears text. Override for Appium @@ -153,7 +152,7 @@ def clear(self): self._execute(Command.CLEAR, data) return self - def set_text(self, keys=''): + def set_text(self, keys: str = '') -> T: """Sends text to the element. Previous text is removed. @@ -176,7 +175,7 @@ def set_text(self, keys=''): return self @property - def location_in_view(self): + def location_in_view(self) -> Dict[str, int]: """Gets the location of an element relative to the view. Usage: @@ -189,7 +188,7 @@ def location_in_view(self): """ return self._execute(Command.LOCATION_IN_VIEW)['value'] - def set_value(self, value): + def set_value(self, value: str) -> T: """Set the value on this element in the application Args: diff --git a/ci.sh b/ci.sh index 0fb53862..a0b1d8fc 100755 --- a/ci.sh +++ b/ci.sh @@ -34,4 +34,11 @@ if [[ $? -ne 0 ]] ; then EXIT_STATUS=1 fi +( + python -m mypy appium +) +if [[ $? -ne 0 ]] ; then + EXIT_STATUS=1 +fi + exit $EXIT_STATUS diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..42606f97 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,9 @@ +[mypy] +check_untyped_defs = True +disallow_untyped_calls = True +disallow_untyped_defs = True +follow_imports = skip +ignore_missing_imports = True +strict_optional = True +warn_redundant_casts = True +warn_unused_ignores = True diff --git a/test/functional/android/remote_fs_tests.py b/test/functional/android/remote_fs_tests.py index fefe0839..1a7a0092 100644 --- a/test/functional/android/remote_fs_tests.py +++ b/test/functional/android/remote_fs_tests.py @@ -19,15 +19,13 @@ from io import BytesIO from zipfile import ZipFile -from appium.common.helper import appium_bytes - from .helper.test_helper import BaseTestCase class RemoteFsTests(BaseTestCase): def test_push_pull_file(self): dest_path = '/data/local/tmp/test_push_file.txt' - data = appium_bytes('This is the contents of the file to push to the device.', 'utf-8') + data = bytes('This is the contents of the file to push to the device.', 'utf-8') self.driver.push_file(dest_path, base64.b64encode(data).decode('utf-8')) data_ret = base64.b64decode(self.driver.pull_file(dest_path)) @@ -35,7 +33,7 @@ def test_push_pull_file(self): self.assertEqual(data, data_ret) def test_pull_folder(self): - data = appium_bytes('random string data {}'.format(random.randint(0, 1000)), 'utf-8') + data = bytes('random string data {}'.format(random.randint(0, 1000)), 'utf-8') dest_dir = '/data/local/tmp/' for filename in ['1.txt', '2.txt']: diff --git a/test/unit/webdriver/device/clipboard_test.py b/test/unit/webdriver/device/clipboard_test.py index 22c80dbe..01176029 100644 --- a/test/unit/webdriver/device/clipboard_test.py +++ b/test/unit/webdriver/device/clipboard_test.py @@ -14,7 +14,6 @@ import httpretty -from appium.common.helper import appium_bytes from appium.webdriver.clipboard_content_type import ClipboardContentType from test.unit.helper.test_helper import ( android_w3c_driver, @@ -34,7 +33,7 @@ def test_set_clipboard_with_url(self): appium_command('/session/1234567890/appium/device/set_clipboard'), body='{"value": ""}' ) - driver.set_clipboard(appium_bytes(str('http://appium.io/'), 'UTF-8'), + driver.set_clipboard(bytes(str('http://appium.io/'), 'UTF-8'), ClipboardContentType.URL, 'label for android') d = get_httpretty_request_body(httpretty.last_request()) diff --git a/test/unit/webdriver/device/remote_fs_test.py b/test/unit/webdriver/device/remote_fs_test.py index 0855b936..dace0ffc 100644 --- a/test/unit/webdriver/device/remote_fs_test.py +++ b/test/unit/webdriver/device/remote_fs_test.py @@ -18,7 +18,6 @@ import pytest from selenium.common.exceptions import InvalidArgumentException -from appium.common.helper import appium_bytes from appium.webdriver.webdriver import WebDriver from test.unit.helper.test_helper import ( android_w3c_driver, @@ -37,7 +36,7 @@ def test_push_file(self): appium_command('/session/1234567890/appium/device/push_file'), ) dest_path = '/path/to/file.txt' - data = base64.b64encode(appium_bytes('HelloWorld', 'utf-8')).decode('utf-8') + data = base64.b64encode(bytes('HelloWorld', 'utf-8')).decode('utf-8') assert isinstance(driver.push_file(dest_path, data), WebDriver) @@ -80,7 +79,7 @@ def test_pull_file(self): ) dest_path = '/path/to/file.txt' - assert driver.pull_file(dest_path) == str(base64.b64encode(appium_bytes('HelloWorld', 'utf-8')).decode('utf-8')) + assert driver.pull_file(dest_path) == str(base64.b64encode(bytes('HelloWorld', 'utf-8')).decode('utf-8')) d = get_httpretty_request_body(httpretty.last_request()) assert d['path'] == dest_path