Skip to content

Commit eebff65

Browse files
authored
Add explicit type declarations (appium#482)
* Fixed mypy warning: touch_action.py * Fixed mypy warning: multi_action.py * Fixed mypy warning: extensions/android * Fixed mypy warning: extensions/search_context * Updated * Revert some changes to run unit test * Review comments * Updates * Updates * Add mypy check to ci.sh * Add mypy to Pipfile * Updates * Update README * Revert unexpected changes * Updates Dict * Revert unexpected changes * Updates * Review comments * Review comments * tweak * Restore and modify changes * Fix wrong return type * Add comments * Revert unexpected changes * Fix mypy error * updates
1 parent e821a31 commit eebff65

51 files changed

Lines changed: 482 additions & 289 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Pipfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ mock = "~=3.0"
2424
pylint = "~=2.4"
2525
astroid = "~=2.3"
2626
isort = "~=4.3"
27+
28+
mypy = "==0.761"

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,18 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz).
4545

4646
- Style Guide: https://www.python.org/dev/peps/pep-0008/
4747
- `autopep8` helps to format code automatically
48-
```
48+
```shell
4949
$ python -m autopep8 -r --global-config .config-pep8 -i .
5050
```
5151
- `isort` helps to order imports automatically
52-
```
52+
```shell
5353
$ python -m isort -rc .
5454
```
5555
- When you use newly 3rd party modules, add it to [.isort.cfg](.isort.cfg) to keep import order correct
56+
- `mypy` helps to check explicit type declarations
57+
```shell
58+
$ python -m mypy appium
59+
```
5660
- Docstring style: Google Style
5761
- Refer [link](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html)
5862
- You can customise `CHANGELOG.rst` with commit messages following [.gitchangelog.rc](.gitchangelog.rc)

appium/common/helper.py

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,44 +12,25 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from collections import OrderedDict
15+
from typing import Dict
1616

1717
from appium import version as appium_version
1818

1919

20-
def appium_bytes(value, encoding):
21-
"""Return a bytes-like object
22-
23-
Has _appium_ prefix to avoid overriding built-in bytes.
24-
25-
Args:
26-
value (str): A value to convert
27-
encoding (str): A encoding which will convert to
28-
29-
Returns:
30-
str: A bytes-like object
31-
"""
32-
33-
try:
34-
return bytes(value, encoding) # Python 3
35-
except TypeError:
36-
return value # Python 2
37-
38-
39-
def extract_const_attributes(cls):
20+
def extract_const_attributes(cls: type) -> Dict:
4021
"""Return dict with constants attributes and values in the class(e.g. {'VAL1': 1, 'VAL2': 2})
4122
4223
Args:
4324
cls (type): Class to be extracted constants
4425
4526
Returns:
46-
OrderedDict: dict with constants attributes and values in the class
27+
dict: dict with constants attributes and values in the class
4728
"""
48-
return OrderedDict(
49-
[(attr, value) for attr, value in vars(cls).items() if not callable(getattr(cls, attr)) and attr.isupper()])
29+
return dict([(attr, value) for attr, value in vars(cls).items()
30+
if not callable(getattr(cls, attr)) and attr.isupper()])
5031

5132

52-
def library_version():
33+
def library_version() -> str:
5334
"""Return a version of this python library
5435
"""
5536

appium/common/logger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import sys
1717

1818

19-
def setup_logger(level=logging.NOTSET):
19+
def setup_logger(level: int = logging.NOTSET) -> None:
2020
logger.propagate = False
2121
logger.setLevel(level)
2222
handler = logging.StreamHandler(stream=sys.stderr)

appium/saucetestcase.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import os
2020
import sys
2121
import unittest
22+
from typing import Any, Callable, List
2223

2324
from sauceclient import SauceClient
2425

@@ -29,8 +30,8 @@
2930
sauce = SauceClient(SAUCE_USERNAME, SAUCE_ACCESS_KEY)
3031

3132

32-
def on_platforms(platforms):
33-
def decorator(base_class):
33+
def on_platforms(platforms: List[str]) -> Callable[[type], None]:
34+
def decorator(base_class: type) -> None:
3435
module = sys.modules[base_class.__module__].__dict__
3536
for i, platform in enumerate(platforms):
3637
name = "%s_%s" % (base_class.__name__, i + 1)
@@ -40,16 +41,16 @@ def decorator(base_class):
4041

4142

4243
class SauceTestCase(unittest.TestCase):
43-
def setUp(self):
44-
self.desired_capabilities['name'] = self.id()
44+
def setUp(self) -> None:
45+
self.desired_capabilities['name'] = self.id() # type: ignore
4546
sauce_url = "http://%s:%[email protected]:80/wd/hub"
4647
self.driver = webdriver.Remote(
47-
desired_capabilities=self.desired_capabilities,
48+
desired_capabilities=self.desired_capabilities, # type: ignore
4849
command_executor=sauce_url % (SAUCE_USERNAME, SAUCE_ACCESS_KEY)
4950
)
5051
self.driver.implicitly_wait(30)
5152

52-
def tearDown(self):
53+
def tearDown(self) -> None:
5354
print("Link to your job: https://saucelabs.com/jobs/%s" % self.driver.session_id)
5455
try:
5556
if sys.exc_info() == (None, None, None):

appium/webdriver/appium_connection.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import Any, Dict
16+
1517
from selenium.webdriver.remote.remote_connection import RemoteConnection
1618

1719
from appium.common.helper import library_version
@@ -20,7 +22,7 @@
2022
class AppiumConnection(RemoteConnection):
2123

2224
@classmethod
23-
def get_remote_connection_headers(cls, parsed_url, keep_alive=True):
25+
def get_remote_connection_headers(cls, parsed_url: str, keep_alive: bool = True) -> Dict[str, Any]:
2426
"""Override get_remote_connection_headers in RemoteConnection"""
2527
headers = RemoteConnection.get_remote_connection_headers(parsed_url, keep_alive=keep_alive)
2628
headers['User-Agent'] = 'appium/python {} ({})'.format(library_version(), headers['User-Agent'])

appium/webdriver/appium_service.py

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
1615
import os
17-
import subprocess
16+
import subprocess as sp
1817
import sys
1918
import time
19+
from typing import Any, List, Optional, TypeVar, Union
2020

2121
import urllib3
2222

@@ -27,7 +27,7 @@
2727
STATUS_URL = '/wd/hub/status'
2828

2929

30-
def find_executable(executable):
30+
def find_executable(executable: str) -> Optional[str]:
3131
path = os.environ['PATH']
3232
paths = path.split(os.pathsep)
3333
base, ext = os.path.splitext(executable)
@@ -45,7 +45,7 @@ def find_executable(executable):
4545
return None
4646

4747

48-
def poll_url(host, port, path, timeout_ms):
48+
def poll_url(host: str, port: int, path: str, timeout_ms: int) -> bool:
4949
time_started_sec = time.time()
5050
while time.time() < time_started_sec + timeout_ms / 1000.0:
5151
try:
@@ -64,62 +64,65 @@ class AppiumServiceError(RuntimeError):
6464
pass
6565

6666

67+
T = TypeVar('T', bound='AppiumService')
68+
69+
6770
class AppiumService(object):
68-
def __init__(self):
69-
self._process = None
70-
self._cmd = None
71+
def __init__(self) -> None:
72+
self._process: Optional[sp.Popen] = None
73+
self._cmd: Optional[List] = None
7174

72-
def _get_node(self):
75+
def _get_node(self) -> str:
7376
if not hasattr(self, '_node_executable'):
7477
self._node_executable = find_executable('node')
7578
if self._node_executable is None:
7679
raise AppiumServiceError('NodeJS main executable cannot be found. ' +
7780
'Make sure it is installed and present in PATH')
7881
return self._node_executable
7982

80-
def _get_npm(self):
83+
def _get_npm(self) -> str:
8184
if not hasattr(self, '_npm_executable'):
8285
self._npm_executable = find_executable('npm.cmd' if sys.platform == 'win32' else 'npm')
8386
if self._npm_executable is None:
8487
raise AppiumServiceError('Node Package Manager executable cannot be found. ' +
8588
'Make sure it is installed and present in PATH')
8689
return self._npm_executable
8790

88-
def _get_main_script(self):
91+
def _get_main_script(self) -> Union[str, bytes]:
8992
if not hasattr(self, '_main_script'):
9093
for args in [['root', '-g'], ['root']]:
9194
try:
92-
modules_root = subprocess.check_output([self._get_npm()] + args).strip().decode('utf-8')
95+
modules_root = sp.check_output([self._get_npm()] + args).strip().decode('utf-8')
9396
if os.path.exists(os.path.join(modules_root, MAIN_SCRIPT_PATH)):
94-
self._main_script = os.path.join(modules_root, MAIN_SCRIPT_PATH)
97+
self._main_script: Union[str, bytes] = os.path.join(modules_root, MAIN_SCRIPT_PATH)
9598
break
96-
except subprocess.CalledProcessError:
99+
except sp.CalledProcessError:
97100
continue
98101
if not hasattr(self, '_main_script'):
99102
try:
100-
self._main_script = subprocess.check_output(
103+
self._main_script = sp.check_output(
101104
[self._get_node(),
102105
'-e',
103106
'console.log(require.resolve("{}"))'.format(MAIN_SCRIPT_PATH)]).strip()
104-
except subprocess.CalledProcessError as e:
107+
except sp.CalledProcessError as e:
105108
raise AppiumServiceError(e.output)
106109
return self._main_script
107110

108111
@staticmethod
109-
def _parse_port(args):
112+
def _parse_port(args: List[str]) -> int:
110113
for idx, arg in enumerate(args or []):
111114
if arg in ('--port', '-p') and idx < len(args) - 1:
112115
return int(args[idx + 1])
113116
return DEFAULT_PORT
114117

115118
@staticmethod
116-
def _parse_host(args):
119+
def _parse_host(args: List[str]) -> str:
117120
for idx, arg in enumerate(args or []):
118121
if arg in ('--address', '-a') and idx < len(args) - 1:
119122
return args[idx + 1]
120123
return DEFAULT_HOST
121124

122-
def start(self, **kwargs):
125+
def start(self, **kwargs: Any) -> sp.Popen:
123126
"""Starts Appium service with given arguments.
124127
125128
The service will be forcefully restarted if it is already running.
@@ -153,31 +156,31 @@ def start(self, **kwargs):
153156

154157
env = kwargs['env'] if 'env' in kwargs else None
155158
node = kwargs['node'] if 'node' in kwargs else self._get_node()
156-
stdout = kwargs['stdout'] if 'stdout' in kwargs else subprocess.PIPE
157-
stderr = kwargs['stderr'] if 'stderr' in kwargs else subprocess.PIPE
159+
stdout = kwargs['stdout'] if 'stdout' in kwargs else sp.PIPE
160+
stderr = kwargs['stderr'] if 'stderr' in kwargs else sp.PIPE
158161
timeout_ms = int(kwargs['timeout_ms']) if 'timeout_ms' in kwargs else STARTUP_TIMEOUT_MS
159162
main_script = kwargs['main_script'] if 'main_script' in kwargs else self._get_main_script()
160163
args = [node, main_script]
161164
if 'args' in kwargs:
162165
args.extend(kwargs['args'])
163166
self._cmd = args
164-
self._process = subprocess.Popen(args=args, stdout=stdout, stderr=stderr, env=env)
167+
self._process = sp.Popen(args=args, stdout=stdout, stderr=stderr, env=env)
165168
host = self._parse_host(args)
166169
port = self._parse_port(args)
167-
error_msg = None
170+
error_msg: Optional[str] = None
168171
if not self.is_running or (timeout_ms > 0 and not poll_url(host, port, STATUS_URL, timeout_ms)):
169172
error_msg = 'Appium has failed to start on {}:{} within {}ms timeout'\
170173
.format(host, port, timeout_ms)
171174
if error_msg is not None:
172-
if stderr == subprocess.PIPE:
175+
if stderr == sp.PIPE:
173176
err_output = self._process.stderr.read()
174177
if err_output:
175-
error_msg += '\nOriginal error: {}'.format(err_output)
178+
error_msg += '\nOriginal error: {}'.format(str(err_output))
176179
self.stop()
177180
raise AppiumServiceError(error_msg)
178181
return self._process
179182

180-
def stop(self):
183+
def stop(self) -> bool:
181184
"""Stops Appium service if it is running.
182185
183186
The call will be ignored if the service is not running
@@ -188,14 +191,14 @@ def stop(self):
188191
"""
189192
is_terminated = False
190193
if self.is_running:
191-
self._process.terminate()
194+
self._process.terminate() # type: ignore
192195
is_terminated = True
193196
self._process = None
194197
self._cmd = None
195198
return is_terminated
196199

197200
@property
198-
def is_running(self):
201+
def is_running(self) -> bool:
199202
"""Check if the service is running.
200203
201204
Returns:
@@ -204,7 +207,7 @@ def is_running(self):
204207
return self._process is not None and self._process.poll() is None
205208

206209
@property
207-
def is_listening(self):
210+
def is_listening(self) -> bool:
208211
"""Check if the service is listening on the given/default host/port.
209212
210213
The fact, that the service is running, does not always mean it is listening.

appium/webdriver/common/multi_action.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,25 @@
1919
# chaining as the spec requires.
2020

2121
import copy
22+
from typing import TYPE_CHECKING, Dict, List, Optional, TypeVar, Union
2223

2324
from appium.webdriver.mobilecommand import MobileCommand as Command
2425

26+
if TYPE_CHECKING:
27+
from appium.webdriver.webdriver import WebDriver
28+
from appium.webdriver.webelement import WebElement
29+
from appium.webdriver.common.touch_action import TouchAction
30+
31+
T = TypeVar('T', bound='MultiAction')
32+
2533

2634
class MultiAction(object):
27-
def __init__(self, driver, element=None):
35+
def __init__(self, driver: 'WebDriver', element: Optional['WebElement'] = None) -> None:
2836
self._driver = driver
2937
self._element = element
30-
self._touch_actions = []
38+
self._touch_actions: List['TouchAction'] = []
3139

32-
def add(self, *touch_actions):
40+
def add(self, *touch_actions: 'TouchAction') -> None:
3341
"""Add TouchAction objects to the MultiAction, to be performed later.
3442
3543
Args:
@@ -49,7 +57,7 @@ def add(self, *touch_actions):
4957

5058
self._touch_actions.append(copy.copy(touch_action))
5159

52-
def perform(self):
60+
def perform(self: T) -> T:
5361
"""Perform the actions stored in the object.
5462
5563
Usage:
@@ -68,7 +76,7 @@ def perform(self):
6876
return self
6977

7078
@property
71-
def json_wire_gestures(self):
79+
def json_wire_gestures(self) -> Dict[str, Union[List, str]]:
7280
actions = []
7381
for action in self._touch_actions:
7482
actions.append(action.json_wire_gestures)

0 commit comments

Comments
 (0)