Add type information to adb module.
mypy still complains about a few things here, but that looks to be
mostly coming from code that is very, very dead in python 3.
The tests don't run, and haven't since the python 3 switch. Will try to
revive those next, but it likely requires moving files around to fix the
package structure (source needs to go in a subdirectory to make a real
package, as do the tests).
Bug: None
Test: mypy . && pylint .
Change-Id: Ide55a41babecbd6684b73787b17e7f5fdb81c090
diff --git a/python-packages/adb/device.py b/python-packages/adb/device.py
index 2a6ebf7..1770e40 100644
--- a/python-packages/adb/device.py
+++ b/python-packages/adb/device.py
@@ -13,12 +13,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+from __future__ import annotations
+
import atexit
import base64
import logging
import os
import re
import subprocess
+from typing import Any, Callable
class FindDeviceError(RuntimeError):
@@ -26,19 +29,21 @@
class DeviceNotFoundError(FindDeviceError):
- def __init__(self, serial):
+ def __init__(self, serial: str) -> None:
self.serial = serial
super(DeviceNotFoundError, self).__init__(
'No device with serial {}'.format(serial))
class NoUniqueDeviceError(FindDeviceError):
- def __init__(self):
+ def __init__(self) -> None:
super(NoUniqueDeviceError, self).__init__('No unique device')
class ShellError(RuntimeError):
- def __init__(self, cmd, stdout, stderr, exit_code):
+ def __init__(
+ self, cmd: list[str], stdout: str, stderr: str, exit_code: int
+ ) -> None:
super(ShellError, self).__init__(
'`{0}` exited with code {1}'.format(cmd, exit_code))
self.cmd = cmd
@@ -47,7 +52,7 @@
self.exit_code = exit_code
-def get_devices(adb_path='adb'):
+def get_devices(adb_path: str = 'adb') -> list[str]:
with open(os.devnull, 'wb') as devnull:
subprocess.check_call([adb_path, 'start-server'], stdout=devnull,
stderr=devnull)
@@ -68,21 +73,27 @@
return devices
-def _get_unique_device(product=None, adb_path='adb'):
+def _get_unique_device(
+ product: str | None = None, adb_path: str = 'adb'
+) -> AndroidDevice:
devices = get_devices(adb_path=adb_path)
if len(devices) != 1:
raise NoUniqueDeviceError()
return AndroidDevice(devices[0], product, adb_path)
-def _get_device_by_serial(serial, product=None, adb_path='adb'):
+def _get_device_by_serial(
+ serial: str, product: str | None = None, adb_path: str = 'adb'
+) -> AndroidDevice:
for device in get_devices(adb_path=adb_path):
if device == serial:
return AndroidDevice(serial, product, adb_path)
raise DeviceNotFoundError(serial)
-def get_device(serial=None, product=None, adb_path='adb'):
+def get_device(
+ serial: str | None = None, product: str | None = None, adb_path: str = 'adb'
+) -> AndroidDevice:
"""Get a uniquely identified AndroidDevice if one is available.
Raises:
@@ -113,7 +124,7 @@
return _get_unique_device(product, adb_path=adb_path)
-def _get_device_by_type(flag, adb_path):
+def _get_device_by_type(flag: str, adb_path: str) -> AndroidDevice:
with open(os.devnull, 'wb') as devnull:
subprocess.check_call([adb_path, 'start-server'], stdout=devnull,
stderr=devnull)
@@ -127,7 +138,7 @@
return _get_device_by_serial(serial, adb_path=adb_path)
-def get_usb_device(adb_path='adb'):
+def get_usb_device(adb_path: str = 'adb') -> AndroidDevice:
"""Get the unique USB-connected AndroidDevice if it is available.
Raises:
@@ -140,7 +151,7 @@
return _get_device_by_type('-d', adb_path=adb_path)
-def get_emulator_device(adb_path='adb'):
+def get_emulator_device(adb_path: str = 'adb') -> AndroidDevice:
"""Get the unique emulator AndroidDevice if it is available.
Raises:
@@ -153,25 +164,31 @@
return _get_device_by_type('-e', adb_path=adb_path)
+# TODO: Refactor so this invoked subprocess rather than returning arguments for it.
+# This function is pretty type-resistant because it returns the arguments that should be
+# passed to subprocess rather than the result of the call. Most of what's here looks
+# like python2 workarounds anyway, so it might be something that can be done away with.
+# For now, just return Any :(
+#
# If necessary, modifies subprocess.check_output() or subprocess.Popen() args
# to run the subprocess via Windows PowerShell to work-around an issue in
# Python 2's subprocess class on Windows where it doesn't support Unicode.
-def _get_subprocess_args(args):
+def _get_subprocess_args(args: tuple[Any, ...]) -> tuple[Any, ...]:
# Only do this slow work-around if Unicode is in the cmd line on Windows.
# PowerShell takes 600-700ms to startup on a 2013-2014 machine, which is
# very slow.
if os.name != 'nt' or all(not isinstance(arg, unicode) for arg in args[0]):
- return args
+ return tuple(args)
- def escape_arg(arg):
+ def escape_arg(arg: str) -> str:
# Escape for the parsing that the C Runtime does in Windows apps. In
# particular, this will take care of double-quotes.
arg = subprocess.list2cmdline([arg])
# Escape single-quote with another single-quote because we're about
# to...
- arg = arg.replace(u"'", u"''")
+ arg = arg.replace("'", "''")
# ...put the arg in a single-quoted string for PowerShell to parse.
- arg = u"'" + arg + u"'"
+ arg = "'" + arg + "'"
return arg
# Escape command line args.
@@ -188,19 +205,19 @@
ps_code += u'\r\nExit $LastExitCode'
# Encode as UTF-16LE (without Byte-Order-Mark) which Windows natively
# understands.
- ps_code = ps_code.encode('utf-16le')
+ ps_code_encoded = ps_code.encode('utf-16le')
# Encode the PowerShell command as base64 and use the special
# -EncodedCommand option that base64 decodes. Base64 is just plain ASCII,
# so it should have no problem passing through Win32 CreateProcessA()
# (which python erroneously calls instead of CreateProcessW()).
return (['powershell.exe', '-NoProfile', '-NonInteractive',
- '-EncodedCommand', base64.b64encode(ps_code)],) + args[1:]
+ '-EncodedCommand', base64.b64encode(ps_code_encoded)],) + args[1:]
# Call this instead of subprocess.check_output() to work-around issue in Python
# 2's subprocess class on Windows where it doesn't support Unicode.
-def _subprocess_check_output(*args, **kwargs):
+def _subprocess_check_output(*args: Any, **kwargs: Any) -> Any:
try:
return subprocess.check_output(*_get_subprocess_args(args), **kwargs)
except subprocess.CalledProcessError as e:
@@ -210,17 +227,17 @@
# Call this instead of subprocess.Popen(). Like _subprocess_check_output().
-def _subprocess_Popen(*args, **kwargs):
+def _subprocess_Popen(*args: Any, **kwargs: Any) -> Any:
return subprocess.Popen(*_get_subprocess_args(args), **kwargs)
-def split_lines(s):
+def split_lines(s: str) -> list[str]:
"""Splits lines in a way that works even on Windows and old devices.
Windows will see \r\n instead of \n, old devices do the same, old devices
on Windows will see \r\r\n.
"""
- # rstrip is used here to workaround a difference between splineslines and
+ # rstrip is used here to workaround a difference between splitlines and
# re.split:
# >>> 'foo\n'.splitlines()
# ['foo']
@@ -229,12 +246,11 @@
return re.split(r'[\r\n]+', s.rstrip())
-def version(adb_path=None):
+def version(adb_path: list[str] | None = None) -> int:
"""Get the version of adb (in terms of ADB_SERVER_VERSION)."""
adb_path = adb_path if adb_path is not None else ['adb']
- version_output = subprocess.check_output(adb_path + ['version'])
- version_output = version_output.decode('utf-8')
+ version_output = subprocess.check_output(adb_path + ['version'], encoding='utf-8')
pattern = r'^Android Debug Bridge version 1.0.(\d+)$'
result = re.match(pattern, version_output.splitlines()[0])
if not result:
@@ -259,28 +275,30 @@
_RETURN_CODE_SEARCH_LENGTH = len(
'{0}255\r\r\n'.format(_RETURN_CODE_DELIMITER))
- def __init__(self, serial, product=None, adb_path='adb'):
+ def __init__(
+ self, serial: str | None, product: str | None = None, adb_path: str = 'adb'
+ ) -> None:
self.serial = serial
self.product = product
self.adb_path = adb_path
self.adb_cmd = [adb_path]
if self.serial is not None:
- self.adb_cmd.extend(['-s', serial])
+ self.adb_cmd.extend(['-s', self.serial])
if self.product is not None:
- self.adb_cmd.extend(['-p', product])
- self._linesep = None
- self._features = None
+ self.adb_cmd.extend(['-p', self.product])
+ self._linesep: str | None = None
+ self._features: list[str] | None = None
@property
- def linesep(self):
+ def linesep(self) -> str:
if self._linesep is None:
self._linesep = subprocess.check_output(
- self.adb_cmd + ['shell', 'echo']).decode('utf-8')
+ self.adb_cmd + ['shell', 'echo'], encoding='utf-8')
return self._linesep
@property
- def features(self):
+ def features(self) -> list[str]:
if self._features is None:
try:
self._features = split_lines(self._simple_call(['features']))
@@ -288,16 +306,16 @@
self._features = []
return self._features
- def has_shell_protocol(self):
+ def has_shell_protocol(self) -> bool:
return version(self.adb_cmd) >= 35 and 'shell_v2' in self.features
- def _make_shell_cmd(self, user_cmd):
+ def _make_shell_cmd(self, user_cmd: list[str]) -> list[str]:
command = self.adb_cmd + ['shell'] + user_cmd
if not self.has_shell_protocol():
command += self._RETURN_CODE_PROBE
return command
- def _parse_shell_output(self, out):
+ def _parse_shell_output(self, out: str) -> tuple[int, str]:
"""Finds the exit code string from shell output.
Args:
@@ -325,12 +343,12 @@
out = out[:-len(partition[1]) - len(partition[2])]
return result, out
- def _simple_call(self, cmd):
+ def _simple_call(self, cmd: list[str]) -> str:
logging.info(' '.join(self.adb_cmd + cmd))
return _subprocess_check_output(
self.adb_cmd + cmd, stderr=subprocess.STDOUT).decode('utf-8')
- def shell(self, cmd):
+ def shell(self, cmd: list[str]) -> tuple[str, str]:
"""Calls `adb shell`
Args:
@@ -348,7 +366,7 @@
raise ShellError(cmd, stdout, stderr, exit_code)
return stdout, stderr
- def shell_nocheck(self, cmd):
+ def shell_nocheck(self, cmd: list[str]) -> tuple[int, str, str]:
"""Calls `adb shell`
Args:
@@ -371,8 +389,14 @@
exit_code, stdout = self._parse_shell_output(stdout)
return exit_code, stdout, stderr
- def shell_popen(self, cmd, kill_atexit=True, preexec_fn=None,
- creationflags=0, **kwargs):
+ def shell_popen(
+ self,
+ cmd: list[str],
+ kill_atexit: bool = True,
+ preexec_fn: Callable[[], None] | None = None,
+ creationflags: int = 0,
+ **kwargs: Any,
+ ) -> subprocess.Popen[Any]:
"""Calls `adb shell` and returns a handle to the adb process.
This function provides direct access to the subprocess used to run the
@@ -400,7 +424,7 @@
preexec_fn = os.setpgrp
elif preexec_fn is not os.setpgrp:
fn = preexec_fn
- def _wrapper():
+ def _wrapper() -> None:
fn()
os.setpgrp()
preexec_fn = _wrapper
@@ -413,14 +437,14 @@
return p
- def install(self, filename, replace=False):
+ def install(self, filename: str, replace: bool = False) -> str:
cmd = ['install']
if replace:
cmd.append('-r')
cmd.append(filename)
return self._simple_call(cmd)
- def push(self, local, remote, sync=False):
+ def push(self, local: str | list[str], remote: str, sync: bool = False) -> str:
"""Transfer a local file or directory to the device.
Args:
@@ -430,7 +454,7 @@
those on the device. If False, transfers all files.
Returns:
- Exit status of the push command.
+ Output of the command.
"""
cmd = ['push']
if sync:
@@ -444,73 +468,73 @@
return self._simple_call(cmd)
- def pull(self, remote, local):
+ def pull(self, remote: str, local: str) -> str:
return self._simple_call(['pull', remote, local])
- def sync(self, directory=None):
+ def sync(self, directory: str | None = None) -> str:
cmd = ['sync']
if directory is not None:
cmd.append(directory)
return self._simple_call(cmd)
- def tcpip(self, port):
+ def tcpip(self, port: str) -> str:
return self._simple_call(['tcpip', port])
- def usb(self):
+ def usb(self) -> str:
return self._simple_call(['usb'])
- def reboot(self):
+ def reboot(self) -> str:
return self._simple_call(['reboot'])
- def remount(self):
+ def remount(self) -> str:
return self._simple_call(['remount'])
- def root(self):
+ def root(self) -> str:
return self._simple_call(['root'])
- def unroot(self):
+ def unroot(self) -> str:
return self._simple_call(['unroot'])
- def connect(self, host):
+ def connect(self, host: str) -> str:
return self._simple_call(['connect', host])
- def disconnect(self, host):
+ def disconnect(self, host: str) -> str:
return self._simple_call(['disconnect', host])
- def forward(self, local, remote):
+ def forward(self, local: str, remote: str) -> str:
return self._simple_call(['forward', local, remote])
- def forward_list(self):
+ def forward_list(self) -> str:
return self._simple_call(['forward', '--list'])
- def forward_no_rebind(self, local, remote):
+ def forward_no_rebind(self, local: str, remote: str) -> str:
return self._simple_call(['forward', '--no-rebind', local, remote])
- def forward_remove(self, local):
+ def forward_remove(self, local: str) -> str:
return self._simple_call(['forward', '--remove', local])
- def forward_remove_all(self):
+ def forward_remove_all(self) -> str:
return self._simple_call(['forward', '--remove-all'])
- def reverse(self, remote, local):
+ def reverse(self, remote: str, local: str) -> str:
return self._simple_call(['reverse', remote, local])
- def reverse_list(self):
+ def reverse_list(self) -> str:
return self._simple_call(['reverse', '--list'])
- def reverse_no_rebind(self, local, remote):
+ def reverse_no_rebind(self, local: str, remote: str) -> str:
return self._simple_call(['reverse', '--no-rebind', local, remote])
- def reverse_remove_all(self):
+ def reverse_remove_all(self) -> str:
return self._simple_call(['reverse', '--remove-all'])
- def reverse_remove(self, remote):
+ def reverse_remove(self, remote: str) -> str:
return self._simple_call(['reverse', '--remove', remote])
- def wait(self):
+ def wait(self) -> str:
return self._simple_call(['wait-for-device'])
- def get_prop(self, prop_name):
+ def get_prop(self, prop_name: str) -> str | None:
output = split_lines(self.shell(['getprop', prop_name])[0])
if len(output) != 1:
raise RuntimeError('Too many lines in getprop output:\n' +
@@ -520,7 +544,7 @@
return None
return value
- def set_prop(self, prop_name, value):
+ def set_prop(self, prop_name: str, value: str) -> None:
self.shell(['setprop', prop_name, value])
def logcat(self) -> str:
diff --git a/python-packages/adb/mypy.ini b/python-packages/adb/mypy.ini
new file mode 100644
index 0000000..dd92b02
--- /dev/null
+++ b/python-packages/adb/mypy.ini
@@ -0,0 +1,19 @@
+[mypy]
+check_untyped_defs = true
+disallow_any_generics = true
+disallow_any_unimported = true
+disallow_subclassing_any = true
+disallow_untyped_decorators = true
+disallow_untyped_defs = true
+follow_imports = silent
+implicit_reexport = false
+namespace_packages = true
+no_implicit_optional = true
+show_error_codes = true
+strict_equality = true
+warn_redundant_casts = true
+# Can't enable because mypy cannot reason about _get_subprocess_args.
+# warn_return_any = true
+warn_unreachable = true
+warn_unused_configs = true
+warn_unused_ignores = true
diff --git a/python-packages/adb/test.py b/python-packages/adb/test.py
index c446883..796d066 100644
--- a/python-packages/adb/test.py
+++ b/python-packages/adb/test.py
@@ -20,12 +20,12 @@
import adb
class GetDeviceTest(unittest.TestCase):
- def setUp(self):
+ def setUp(self) -> None:
self.android_serial = os.getenv('ANDROID_SERIAL')
if 'ANDROID_SERIAL' in os.environ:
del os.environ['ANDROID_SERIAL']
- def tearDown(self):
+ def tearDown(self) -> None:
if self.android_serial is not None:
os.environ['ANDROID_SERIAL'] = self.android_serial
else:
@@ -33,27 +33,27 @@
del os.environ['ANDROID_SERIAL']
@mock.patch('adb.device.get_devices')
- def test_explicit(self, mock_get_devices):
+ def test_explicit(self, mock_get_devices: mock.Mock) -> None:
mock_get_devices.return_value = ['foo', 'bar']
device = adb.get_device('foo')
self.assertEqual(device.serial, 'foo')
@mock.patch('adb.device.get_devices')
- def test_from_env(self, mock_get_devices):
+ def test_from_env(self, mock_get_devices: mock.Mock) -> None:
mock_get_devices.return_value = ['foo', 'bar']
os.environ['ANDROID_SERIAL'] = 'foo'
device = adb.get_device()
self.assertEqual(device.serial, 'foo')
@mock.patch('adb.device.get_devices')
- def test_arg_beats_env(self, mock_get_devices):
+ def test_arg_beats_env(self, mock_get_devices: mock.Mock) -> None:
mock_get_devices.return_value = ['foo', 'bar']
os.environ['ANDROID_SERIAL'] = 'bar'
device = adb.get_device('foo')
self.assertEqual(device.serial, 'foo')
@mock.patch('adb.device.get_devices')
- def test_no_such_device(self, mock_get_devices):
+ def test_no_such_device(self, mock_get_devices: mock.Mock) -> None:
mock_get_devices.return_value = ['foo', 'bar']
self.assertRaises(adb.DeviceNotFoundError, adb.get_device, ['baz'])
@@ -61,18 +61,18 @@
self.assertRaises(adb.DeviceNotFoundError, adb.get_device)
@mock.patch('adb.device.get_devices')
- def test_unique_device(self, mock_get_devices):
+ def test_unique_device(self, mock_get_devices: mock.Mock) -> None:
mock_get_devices.return_value = ['foo']
device = adb.get_device()
self.assertEqual(device.serial, 'foo')
@mock.patch('adb.device.get_devices')
- def test_no_unique_device(self, mock_get_devices):
+ def test_no_unique_device(self, mock_get_devices: mock.Mock) -> None:
mock_get_devices.return_value = ['foo', 'bar']
self.assertRaises(adb.NoUniqueDeviceError, adb.get_device)
-def main():
+def main() -> None:
suite = unittest.TestLoader().loadTestsFromName(__name__)
unittest.TextTestRunner(verbosity=3).run(suite)