"""Device and simulator discovery for ``pn devices`` / ``pn run --device``.
Enumerates the three kinds of targets a PythonNative app can run on:
- **Android devices and emulators** via ``adb devices -l``.
- **iOS Simulators** via ``xcrun simctl list devices --json``.
- **Physical iOS devices** via ``xcrun devicectl list devices`` (Xcode
15+; earlier Xcodes simply report no physical devices).
The shell-outs are isolated in ``list_*`` functions; the parsers are
pure functions over captured text/JSON so they can be unit tested
without any toolchain installed.
"""
from __future__ import annotations
import json
import re
import subprocess
import tempfile
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional
__all__ = [
"Device",
"list_devices",
"list_android_devices",
"list_ios_simulators",
"list_ios_physical_devices",
"find_device",
"parse_adb_devices",
"parse_simctl_devices",
"parse_devicectl_devices",
]
@dataclass
class Device:
"""One runnable target.
Attributes:
platform: ``"android"`` or ``"ios"``.
kind: ``"device"``, ``"emulator"``, or ``"simulator"``.
identifier: The stable id used to target the device (adb
serial, simulator UDID, or devicectl identifier).
name: Human-readable model or simulator name.
os_version: OS version string when known.
state: ``"booted"``, ``"connected"``, ``"shutdown"``, or
``"offline"``.
"""
platform: str
kind: str
identifier: str
name: str
os_version: str = ""
state: str = ""
@property
def is_ready(self) -> bool:
"""Whether the target can be used right now (or booted on demand)."""
if self.kind == "simulator":
return self.state in ("booted", "shutdown")
return self.state in ("booted", "connected")
def to_dict(self) -> Dict[str, Any]:
"""Return a JSON-serializable view of this target.
Derived from the dataclass fields so a field added later is
carried into the payload automatically, plus the computed
``is_ready`` that consumers would otherwise have to re-derive
from ``kind`` and ``state``.
Returns:
The declared fields in declaration order, then ``is_ready``.
"""
return {**asdict(self), "is_ready": self.is_ready}
def format(self) -> str:
"""Return one aligned listing row for the CLI."""
os_part = f" ({self.os_version})" if self.os_version else ""
return f" {self.identifier: