FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
pythonnative/src/pythonnative/project/devices.py at v0.43.1 · pythonnative/pythonnative · GitHub
pythonnative
pythonnative
Repository navigation
Code
Issues
11
(11)
Pull requests
4
(4)
Actions
Projects
Security and quality
Insights
Expand file tree
Breadcrumbs
pythonnative
/
src
/
pythonnative
/
project
/
devices.py
Copy path
More file actions
More file actions
Latest commit
History
History
History
304 lines (254 loc) · 9.87 KB
Breadcrumbs
pythonnative
/
src
/
pythonnative
/
project
/
devices.py
Copy path
File metadata and controls
304 lines (254 loc) · 9.87 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
"""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
:<40
}
{
self
.
kind
:<10
}
{
self
.
state
:<10
}
{
self
.
name
}
{
os_part
}
"
# ======================================================================
# Android (adb)
# ======================================================================
def
parse_adb_devices
(
output
:
str
)
->
List
[
Device
]:
"""Parse ``adb devices -l`` output into devices.
Args:
output: Raw stdout from ``adb devices -l``.
Returns:
Android devices and emulators (offline entries included, with
``state="offline"``).
"""
devices
:
List
[
Device
]
=
[]
for
line
in
output
.
splitlines
():
line
=
line
.
strip
()
if
not
line
or
line
.
startswith
(
"List of devices"
):
continue
parts
=
line
.
split
()
if
len
(
parts
)
<
2
:
continue
serial
,
state
=
parts
[
0
],
parts
[
1
]
if
state
==
"unauthorized"
:
state
=
"offline"
elif
state
==
"device"
:
state
=
"connected"
model
=
""
for
token
in
parts
[
2
:]:
if
token
.
startswith
(
"model:"
):
model
=
token
[
len
(
"model:"
) :].
replace
(
"_"
,
" "
)
kind
=
"emulator"
if
serial
.
startswith
(
"emulator-"
)
else
"device"
devices
.
append
(
Device
(
platform
=
"android"
,
kind
=
kind
,
identifier
=
serial
,
name
=
model
or
serial
,
state
=
state
if
state
in
(
"connected"
,
"offline"
)
else
"offline"
,
)
)
return
devices
def
list_android_devices
()
->
List
[
Device
]:
"""Return connected Android devices/emulators (empty without adb)."""
try
:
result
=
subprocess
.
run
([
"adb"
,
"devices"
,
"-l"
],
check
=
False
,
capture_output
=
True
,
text
=
True
)
except
FileNotFoundError
:
return
[]
return
parse_adb_devices
(
result
.
stdout
or
""
)
# ======================================================================
# iOS Simulators (simctl)
# ======================================================================
_RUNTIME_RE
=
re
.
compile
(
r"iOS[.-](\d+)[.-](\d+)"
)
def
parse_simctl_devices
(
data
:
Dict
[
str
,
Any
])
->
List
[
Device
]:
"""Parse ``simctl list devices --json`` payload into devices.
Args:
data: The decoded JSON payload.
Returns:
Available iOS Simulators (unavailable ones are skipped).
"""
devices
:
List
[
Device
]
=
[]
for
runtime
,
entries
in
(
data
.
get
(
"devices"
)
or
{}).
items
():
match
=
_RUNTIME_RE
.
search
(
runtime
)
os_version
=
f"iOS
{
match
.
group
(
1
)
}
.
{
match
.
group
(
2
)
}
"
if
match
else
""
if
"iOS"
not
in
runtime
:
continue
for
entry
in
entries
or
[]:
if
not
entry
.
get
(
"isAvailable"
)
or
not
entry
.
get
(
"udid"
):
continue
devices
.
append
(
Device
(
platform
=
"ios"
,
kind
=
"simulator"
,
identifier
=
str
(
entry
[
"udid"
]),
name
=
str
(
entry
.
get
(
"name"
)
or
entry
[
"udid"
]),
os_version
=
os_version
,
state
=
"booted"
if
entry
.
get
(
"state"
)
==
"Booted"
else
"shutdown"
,
)
)
return
devices
def
list_ios_simulators
()
->
List
[
Device
]:
"""Return available iOS Simulators (empty without Xcode)."""
try
:
result
=
subprocess
.
run
(
[
"xcrun"
,
"simctl"
,
"list"
,
"devices"
,
"available"
,
"--json"
],
check
=
False
,
capture_output
=
True
,
text
=
True
,
)
except
FileNotFoundError
:
return
[]
try
:
data
=
json
.
loads
(
result
.
stdout
or
"{}"
)
except
json
.
JSONDecodeError
:
return
[]
return
parse_simctl_devices
(
data
)
# ======================================================================
# Physical iOS devices (devicectl)
# ======================================================================
def
parse_devicectl_devices
(
data
:
Dict
[
str
,
Any
])
->
List
[
Device
]:
"""Parse ``devicectl list devices --json-output`` payload into devices.
Args:
data: The decoded JSON payload.
Returns:
Physical iOS devices.
"""
devices
:
List
[
Device
]
=
[]
for
entry
in
(
data
.
get
(
"result"
)
or
{}).
get
(
"devices"
)
or
[]:
props
=
entry
.
get
(
"deviceProperties"
)
or
{}
hardware
=
entry
.
get
(
"hardwareProperties"
)
or
{}
connection
=
entry
.
get
(
"connectionProperties"
)
or
{}
identifier
=
str
(
entry
.
get
(
"identifier"
)
or
""
)
if
not
identifier
:
continue
state
=
"connected"
if
connection
.
get
(
"tunnelState"
)
in
(
"connected"
,
"available"
)
else
"offline"
os_version
=
props
.
get
(
"osVersionNumber"
)
or
""
devices
.
append
(
Device
(
platform
=
"ios"
,
kind
=
"device"
,
identifier
=
identifier
,
name
=
str
(
props
.
get
(
"name"
)
or
hardware
.
get
(
"marketingName"
)
or
identifier
),
os_version
=
f"iOS
{
os_version
}
"
if
os_version
else
""
,
state
=
state
,
)
)
return
devices
def
list_ios_physical_devices
()
->
List
[
Device
]:
"""Return physical iOS devices (empty without Xcode 15+)."""
with
tempfile
.
TemporaryDirectory
()
as
tmp
:
out_path
=
Path
(
tmp
)
/
"devices.json"
try
:
subprocess
.
run
(
[
"xcrun"
,
"devicectl"
,
"list"
,
"devices"
,
"--json-output"
,
str
(
out_path
)],
check
=
False
,
capture_output
=
True
,
text
=
True
,
timeout
=
30
,
)
except
(
FileNotFoundError
,
subprocess
.
TimeoutExpired
):
return
[]
if
not
out_path
.
is_file
():
return
[]
try
:
data
=
json
.
loads
(
out_path
.
read_text
(
encoding
=
"utf-8"
))
except
(
json
.
JSONDecodeError
,
OSError
):
return
[]
return
parse_devicectl_devices
(
data
)
# ======================================================================
# Aggregation / selection
# ======================================================================
def
list_devices
(
platform
:
Optional
[
str
]
=
None
)
->
List
[
Device
]:
"""Return all known targets, optionally restricted to one platform.
Args:
platform: ``"android"``, ``"ios"``, or ``None`` for both.
Returns:
Devices ordered: Android first, then physical iOS devices, then
simulators (booted simulators before shutdown ones).
"""
devices
:
List
[
Device
]
=
[]
if
platform
in
(
None
,
"android"
):
devices
.
extend
(
list_android_devices
())
if
platform
in
(
None
,
"ios"
):
devices
.
extend
(
list_ios_physical_devices
())
simulators
=
list_ios_simulators
()
simulators
.
sort
(
key
=
lambda
dev
: (
dev
.
state
!=
"booted"
,
dev
.
name
))
devices
.
extend
(
simulators
)
return
devices
def
find_device
(
devices
:
List
[
Device
],
query
:
str
)
->
Optional
[
Device
]:
"""Resolve ``query`` against ``devices`` (exact id, then name match).
Args:
devices: Candidate devices.
query: An identifier, exact name, or case-insensitive name
substring.
Returns:
The best match, or ``None``. Ready devices win ties.
"""
ready_first
=
sorted
(
devices
,
key
=
lambda
dev
:
not
dev
.
is_ready
)
for
device
in
ready_first
:
if
device
.
identifier
==
query
:
return
device
lowered
=
query
.
lower
()
for
device
in
ready_first
:
if
device
.
name
.
lower
()
==
lowered
:
return
device
for
device
in
ready_first
:
if
lowered
in
device
.
name
.
lower
():
return
device
return
None
Back
|
FazBrowse Home
|
New Git URL