pymobiledevice3 5.0.4__py3-none-any.whl → 7.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. misc/understanding_idevice_protocol_layers.md +10 -5
  2. pymobiledevice3/__main__.py +171 -46
  3. pymobiledevice3/_version.py +2 -2
  4. pymobiledevice3/bonjour.py +22 -21
  5. pymobiledevice3/cli/activation.py +24 -22
  6. pymobiledevice3/cli/afc.py +49 -41
  7. pymobiledevice3/cli/amfi.py +13 -18
  8. pymobiledevice3/cli/apps.py +71 -65
  9. pymobiledevice3/cli/backup.py +134 -93
  10. pymobiledevice3/cli/bonjour.py +31 -29
  11. pymobiledevice3/cli/cli_common.py +175 -232
  12. pymobiledevice3/cli/companion_proxy.py +12 -12
  13. pymobiledevice3/cli/crash.py +95 -52
  14. pymobiledevice3/cli/developer/__init__.py +62 -0
  15. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  16. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  17. pymobiledevice3/cli/developer/arbitration.py +50 -0
  18. pymobiledevice3/cli/developer/condition.py +33 -0
  19. pymobiledevice3/cli/developer/core_device.py +294 -0
  20. pymobiledevice3/cli/developer/debugserver.py +244 -0
  21. pymobiledevice3/cli/developer/dvt/__init__.py +438 -0
  22. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  23. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  24. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  25. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  26. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  27. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  28. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  29. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  30. pymobiledevice3/cli/idam.py +42 -0
  31. pymobiledevice3/cli/lockdown.py +70 -75
  32. pymobiledevice3/cli/mounter.py +99 -57
  33. pymobiledevice3/cli/notification.py +38 -26
  34. pymobiledevice3/cli/pcap.py +36 -20
  35. pymobiledevice3/cli/power_assertion.py +15 -16
  36. pymobiledevice3/cli/processes.py +11 -17
  37. pymobiledevice3/cli/profile.py +120 -75
  38. pymobiledevice3/cli/provision.py +27 -26
  39. pymobiledevice3/cli/remote.py +109 -100
  40. pymobiledevice3/cli/restore.py +134 -129
  41. pymobiledevice3/cli/springboard.py +50 -50
  42. pymobiledevice3/cli/syslog.py +145 -65
  43. pymobiledevice3/cli/usbmux.py +66 -27
  44. pymobiledevice3/cli/version.py +2 -5
  45. pymobiledevice3/cli/webinspector.py +232 -156
  46. pymobiledevice3/exceptions.py +6 -2
  47. pymobiledevice3/lockdown.py +5 -1
  48. pymobiledevice3/lockdown_service_provider.py +5 -0
  49. pymobiledevice3/remote/remote_service_discovery.py +18 -10
  50. pymobiledevice3/restore/device.py +28 -4
  51. pymobiledevice3/restore/restore.py +2 -2
  52. pymobiledevice3/service_connection.py +15 -12
  53. pymobiledevice3/services/afc.py +731 -220
  54. pymobiledevice3/services/device_link.py +45 -31
  55. pymobiledevice3/services/idam.py +20 -0
  56. pymobiledevice3/services/lockdown_service.py +12 -9
  57. pymobiledevice3/services/mobile_config.py +1 -0
  58. pymobiledevice3/services/mobilebackup2.py +6 -3
  59. pymobiledevice3/services/os_trace.py +97 -55
  60. pymobiledevice3/services/remote_fetch_symbols.py +13 -8
  61. pymobiledevice3/services/screenshot.py +2 -2
  62. pymobiledevice3/services/web_protocol/alert.py +8 -8
  63. pymobiledevice3/services/web_protocol/automation_session.py +87 -79
  64. pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
  65. pymobiledevice3/services/web_protocol/driver.py +71 -70
  66. pymobiledevice3/services/web_protocol/element.py +58 -56
  67. pymobiledevice3/services/web_protocol/selenium_api.py +47 -47
  68. pymobiledevice3/services/web_protocol/session_protocol.py +3 -2
  69. pymobiledevice3/services/web_protocol/switch_to.py +23 -19
  70. pymobiledevice3/services/webinspector.py +42 -67
  71. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +5 -3
  72. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/RECORD +76 -61
  73. pymobiledevice3/cli/completions.py +0 -50
  74. pymobiledevice3/cli/developer.py +0 -1539
  75. pymobiledevice3/cli/diagnostics.py +0 -110
  76. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +0 -0
  77. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
  78. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/licenses/LICENSE +0 -0
  79. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
@@ -6,63 +6,38 @@ import os
6
6
  import sys
7
7
  import uuid
8
8
  from functools import wraps
9
- from typing import Any, Callable, Optional
9
+ from textwrap import dedent
10
+ from typing import Annotated, Any, Callable, Optional
10
11
 
11
12
  import click
12
13
  import coloredlogs
13
14
  import hexdump
14
15
  import inquirer3
15
- from click import Option, UsageError
16
+ import typer
17
+ from click import UsageError
16
18
  from inquirer3.themes import GreenPassion
17
19
  from pygments import formatters, highlight, lexers
20
+ from typer_injector import Depends
18
21
 
19
22
  from pymobiledevice3.exceptions import AccessDeniedError, DeviceNotFoundError, NoDeviceConnectedError
20
- from pymobiledevice3.lockdown import LockdownClient, TcpLockdownClient, create_using_usbmux, get_mobdev2_lockdowns
23
+ from pymobiledevice3.lockdown import TcpLockdownClient, create_using_usbmux, get_mobdev2_lockdowns
24
+ from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
21
25
  from pymobiledevice3.osu.os_utils import get_os_utils
22
26
  from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
23
27
  from pymobiledevice3.tunneld.api import TUNNELD_DEFAULT_ADDRESS, async_get_tunneld_devices
24
28
  from pymobiledevice3.usbmux import select_devices_by_connection_type
25
29
 
26
- COLORED_OUTPUT = True
27
30
  UDID_ENV_VAR = "PYMOBILEDEVICE3_UDID"
28
31
  TUNNEL_ENV_VAR = "PYMOBILEDEVICE3_TUNNEL"
29
32
  USBMUX_ENV_VAR = "PYMOBILEDEVICE3_USBMUX"
30
- OSUTILS = get_os_utils()
31
-
32
33
  USBMUX_OPTION_HELP = (
33
- f"usbmuxd listener address (in the form of either /path/to/unix/socket OR HOST:PORT). "
34
- f"Can be specified via {USBMUX_ENV_VAR} envvar"
34
+ "Address of the usbmuxd daemon (unix socket path or HOST:PORT). Defaults to the platform usbmuxd if omitted."
35
35
  )
36
+ DEVICE_OPTIONS_PANEL_TITLE = "Device Options"
37
+ OSUTILS = get_os_utils()
36
38
 
37
-
38
- class RSDOption(Option):
39
- def __init__(self, *args, **kwargs):
40
- self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
41
- help_option = kwargs.get("help", "")
42
- if self.mutually_exclusive:
43
- ex_str = ", ".join(self.mutually_exclusive)
44
- kwargs["help"] = help_option + (
45
- "\nNOTE: This argument is mutually exclusive with arguments: [" + ex_str + "]."
46
- )
47
- super().__init__(*args, **kwargs)
48
-
49
- def handle_parse_result(self, ctx, opts, args):
50
- if (
51
- isinstance(ctx.command, RSDCommand)
52
- and not (isinstance(ctx.command, Command))
53
- and ("rsd_service_provider_using_tunneld" not in opts)
54
- and ("rsd_service_provider_manually" not in opts)
55
- ):
56
- # defaulting to `--tunnel ''` if no remote option was specified
57
- opts["rsd_service_provider_using_tunneld"] = ""
58
- if self.mutually_exclusive.intersection(opts) and self.name in opts:
59
- raise UsageError(
60
- "Illegal usage: `{}` is mutually exclusive with arguments `{}`.".format(
61
- self.name, ", ".join(self.mutually_exclusive)
62
- )
63
- )
64
-
65
- return super().handle_parse_result(ctx, opts, args)
39
+ # Global options
40
+ COLORED_OUTPUT: bool = True
66
41
 
67
42
 
68
43
  def default_json_encoder(obj):
@@ -75,7 +50,7 @@ def default_json_encoder(obj):
75
50
  raise TypeError()
76
51
 
77
52
 
78
- def print_json(buf, colored: Optional[bool] = None, default=default_json_encoder):
53
+ def print_json(buf, colored: Optional[bool] = None, default=default_json_encoder) -> str:
79
54
  if colored is None:
80
55
  colored = user_requested_colored_output()
81
56
  formatted_json = json.dumps(buf, sort_keys=True, indent=4, default=default)
@@ -90,7 +65,7 @@ def print_json(buf, colored: Optional[bool] = None, default=default_json_encoder
90
65
  return formatted_json
91
66
 
92
67
 
93
- def print_hex(data, colored=True):
68
+ def print_hex(data, colored=True) -> None:
94
69
  hex_dump = hexdump.hexdump(data, result="return")
95
70
  if colored:
96
71
  print(highlight(hex_dump, lexers.HexdumpLexer(), formatters.Terminal256Formatter(style="native")))
@@ -98,11 +73,11 @@ def print_hex(data, colored=True):
98
73
  print(hex_dump, end="\n\n")
99
74
 
100
75
 
101
- def set_verbosity(ctx, param, value):
102
- coloredlogs.set_level(logging.INFO - (value * 10))
76
+ def set_verbosity(level: int) -> None:
77
+ coloredlogs.set_level(logging.INFO - (level * 10))
103
78
 
104
79
 
105
- def set_color_flag(ctx, param, value) -> None:
80
+ def set_color_flag(value: bool) -> None:
106
81
  global COLORED_OUTPUT
107
82
  COLORED_OUTPUT = value
108
83
 
@@ -134,8 +109,8 @@ def prompt_selection(choices: list[Any], message: str, idx: bool = False) -> Any
134
109
  question = [inquirer3.List("selection", message=message, choices=choices, carousel=True)]
135
110
  try:
136
111
  result = inquirer3.prompt(question, theme=GreenPassion(), raise_keyboard_interrupt=True)
137
- except KeyboardInterrupt as e:
138
- raise click.ClickException("No selection was made") from e
112
+ except KeyboardInterrupt:
113
+ raise click.ClickException("No selection was made") from None
139
114
  return result["selection"] if not idx else choices.index(result["selection"])
140
115
 
141
116
 
@@ -143,214 +118,182 @@ def prompt_device_list(device_list: list):
143
118
  return prompt_selection(device_list, "Choose device")
144
119
 
145
120
 
146
- def choose_service_provider(callback: Callable):
147
- def wrap_callback_calling(**kwargs: dict) -> None:
148
- service_provider = None
149
- lockdown_service_provider = kwargs.pop("lockdown_service_provider", None)
150
- rsd_service_provider_manually = kwargs.pop("rsd_service_provider_manually", None)
151
- rsd_service_provider_using_tunneld = kwargs.pop("rsd_service_provider_using_tunneld", None)
152
- if lockdown_service_provider is not None:
153
- service_provider = lockdown_service_provider
154
- if rsd_service_provider_manually is not None:
155
- service_provider = rsd_service_provider_manually
156
- if rsd_service_provider_using_tunneld is not None:
157
- service_provider = rsd_service_provider_using_tunneld
158
- callback(service_provider=service_provider, **kwargs)
159
-
160
- return wrap_callback_calling
161
-
162
-
163
121
  def is_invoked_for_completion() -> bool:
164
- """Returns True if the command is ivoked for autocompletion."""
122
+ """Returns True if the command is invoked for autocompletion."""
165
123
  return any(env.startswith("_") and env.endswith("_COMPLETE") for env in os.environ)
166
124
 
167
125
 
168
- class BaseCommand(click.Command):
169
- def __init__(self, *args, **kwargs):
170
- super().__init__(*args, **kwargs)
171
- self.params[:0] = [
172
- click.Option(("verbosity", "-v", "--verbose"), count=True, callback=set_verbosity, expose_value=False),
173
- click.Option(
174
- ("color", "--color/--no-color"),
175
- default=True,
176
- callback=set_color_flag,
177
- is_flag=True,
178
- expose_value=False,
179
- help="colorize output",
180
- ),
181
- ]
182
-
183
-
184
- class BaseServiceProviderCommand(BaseCommand):
185
- def __init__(self, *args, **kwargs):
186
- super().__init__(*args, **kwargs)
187
- self.service_provider = None
188
- self.callback = choose_service_provider(self.callback)
189
-
190
-
191
- class LockdownCommand(BaseServiceProviderCommand):
192
- def __init__(self, *args, **kwargs):
193
- super().__init__(*args, **kwargs)
194
- self.usbmux_address = None
195
- self.mobdev2_option = None
196
- self.params[:0] = [
197
- click.Option(
198
- ("mobdev2", "--mobdev2"),
199
- callback=self.mobdev2,
200
- expose_value=False,
201
- default=None,
202
- help="Use bonjour browse for mobdev2 devices. Expected value IP address of the interface to "
203
- "use. Leave empty to browse through all interfaces",
204
- ),
205
- click.Option(
206
- ("usbmux", "--usbmux"),
207
- callback=self.usbmux,
208
- expose_value=False,
209
- envvar=USBMUX_ENV_VAR,
210
- help=USBMUX_OPTION_HELP,
211
- ),
212
- click.Option(
213
- ("lockdown_service_provider", "--udid"),
214
- envvar=UDID_ENV_VAR,
215
- callback=self.udid,
216
- help=f"Device unique identifier. You may pass {UDID_ENV_VAR} environment variable to pass this"
217
- f" option as well",
218
- ),
219
- ]
126
+ async def get_mobdev2_devices(udid: Optional[str] = None) -> list[TcpLockdownClient]:
127
+ return [lockdown async for _, lockdown in get_mobdev2_lockdowns(udid=udid)]
220
128
 
221
- async def get_mobdev2_devices(
222
- self, udid: Optional[str] = None, ips: Optional[list[str]] = None
223
- ) -> list[TcpLockdownClient]:
224
- result = []
225
- async for _ip, lockdown in get_mobdev2_lockdowns(udid=udid, ips=ips):
226
- result.append(lockdown)
227
- return result
228
129
 
229
- def mobdev2(self, ctx, param: str, value: Optional[str] = None) -> None:
230
- self.mobdev2_option = value
130
+ async def _tunneld(udid: Optional[str] = None) -> Optional[RemoteServiceDiscoveryService]:
131
+ if udid is None:
132
+ return
231
133
 
232
- def usbmux(self, ctx, param: str, value: Optional[str] = None) -> None:
233
- if value is None:
234
- return
235
- self.usbmux_address = value
134
+ udid = udid.strip()
135
+ port = TUNNELD_DEFAULT_ADDRESS[1]
136
+ if ":" in udid:
137
+ udid, port = udid.split(":")
236
138
 
237
- def udid(self, ctx, param: str, value: Optional[str]) -> Optional[LockdownClient]:
238
- if is_invoked_for_completion():
239
- # prevent lockdown connection establishment when in autocomplete mode
240
- return
241
-
242
- if self.service_provider is not None:
243
- return self.service_provider
244
-
245
- if self.mobdev2_option is not None:
246
- devices = asyncio.run(
247
- self.get_mobdev2_devices(
248
- udid=value if value else None, ips=[self.mobdev2_option] if self.mobdev2_option else None
249
- )
250
- )
251
- if not devices:
252
- raise NoDeviceConnectedError()
253
-
254
- if len(devices) == 1:
255
- self.service_provider = devices[0]
256
- return self.service_provider
257
-
258
- self.service_provider = prompt_device_list(devices)
259
- return self.service_provider
260
-
261
- if value is not None:
262
- return create_using_usbmux(serial=value)
263
-
264
- devices = select_devices_by_connection_type(connection_type="USB", usbmux_address=self.usbmux_address)
265
- if len(devices) <= 1:
266
- return create_using_usbmux(usbmux_address=self.usbmux_address)
267
-
268
- return prompt_device_list([
269
- create_using_usbmux(serial=device.serial, usbmux_address=self.usbmux_address) for device in devices
270
- ])
271
-
272
-
273
- class RSDCommand(BaseServiceProviderCommand):
274
- def __init__(self, *args, **kwargs):
275
- super().__init__(*args, **kwargs)
276
- self.params[:0] = [
277
- RSDOption(
278
- ("rsd_service_provider_manually", "--rsd"),
279
- type=(str, int),
280
- callback=self.rsd,
281
- mutually_exclusive=["rsd_service_provider_using_tunneld"],
282
- help="\b\nRSD hostname and port number (as provided by a `start-tunnel` subcommand).",
139
+ rsds = await async_get_tunneld_devices((TUNNELD_DEFAULT_ADDRESS[0], int(port)))
140
+ if len(rsds) == 0:
141
+ raise NoDeviceConnectedError()
142
+
143
+ if udid != "":
144
+ service_provider = next((rsd for rsd in rsds if rsd.udid == udid), None)
145
+ if service_provider is None:
146
+ raise DeviceNotFoundError(udid) from None
147
+ else:
148
+ service_provider = rsds[0] if len(rsds) == 1 else prompt_device_list(rsds)
149
+
150
+ for rsd in rsds:
151
+ if rsd == service_provider:
152
+ continue
153
+ await rsd.close()
154
+
155
+ return service_provider
156
+
157
+
158
+ def make_rsd_dependency(*, allow_none: bool) -> Callable[..., Optional[RemoteServiceDiscoveryService]]:
159
+ def rsd_dependency(
160
+ rsd: Annotated[
161
+ Optional[tuple[str, int]],
162
+ typer.Option(
163
+ metavar="HOST PORT",
164
+ help=dedent("""\
165
+ Hostname and port of a RemoteServiceDiscovery (from any of the `start-tunnel` subcommands).
166
+ Mutually exclusive with --tunnel.
167
+ """),
168
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
283
169
  ),
284
- RSDOption(
285
- ("rsd_service_provider_using_tunneld", "--tunnel"),
286
- callback=self.tunneld,
287
- mutually_exclusive=["rsd_service_provider_manually"],
170
+ ] = None,
171
+ tunnel: Annotated[
172
+ Optional[str],
173
+ typer.Option(
288
174
  envvar=TUNNEL_ENV_VAR,
289
- help="\b\n"
290
- "Either an empty string to force tunneld device selection, or a UDID of a tunneld "
291
- "discovered device.\n"
292
- "The string may be suffixed with :PORT in case tunneld is not serving at the default port.\n"
293
- f"This option may also be transferred as an environment variable: {TUNNEL_ENV_VAR}",
175
+ help=dedent("""\
176
+ Use a device discovered via tunneld. Provide a UDID (optionally with :PORT) or leave empty to pick
177
+ interactively. Mutually exclusive with --rsd.
178
+ """),
179
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
294
180
  ),
295
- ]
296
-
297
- def rsd(self, ctx, param: str, value: Optional[tuple[str, int]]) -> Optional[RemoteServiceDiscoveryService]:
298
- if value is not None:
299
- rsd = RemoteServiceDiscoveryService(value)
300
- asyncio.run(rsd.connect(), debug=True)
301
- self.service_provider = rsd
302
- return self.service_provider
303
-
304
- async def _tunneld(self, udid: Optional[str] = None) -> Optional[RemoteServiceDiscoveryService]:
305
- if udid is None:
306
- return
307
-
308
- udid = udid.strip()
309
- port = TUNNELD_DEFAULT_ADDRESS[1]
310
- if ":" in udid:
311
- udid, port = udid.split(":")
312
-
313
- rsds = await async_get_tunneld_devices((TUNNELD_DEFAULT_ADDRESS[0], port))
314
- if len(rsds) == 0:
181
+ ] = None,
182
+ ) -> Optional[RemoteServiceDiscoveryService]:
183
+ if is_invoked_for_completion():
184
+ # prevent lockdown connection establishment when in autocomplete mode
185
+ return None
186
+
187
+ if rsd is not None and tunnel is not None:
188
+ raise UsageError("Illegal usage: --rsd is mutually exclusive with --tunnel.")
189
+
190
+ if rsd is not None:
191
+ rsd_service = RemoteServiceDiscoveryService(rsd)
192
+ asyncio.run(rsd_service.connect(), debug=True)
193
+ return rsd_service
194
+
195
+ if tunnel is not None or not allow_none:
196
+ return asyncio.run(_tunneld(tunnel or ""), debug=True)
197
+
198
+ return rsd_dependency
199
+
200
+
201
+ def any_service_provider_dependency(
202
+ rsd_service_provider: Annotated[
203
+ Optional[RemoteServiceDiscoveryService],
204
+ Depends(make_rsd_dependency(allow_none=True)),
205
+ ] = None,
206
+ mobdev2: Annotated[
207
+ bool,
208
+ typer.Option(
209
+ help="Discover devices over bonjour/mobdev2 instead of usbmux.",
210
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
211
+ ),
212
+ ] = False,
213
+ usbmux: Annotated[
214
+ Optional[str],
215
+ typer.Option(
216
+ envvar=USBMUX_ENV_VAR,
217
+ help=USBMUX_OPTION_HELP,
218
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
219
+ ),
220
+ ] = None,
221
+ udid: Annotated[
222
+ Optional[str],
223
+ typer.Option(
224
+ envvar=UDID_ENV_VAR,
225
+ help="Target device UDID (defaults to the first USB device).",
226
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
227
+ ),
228
+ ] = None,
229
+ ) -> LockdownServiceProvider:
230
+ if is_invoked_for_completion():
231
+ # prevent lockdown connection establishment when in autocomplete mode
232
+ return # type: ignore[return-value]
233
+
234
+ if rsd_service_provider is not None:
235
+ return rsd_service_provider
236
+
237
+ if mobdev2:
238
+ devices = asyncio.run(get_mobdev2_devices(udid=udid))
239
+ if not devices:
315
240
  raise NoDeviceConnectedError()
316
241
 
317
- if udid != "":
318
- try:
319
- # Connect to the specified device
320
- self.service_provider = next(
321
- rsd for rsd in rsds if rsd.udid == udid or rsd.udid.replace("-", "") == udid
322
- )
323
- except IndexError as e:
324
- raise DeviceNotFoundError(udid) from e
325
- else:
326
- if len(rsds) == 1:
327
- self.service_provider = rsds[0]
328
- else:
329
- self.service_provider = prompt_device_list(rsds)
242
+ if len(devices) == 1:
243
+ return devices[0]
330
244
 
331
- for rsd in rsds:
332
- if rsd == self.service_provider:
333
- continue
334
- await rsd.close()
245
+ return prompt_device_list(devices)
335
246
 
336
- return self.service_provider
247
+ if udid is not None:
248
+ return create_using_usbmux(serial=udid, usbmux_address=usbmux)
337
249
 
338
- def tunneld(self, ctx, param: str, udid: Optional[str] = None) -> Optional[RemoteServiceDiscoveryService]:
339
- return asyncio.run(self._tunneld(udid), debug=True)
250
+ devices = select_devices_by_connection_type(connection_type="USB", usbmux_address=usbmux)
251
+ if len(devices) <= 1:
252
+ return create_using_usbmux(usbmux_address=usbmux)
340
253
 
254
+ return prompt_device_list([create_using_usbmux(serial=device.serial, usbmux_address=usbmux) for device in devices])
341
255
 
342
- class Command(RSDCommand, LockdownCommand):
343
- def __init__(self, *args, **kwargs):
344
- super().__init__(*args, **kwargs)
345
256
 
257
+ def no_autopair_service_provider_dependency(
258
+ rsd_service_provider: Annotated[
259
+ Optional[RemoteServiceDiscoveryService],
260
+ Depends(make_rsd_dependency(allow_none=True)),
261
+ ] = None,
262
+ udid: Annotated[
263
+ Optional[str],
264
+ typer.Option(
265
+ envvar=UDID_ENV_VAR,
266
+ help="Target device UDID (defaults to the first USB device).",
267
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
268
+ ),
269
+ ] = None,
270
+ ) -> LockdownServiceProvider:
271
+ if is_invoked_for_completion():
272
+ # prevent lockdown connection establishment when in autocomplete mode
273
+ return # type: ignore[return-value]
346
274
 
347
- class CommandWithoutAutopair(Command):
348
- @staticmethod
349
- def udid(ctx, param, value):
350
- if is_invoked_for_completion():
351
- # prevent lockdown connection establishment when in autocomplete mode
352
- return
353
- return create_using_usbmux(serial=value, autopair=False)
275
+ if rsd_service_provider is not None:
276
+ return rsd_service_provider
277
+
278
+ return create_using_usbmux(serial=udid, autopair=False)
279
+
280
+
281
+ RSDServiceProviderDep = Annotated[
282
+ RemoteServiceDiscoveryService,
283
+ Depends(make_rsd_dependency(allow_none=False)),
284
+ ]
285
+
286
+
287
+ ServiceProviderDep = Annotated[
288
+ LockdownServiceProvider,
289
+ Depends(any_service_provider_dependency),
290
+ ]
291
+
292
+
293
+ NoAutoPairServiceProviderDep = Annotated[
294
+ LockdownServiceProvider,
295
+ Depends(no_autopair_service_provider_dependency),
296
+ ]
354
297
 
355
298
 
356
299
  class BasedIntParamType(click.ParamType):
@@ -1,22 +1,22 @@
1
- import click
1
+ from typer_injector import InjectingTyper
2
2
 
3
- from pymobiledevice3.cli.cli_common import Command, print_json
4
- from pymobiledevice3.lockdown import LockdownClient
3
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
5
4
  from pymobiledevice3.services.companion import CompanionProxyService
6
5
 
7
-
8
- @click.group()
9
- def cli() -> None:
10
- pass
6
+ cli = InjectingTyper(
7
+ name="companion",
8
+ help='List paired "companion" devices',
9
+ no_args_is_help=True,
10
+ )
11
11
 
12
12
 
13
- @cli.group()
14
- def companion() -> None:
15
- """List paired "companion" devices"""
13
+ @cli.callback()
14
+ def callback() -> None:
15
+ # Force subgroup
16
16
  pass
17
17
 
18
18
 
19
- @companion.command("list", cls=Command)
20
- def companion_list(service_provider: LockdownClient):
19
+ @cli.command("list")
20
+ def companion_list(service_provider: ServiceProviderDep) -> None:
21
21
  """list all paired companion devices"""
22
22
  print_json(CompanionProxyService(service_provider).list(), default=lambda x: "<non-serializable>")