Skip to content

Commit

Permalink
Implement Tool base class; complete typing for tools
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Feb 11, 2023
1 parent b49fd81 commit 64588e3
Show file tree
Hide file tree
Showing 27 changed files with 586 additions and 325 deletions.
1 change: 1 addition & 0 deletions changes/1093.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implemented the ``Tool`` base class for all tools.
4 changes: 2 additions & 2 deletions src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
TemplateUnsupportedVersion,
UnsupportedPlatform,
)
from briefcase.integrations import git
from briefcase.integrations.git import Git
from briefcase.integrations.subprocess import NativeAppContext

from .base import BaseCommand, full_options
Expand Down Expand Up @@ -778,7 +778,7 @@ def verify_tools(self):
Raises MissingToolException if a required system tool is missing.
"""
super().verify_tools()
git.verify_git_is_installed(tools=self.tools)
Git.verify(tools=self.tools)

def verify_app_tools(self, app: BaseConfig):
"""Verify that tools needed to run the command for this app exist."""
Expand Down
4 changes: 2 additions & 2 deletions src/briefcase/commands/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
make_class_name,
)
from briefcase.exceptions import BriefcaseCommandError, TemplateUnsupportedVersion
from briefcase.integrations import git
from briefcase.integrations.git import Git

from .base import BaseCommand

Expand Down Expand Up @@ -501,7 +501,7 @@ def verify_tools(self):
Raises MissingToolException if a required system tool is missing.
"""
super().verify_tools()
git.verify_git_is_installed(tools=self.tools)
Git.verify(tools=self.tools)

def __call__(self, template: Optional[str] = None, **options):
# Confirm host compatibility and all required tools are available
Expand Down
91 changes: 47 additions & 44 deletions src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

import json
import os
import re
import shutil
import subprocess
import time
from contextlib import suppress
from datetime import datetime
from pathlib import Path

from briefcase.config import PEP508_NAME_RE
Expand Down Expand Up @@ -51,7 +54,7 @@ def __init__(self, tools: ToolCache, root_path: Path):
self.sleep = time.sleep

@property
def cmdline_tools_url(self):
def cmdline_tools_url(self) -> str:
"""The Android SDK Command-Line Tools URL appropriate to the current operating
system."""
platform_name = self.tools.host_os.lower()
Expand All @@ -63,11 +66,11 @@ def cmdline_tools_url(self):
return f"https://dl.google.com/android/repository/commandlinetools-{platform_name}-{self.cmdline_tools_version}_latest.zip" # noqa: E501

@property
def cmdline_tools_path(self):
def cmdline_tools_path(self) -> Path:
return self.root_path / "cmdline-tools" / "latest"

@property
def cmdline_tools_version(self):
def cmdline_tools_version(self) -> str:
# This is the version of the Android SDK Command-line tools that
# are current as of May 2022. These tools can generally self-update,
# so using a fixed download URL isn't a problem.
Expand All @@ -76,49 +79,49 @@ def cmdline_tools_version(self):
return "8092744"

@property
def cmdline_tools_version_path(self):
def cmdline_tools_version_path(self) -> Path:
return self.root_path / "cmdline-tools" / self.cmdline_tools_version

@property
def sdkmanager_path(self):
def sdkmanager_path(self) -> Path:
sdkmanager = (
"sdkmanager.bat" if self.tools.host_os == "Windows" else "sdkmanager"
)
return self.cmdline_tools_path / "bin" / sdkmanager

@property
def adb_path(self):
def adb_path(self) -> Path:
adb = "adb.exe" if self.tools.host_os == "Windows" else "adb"
return self.root_path / "platform-tools" / adb

@property
def avdmanager_path(self):
def avdmanager_path(self) -> Path:
avdmanager = (
"avdmanager.bat" if self.tools.host_os == "Windows" else "avdmanager"
)
return self.cmdline_tools_path / "bin" / avdmanager

@property
def emulator_path(self):
def emulator_path(self) -> Path:
emulator = "emulator.exe" if self.tools.host_os == "Windows" else "emulator"
return self.root_path / "emulator" / emulator

@property
def avd_path(self):
def avd_path(self) -> Path:
return self.dot_android_path / "avd"

def avd_config_filename(self, avd):
return self.avd_path / f"{avd}.avd" / "config.ini"

@property
def env(self):
def env(self) -> dict[str, str]:
return {
"ANDROID_SDK_ROOT": os.fsdecode(self.root_path),
"JAVA_HOME": str(self.tools.java.java_home),
}

@property
def emulator_abi(self):
def emulator_abi(self) -> str:
"""The ABI to use for the Android emulator."""
if self.tools.host_arch == "arm64" and self.tools.host_os == "Darwin":
return "arm64-v8a"
Expand All @@ -131,19 +134,19 @@ def emulator_abi(self):
)

@property
def DEFAULT_DEVICE_TYPE(self):
def DEFAULT_DEVICE_TYPE(self) -> str:
return "pixel"

@property
def DEFAULT_DEVICE_SKIN(self):
def DEFAULT_DEVICE_SKIN(self) -> str:
return "pixel_3a"

@property
def DEFAULT_SYSTEM_IMAGE(self):
def DEFAULT_SYSTEM_IMAGE(self) -> str:
return f"system-images;android-31;default;{self.emulator_abi}"

@classmethod
def verify(cls, tools: ToolCache, install=True):
def verify(cls, tools: ToolCache, install=True) -> AndroidSDK:
"""Verify an Android SDK is available.
If the ANDROID_SDK_ROOT environment variable is set, that location will
Expand Down Expand Up @@ -236,7 +239,7 @@ def verify(cls, tools: ToolCache, install=True):
tools.android_sdk = sdk
return sdk

def exists(self):
def exists(self) -> bool:
"""Confirm that the SDK actually exists.
Look for the sdkmanager; and, if necessary, confirm that it is executable.
Expand All @@ -247,7 +250,7 @@ def exists(self):
)

@property
def managed_install(self):
def managed_install(self) -> bool:
"""Is the Android SDK install managed by Briefcase?"""
# Although the end-user can provide their own SDK, the SDK also
# provides a built-in upgrade mechanism. Therefore, all Android SDKs
Expand Down Expand Up @@ -350,7 +353,7 @@ def list_packages(self):
"Unable to invoke the Android SDK manager"
) from e

def adb(self, device):
def adb(self, device) -> ADB:
"""Obtain an ADB instance for managing a specific device.
:param device: The device ID to manage.
Expand Down Expand Up @@ -613,7 +616,7 @@ def verify_emulator_skin(self, skin):
# Delete the downloaded file.
skin_tgz_path.unlink()

def emulators(self):
def emulators(self) -> list[str]:
"""Find the list of emulators that are available."""
try:
# Capture `stderr` so that if the process exits with failure, the
Expand All @@ -629,11 +632,9 @@ def emulators(self):
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError("Unable to obtain Android emulator list") from e

def devices(self):
def devices(self) -> dict[str, dict[str, str | bool]]:
"""Find the devices that are attached and available to ADB."""
try:
# Capture `stderr` so that if the process exits with failure, the
# stderr data is in `e.output`.
output = self.tools.subprocess.check_output(
[os.fsdecode(self.adb_path), "devices", "-l"]
).strip()
Expand Down Expand Up @@ -676,7 +677,10 @@ def devices(self):
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError("Unable to obtain Android device list") from e

def select_target_device(self, device_or_avd):
def select_target_device(
self,
device_or_avd: str | None,
) -> tuple[None | str, None | str, None | str]:
"""Select a device to be the target for actions.
Interrogates the system to get the list of available devices.
Expand All @@ -693,7 +697,7 @@ def select_target_device(self, device_or_avd):
available.
:returns: A tuple containing ``(device, name, avd)``. ``avd`` will only
be provided if an emulator with that AVD is not currently running.
If ``device`` is null, a new emulator should be created.
If ``device`` is None, a new emulator should be created.
"""
# If the device_or_avd starts with "{", it's a definition for a new
# emulator to be created.
Expand Down Expand Up @@ -770,8 +774,8 @@ def select_target_device(self, device_or_avd):

if device_or_avd.startswith("@"):
# specifier is an AVD
avd = device_or_avd[1:]
try:
avd = device_or_avd[1:]
device = running_avds[avd]
except KeyError:
# device_or_avd isn't in the list of running avds;
Expand Down Expand Up @@ -834,7 +838,7 @@ def select_target_device(self, device_or_avd):
else:
# Either a running emulator, or a physical device. Regardless,
# we need to check if the device is developer enabled.
# Functionally, we know the the device *must* be in the list of
# Functionally, we know the device *must* be in the list of
# choices; which means it's also in the list of running devices
# and the list of device choices, so any KeyError on those lookups
# indicates a deeper problem.
Expand Down Expand Up @@ -867,10 +871,9 @@ def select_target_device(self, device_or_avd):

return device, name, avd

def create_emulator(self, use_defaults=False):
def create_emulator(self) -> str:
"""Create a new Android emulator.
:param use_defaults: Should the emulator be created with defaults?
:returns: The AVD of the newly created emulator.
"""
# Get the list of existing emulators
Expand Down Expand Up @@ -948,10 +951,10 @@ def create_emulator(self, use_defaults=False):

def _create_emulator(
self,
avd,
device_type=None,
skin=None,
system_image=None,
avd: str,
device_type: str = None,
skin: str = None,
system_image: str = None,
):
"""Internal method that does the actual work of creating the emulator.
Expand Down Expand Up @@ -1014,7 +1017,7 @@ def _create_emulator(
},
)

def avd_config(self, avd):
def avd_config(self, avd: str) -> dict[str, str]:
"""Obtain the AVD configuration as key-value pairs.
:params avd: The AVD whose config will be retrieved
Expand All @@ -1031,7 +1034,7 @@ def avd_config(self, avd):

return avd_config

def update_emulator_config(self, avd, updates):
def update_emulator_config(self, avd: str, updates: dict[str, str]):
"""Update the AVD configuration with specific values.
:params avd: The AVD whose config will be updated
Expand All @@ -1048,7 +1051,7 @@ def update_emulator_config(self, avd, updates):
for key, value in avd_config.items():
f.write(f"{key}={value}\n")

def start_emulator(self, avd, extra_args=None):
def start_emulator(self, avd: str, extra_args: list[str] = None) -> tuple[str, str]:
"""Start an existing Android emulator.
Returns when the emulator is booted and ready to accept apps.
Expand Down Expand Up @@ -1183,7 +1186,7 @@ def __init__(self, tools: ToolCache, device: str):
self.tools = tools
self.device = device

def avd_name(self):
def avd_name(self) -> str | None:
"""Get the AVD name for the device.
:returns: The AVD name for the device; or ``None`` if the device isn't
Expand Down Expand Up @@ -1217,7 +1220,7 @@ def has_booted(self):
f"Unable to determine if emulator {self.device} has booted."
) from e

def run(self, *arguments, quiet=False):
def run(self, *arguments, quiet: bool = False) -> str:
"""Run a command on a device using Android debug bridge, `adb`. The device name
is mandatory to ensure clarity in the case of multiple attached devices.
Expand Down Expand Up @@ -1253,7 +1256,7 @@ def run(self, *arguments, quiet=False):
raise InvalidDeviceError("device id", self.device) from e
raise

def install_apk(self, apk_path):
def install_apk(self, apk_path: str | Path):
"""Install an APK file on an Android device.
:param apk_path: The path of the Android APK file to install.
Expand All @@ -1267,7 +1270,7 @@ def install_apk(self, apk_path):
f"Unable to install APK {apk_path} on {self.device}"
) from e

def force_stop_app(self, package):
def force_stop_app(self, package: str):
"""Force-stop an app, specified as a package name.
:param package: The name of the Android package, e.g., com.username.myapp.
Expand All @@ -1284,7 +1287,7 @@ def force_stop_app(self, package):
f"Unable to force stop app {package} on {self.device}"
) from e

def start_app(self, package, activity):
def start_app(self, package: str, activity: str):
"""Start an app, specified as a package name & activity name.
:param package: The name of the Android package, e.g., com.username.myapp.
Expand Down Expand Up @@ -1329,7 +1332,7 @@ def start_app(self, package, activity):
f"Unable to start {package}/{activity} on {self.device}"
) from e

def logcat(self, pid):
def logcat(self, pid: str):
"""Start following the adb log for the device.
:param pid: The PID whose logs you want to display.
Expand All @@ -1352,7 +1355,7 @@ def logcat(self, pid):
bufsize=1,
)

def logcat_tail(self, since=None):
def logcat_tail(self, since: datetime):
"""Show the tail of the logs for Python-like apps, starting from a given
timestamp.
Expand Down Expand Up @@ -1381,7 +1384,7 @@ def logcat_tail(self, since=None):
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError("Error starting ADB logcat.") from e

def pidof(self, package, **kwargs):
def pidof(self, package: str, **kwargs) -> str | None:
"""Obtain the PID of a running app by package name.
:param package: The package ID for the application (e.g.,
Expand All @@ -1398,7 +1401,7 @@ def pidof(self, package, **kwargs):
except subprocess.CalledProcessError:
return None

def pid_exists(self, pid, **kwargs):
def pid_exists(self, pid: str, **kwargs) -> bool:
"""Confirm if the PID exists on the emulator.
:param pid: The PID to check
Expand Down
Loading

0 comments on commit 64588e3

Please sign in to comment.