pymobiledevice3 6.2.0__py3-none-any.whl → 7.0.0__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 (61) hide show
  1. pymobiledevice3/__main__.py +136 -44
  2. pymobiledevice3/_version.py +2 -2
  3. pymobiledevice3/bonjour.py +19 -20
  4. pymobiledevice3/cli/activation.py +24 -22
  5. pymobiledevice3/cli/afc.py +49 -41
  6. pymobiledevice3/cli/amfi.py +13 -18
  7. pymobiledevice3/cli/apps.py +71 -65
  8. pymobiledevice3/cli/backup.py +134 -93
  9. pymobiledevice3/cli/bonjour.py +31 -29
  10. pymobiledevice3/cli/cli_common.py +179 -232
  11. pymobiledevice3/cli/companion_proxy.py +12 -12
  12. pymobiledevice3/cli/crash.py +95 -52
  13. pymobiledevice3/cli/developer/__init__.py +62 -0
  14. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  15. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  16. pymobiledevice3/cli/developer/arbitration.py +50 -0
  17. pymobiledevice3/cli/developer/condition.py +33 -0
  18. pymobiledevice3/cli/developer/core_device.py +294 -0
  19. pymobiledevice3/cli/developer/debugserver.py +244 -0
  20. pymobiledevice3/cli/developer/dvt/__init__.py +387 -0
  21. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  22. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  23. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  24. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  25. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  26. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  27. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  28. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  29. pymobiledevice3/cli/idam.py +18 -22
  30. pymobiledevice3/cli/lockdown.py +70 -75
  31. pymobiledevice3/cli/mounter.py +99 -57
  32. pymobiledevice3/cli/notification.py +38 -26
  33. pymobiledevice3/cli/pcap.py +36 -20
  34. pymobiledevice3/cli/power_assertion.py +15 -16
  35. pymobiledevice3/cli/processes.py +11 -17
  36. pymobiledevice3/cli/profile.py +120 -75
  37. pymobiledevice3/cli/provision.py +27 -26
  38. pymobiledevice3/cli/remote.py +108 -99
  39. pymobiledevice3/cli/restore.py +134 -129
  40. pymobiledevice3/cli/springboard.py +50 -50
  41. pymobiledevice3/cli/syslog.py +138 -74
  42. pymobiledevice3/cli/usbmux.py +66 -27
  43. pymobiledevice3/cli/version.py +2 -5
  44. pymobiledevice3/cli/webinspector.py +149 -103
  45. pymobiledevice3/remote/remote_service_discovery.py +11 -10
  46. pymobiledevice3/restore/device.py +28 -4
  47. pymobiledevice3/service_connection.py +1 -1
  48. pymobiledevice3/services/mobilebackup2.py +4 -1
  49. pymobiledevice3/services/screenshot.py +2 -2
  50. pymobiledevice3/services/web_protocol/automation_session.py +4 -2
  51. pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
  52. pymobiledevice3/services/web_protocol/element.py +3 -3
  53. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/METADATA +3 -2
  54. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/RECORD +58 -45
  55. pymobiledevice3/cli/completions.py +0 -50
  56. pymobiledevice3/cli/developer.py +0 -1645
  57. pymobiledevice3/cli/diagnostics.py +0 -110
  58. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/WHEEL +0 -0
  59. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/entry_points.txt +0 -0
  60. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/licenses/LICENSE +0 -0
  61. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/top_level.txt +0 -0
@@ -1,30 +1,34 @@
1
1
  import asyncio
2
+ import inspect
2
3
  import logging
3
4
  import re
4
5
  from abc import ABC, abstractmethod
5
6
  from asyncio import CancelledError
6
7
  from collections.abc import AsyncIterator, Iterable
7
- from contextlib import asynccontextmanager
8
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager
8
9
  from functools import update_wrapper
9
- from typing import Optional, Union
10
+ from string import Template
11
+ from typing import Annotated, Any, Optional
10
12
 
11
- import click
12
13
  import inquirer3
13
14
  import IPython
14
15
  import nest_asyncio
16
+ import typer
15
17
  import uvicorn
16
18
  from inquirer3.themes import GreenPassion
17
19
  from prompt_toolkit import HTML, PromptSession
18
20
  from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
19
- from prompt_toolkit.completion.base import CompleteEvent, Completer, Completion, Document
21
+ from prompt_toolkit.completion.base import CompleteEvent, Completer, Completion
22
+ from prompt_toolkit.document import Document
20
23
  from prompt_toolkit.history import FileHistory
21
24
  from prompt_toolkit.lexers import PygmentsLexer
22
25
  from prompt_toolkit.patch_stdout import patch_stdout
23
26
  from prompt_toolkit.styles import style_from_pygments_cls
24
27
  from pygments import formatters, highlight, lexers
25
28
  from pygments.styles import get_style_by_name
29
+ from typer_injector import InjectingTyper
26
30
 
27
- from pymobiledevice3.cli.cli_common import Command
31
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep
28
32
  from pymobiledevice3.common import get_home_folder
29
33
  from pymobiledevice3.exceptions import (
30
34
  InspectorEvaluateError,
@@ -33,35 +37,34 @@ from pymobiledevice3.exceptions import (
33
37
  WebInspectorNotEnabledError,
34
38
  WirError,
35
39
  )
36
- from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux
40
+ from pymobiledevice3.lockdown import create_using_usbmux
37
41
  from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
38
42
  from pymobiledevice3.osu.os_utils import get_os_utils
39
43
  from pymobiledevice3.services.web_protocol.cdp_server import app
40
44
  from pymobiledevice3.services.web_protocol.driver import By, Cookie, WebDriver
41
45
  from pymobiledevice3.services.web_protocol.inspector_session import InspectorSession
42
- from pymobiledevice3.services.webinspector import SAFARI, ApplicationPage, WebinspectorService
43
-
44
- SCRIPT = """
45
- function inspectedPage_evalResult_getCompletions(primitiveType) {{
46
- var resultSet={{}};
47
- var object = primitiveType;
48
- for(var o=object;o;o=o.__proto__) {{
49
-
50
- try{{
51
- var names=Object.getOwnPropertyNames(o);
52
- for(var i=0;i<names.length;++i)
53
- resultSet[names[i]]=true;
54
- }} catch(e){{}}
55
- }}
46
+ from pymobiledevice3.services.webinspector import SAFARI, Application, ApplicationPage, WebinspectorService
47
+
48
+ SCRIPT = Template("""
49
+ function inspectedPage_evalResult_getCompletions(primitiveType) {
50
+ let resultSet = {};
51
+ let object = primitiveType;
52
+ for (let o = object; o; o = o.__proto__) {
53
+ try {
54
+ let names = Object.getOwnPropertyNames(o);
55
+ for (let i = 0; i < names.length; ++i)
56
+ resultSet[names[i]] = true;
57
+ } catch(e) {}
58
+ }
56
59
  return resultSet;
57
- }}
60
+ }
58
61
 
59
- try {{
60
- inspectedPage_evalResult_getCompletions({object})
61
- }} catch (e) {{}}
62
- """
62
+ try {
63
+ inspectedPage_evalResult_getCompletions(${object})
64
+ } catch (e) {}
65
+ """)
63
66
 
64
- JS_RESERVED_WORDS = [
67
+ JS_RESERVED_WORDS = frozenset({
65
68
  "abstract",
66
69
  "arguments",
67
70
  "await",
@@ -126,51 +129,67 @@ JS_RESERVED_WORDS = [
126
129
  "while",
127
130
  "with",
128
131
  "yield",
129
- ]
132
+ })
130
133
 
131
134
  OSUTILS = get_os_utils()
132
135
  logger = logging.getLogger(__name__)
133
136
 
134
137
 
135
- @click.group()
136
- def cli() -> None:
137
- pass
138
+ cli = InjectingTyper(
139
+ name="webinspector",
140
+ help=(
141
+ "Control Safari/WebViews (tabs, automation, JS shells, CDP). "
142
+ "Requires Web Inspector and Remote Automation enabled on the device."
143
+ ),
144
+ no_args_is_help=True,
145
+ )
138
146
 
139
147
 
140
- @cli.group()
141
- def webinspector() -> None:
142
- """Access webinspector services"""
143
- pass
148
+ def catch_errors(func):
149
+ errors = {
150
+ LaunchingApplicationError: "Unable to launch application (try to unlock device)",
151
+ WebInspectorNotEnabledError: "Web inspector is not enabled",
152
+ RemoteAutomationNotEnabledError: "Remote automation is not enabled",
153
+ }
144
154
 
155
+ def handle_error(e):
156
+ logger.error(next(msg for exc, msg in errors.items() if isinstance(e, exc)))
145
157
 
146
- def catch_errors(func):
147
- def catch_function(*args, **kwargs):
148
- try:
149
- return func(*args, **kwargs)
150
- except LaunchingApplicationError:
151
- logger.error("Unable to launch application (try to unlock device)")
152
- except WebInspectorNotEnabledError:
153
- logger.error("Web inspector is not enable")
154
- except RemoteAutomationNotEnabledError:
155
- logger.error("Remote automation is not enable")
158
+ if inspect.iscoroutinefunction(func):
159
+
160
+ async def catch_function(*args, **kwargs):
161
+ try:
162
+ return await func(*args, **kwargs)
163
+ except tuple(errors) as e:
164
+ handle_error(e)
165
+
166
+ else:
167
+
168
+ def catch_function(*args, **kwargs):
169
+ try:
170
+ return func(*args, **kwargs)
171
+ except tuple(errors) as e:
172
+ handle_error(e)
156
173
 
157
174
  return update_wrapper(catch_function, func)
158
175
 
159
176
 
160
- def reload_pages(inspector: WebinspectorService):
161
- inspector.get_open_pages()
177
+ async def reload_pages(inspector: WebinspectorService) -> None:
178
+ await inspector.get_open_pages()
162
179
  # Best effort.
163
- inspector.flush_input(2)
180
+ await inspector.flush_input(2)
164
181
 
165
182
 
166
- async def create_webinspector_and_launch_app(lockdown: LockdownClient, timeout: float, app: str):
183
+ async def create_webinspector_and_launch_app(
184
+ lockdown: LockdownServiceProvider, timeout: float, app: str
185
+ ) -> tuple[WebinspectorService, Application]:
167
186
  inspector = WebinspectorService(lockdown=lockdown)
168
187
  await inspector.connect(timeout)
169
188
  application = await inspector.open_app(app)
170
189
  return inspector, application
171
190
 
172
191
 
173
- async def opened_tabs_task(service_provider: LockdownClient, timeout):
192
+ async def opened_tabs_task(service_provider: LockdownServiceProvider, timeout: float) -> None:
174
193
  inspector = WebinspectorService(lockdown=service_provider)
175
194
  await inspector.connect(timeout)
176
195
  application_pages = await inspector.get_open_application_pages(timeout=timeout)
@@ -179,10 +198,15 @@ async def opened_tabs_task(service_provider: LockdownClient, timeout):
179
198
  await inspector.close()
180
199
 
181
200
 
182
- @webinspector.command(cls=Command)
183
- @click.option("-t", "--timeout", default=3, show_default=True, type=float)
201
+ @cli.command()
184
202
  @catch_errors
185
- def opened_tabs(service_provider: LockdownClient, timeout):
203
+ def opened_tabs(
204
+ service_provider: ServiceProviderDep,
205
+ timeout: Annotated[
206
+ float,
207
+ typer.Option("--timeout", "-t", help="Seconds to wait for WebInspector to respond."),
208
+ ] = 3.0,
209
+ ) -> None:
186
210
  """
187
211
  Show all currently opened tabs.
188
212
 
@@ -196,7 +220,7 @@ def opened_tabs(service_provider: LockdownClient, timeout):
196
220
 
197
221
 
198
222
  @catch_errors
199
- async def launch_task(service_provider: LockdownClient, url, timeout):
223
+ async def launch_task(service_provider: LockdownServiceProvider, url, timeout) -> None:
200
224
  inspector, safari = await create_webinspector_and_launch_app(service_provider, timeout, SAFARI)
201
225
  session = await inspector.automation_session(safari)
202
226
  driver = WebDriver(session)
@@ -209,11 +233,16 @@ async def launch_task(service_provider: LockdownClient, url, timeout):
209
233
  await inspector.close()
210
234
 
211
235
 
212
- @webinspector.command(cls=Command)
213
- @click.argument("url")
214
- @click.option("-t", "--timeout", default=3, show_default=True, type=float)
236
+ @cli.command()
215
237
  @catch_errors
216
- def launch(service_provider: LockdownClient, url, timeout):
238
+ def launch(
239
+ service_provider: ServiceProviderDep,
240
+ url: str,
241
+ timeout: Annotated[
242
+ float,
243
+ typer.Option("--timeout", "-t", help="Seconds to wait for WebInspector to respond."),
244
+ ] = 3.0,
245
+ ) -> None:
217
246
  """
218
247
  Launch a specific URL in Safari.
219
248
 
@@ -252,7 +281,7 @@ driver.add_cookie(
252
281
 
253
282
 
254
283
  @catch_errors
255
- async def shell_task(service_provider: LockdownClient, timeout):
284
+ async def shell_task(service_provider: LockdownServiceProvider, timeout: float) -> None:
256
285
  inspector, safari = await create_webinspector_and_launch_app(service_provider, timeout, SAFARI)
257
286
  session = await inspector.automation_session(safari)
258
287
  driver = WebDriver(session)
@@ -271,9 +300,15 @@ async def shell_task(service_provider: LockdownClient, timeout):
271
300
  await inspector.close()
272
301
 
273
302
 
274
- @webinspector.command(cls=Command)
275
- @click.option("-t", "--timeout", default=3, show_default=True, type=float)
276
- def shell(service_provider: LockdownClient, timeout):
303
+ @cli.command()
304
+ @catch_errors
305
+ def shell(
306
+ service_provider: ServiceProviderDep,
307
+ timeout: Annotated[
308
+ float,
309
+ typer.Option("--timeout", "-t", help="Seconds to wait for WebInspector to respond."),
310
+ ] = 3.0,
311
+ ) -> None:
277
312
  """
278
313
  Create an IPython shell for interacting with a WebView.
279
314
 
@@ -289,14 +324,23 @@ def shell(service_provider: LockdownClient, timeout):
289
324
  asyncio.run(shell_task(service_provider, timeout), debug=True)
290
325
 
291
326
 
292
- @webinspector.command(cls=Command)
293
- @click.option("-t", "--timeout", default=3, show_default=True, type=float)
294
- @click.option("--automation", is_flag=True, help="Use remote automation")
295
- @click.option("--no-open-safari", is_flag=True, help="Avoid opening the Safari app")
296
- @click.argument("url", required=False, default="")
327
+ @cli.command()
297
328
  @catch_errors
298
329
  def js_shell(
299
- service_provider: LockdownServiceProvider, timeout: float, automation: bool, no_open_safari: bool, url: str
330
+ service_provider: ServiceProviderDep,
331
+ url: str = "",
332
+ timeout: Annotated[
333
+ float,
334
+ typer.Option("--timeout", "-t", help="Seconds to wait for WebInspector to respond."),
335
+ ] = 3.0,
336
+ automation: Annotated[
337
+ bool,
338
+ typer.Option(help="Use remote automation (requires Remote Automation toggle)."),
339
+ ] = False,
340
+ open_safari: Annotated[
341
+ bool,
342
+ typer.Option(help="Use an existing WebView; skip auto-opening Safari."),
343
+ ] = False,
300
344
  ) -> None:
301
345
  """
302
346
  Create a javascript shell. This interpreter runs on your local machine,
@@ -313,7 +357,7 @@ def js_shell(
313
357
  """
314
358
 
315
359
  js_shell_class = AutomationJsShell if automation else InspectorJsShell
316
- asyncio.run(run_js_shell(js_shell_class, service_provider, timeout, url, not no_open_safari))
360
+ asyncio.run(run_js_shell(js_shell_class, service_provider, timeout, url, open_safari))
317
361
 
318
362
 
319
363
  udid = ""
@@ -325,10 +369,8 @@ def create_app():
325
369
  return app
326
370
 
327
371
 
328
- @webinspector.command(cls=Command)
329
- @click.option("--host", default="127.0.0.1")
330
- @click.option("--port", type=click.INT, default=9222)
331
- def cdp(service_provider: LockdownClient, host, port):
372
+ @cli.command()
373
+ def cdp(service_provider: ServiceProviderDep, host: str = "127.0.0.1", port: int = 9222) -> None:
332
374
  """
333
375
  Start a CDP server for debugging WebViews.
334
376
 
@@ -339,7 +381,7 @@ def cdp(service_provider: LockdownClient, host, port):
339
381
  global udid
340
382
  udid = service_provider.udid
341
383
  uvicorn.run(
342
- "pymobiledevice3.cli.webinspector:create_app",
384
+ f"{__name__}:{create_app.__name__}",
343
385
  host=host,
344
386
  port=port,
345
387
  factory=True,
@@ -354,7 +396,7 @@ async def get_js_completions(jsshell: "JsShell", obj: str, prefix: str) -> Async
354
396
  return
355
397
 
356
398
  try:
357
- for key in await jsshell.evaluate_expression(SCRIPT.format(object=obj), return_by_value=True):
399
+ for key in await jsshell.evaluate_expression(SCRIPT.substitute(object=obj), return_by_value=True):
358
400
  if not key.startswith(prefix):
359
401
  continue
360
402
  yield Completion(key.removeprefix(prefix), display=key)
@@ -364,14 +406,14 @@ async def get_js_completions(jsshell: "JsShell", obj: str, prefix: str) -> Async
364
406
 
365
407
 
366
408
  class JsShellCompleter(Completer):
367
- def __init__(self, jsshell: "JsShell"):
368
- self.jsshell = jsshell
409
+ def __init__(self, jsshell: "JsShell") -> None:
410
+ self.jsshell: JsShell = jsshell
369
411
 
370
- def get_completions_async(
412
+ async def get_completions_async(
371
413
  self,
372
414
  document: Document,
373
415
  complete_event: CompleteEvent,
374
- ) -> Union[AsyncIterator[Completion], Iterable[Completion]]:
416
+ ) -> AsyncIterator[Completion]:
375
417
  # Build the JS expression we want to inspect
376
418
  text = f"globalThis.{document.text_before_cursor}"
377
419
 
@@ -379,7 +421,7 @@ class JsShellCompleter(Completer):
379
421
  matches = re.findall(r"[a-zA-Z_][a-zA-Z_0-9.]+", text)
380
422
  if not matches:
381
423
  # async *generator*: just end, don't return a list
382
- return iter(())
424
+ return
383
425
 
384
426
  text = matches[-1]
385
427
  if "." in text:
@@ -389,7 +431,8 @@ class JsShellCompleter(Completer):
389
431
  prefix = ""
390
432
 
391
433
  # This should return an iterable of Completion (or something we can wrap)
392
- return get_js_completions(self.jsshell, js_obj, prefix)
434
+ async for completion in get_js_completions(self.jsshell, js_obj, prefix):
435
+ yield completion
393
436
 
394
437
  # Optional: keep sync completions empty so PTK knows we prefer async
395
438
  def get_completions(
@@ -403,7 +446,7 @@ class JsShellCompleter(Completer):
403
446
  class JsShell(ABC):
404
447
  def __init__(self) -> None:
405
448
  super().__init__()
406
- self.prompt_session = PromptSession(
449
+ self.prompt_session: PromptSession = PromptSession(
407
450
  lexer=PygmentsLexer(lexers.JavascriptLexer),
408
451
  auto_suggest=AutoSuggestFromHistory(),
409
452
  style=style_from_pygments_cls(get_style_by_name("stata-dark")),
@@ -413,18 +456,17 @@ class JsShell(ABC):
413
456
 
414
457
  @classmethod
415
458
  @abstractmethod
416
- def create(cls, lockdown: LockdownServiceProvider, timeout: float, open_safari: bool) -> None:
417
- pass
459
+ def create(
460
+ cls, lockdown: LockdownServiceProvider, timeout: float, open_safari: bool
461
+ ) -> "AbstractAsyncContextManager[JsShell]": ...
418
462
 
419
463
  @abstractmethod
420
- async def evaluate_expression(self, exp, return_by_value: bool = False):
421
- pass
464
+ async def evaluate_expression(self, exp, return_by_value: bool = False) -> Any: ...
422
465
 
423
466
  @abstractmethod
424
- async def navigate(self, url: str):
425
- pass
467
+ async def navigate(self, url: str) -> None: ...
426
468
 
427
- async def js_iter(self):
469
+ async def js_iter(self) -> None:
428
470
  with patch_stdout(True):
429
471
  exp = await self.prompt_session.prompt_async(HTML('<style fg="cyan"><b>&gt;</b></style> '))
430
472
 
@@ -437,7 +479,7 @@ class JsShell(ABC):
437
479
  )
438
480
  print(colorful_result, end="")
439
481
 
440
- async def start(self, url: str = ""):
482
+ async def start(self, url: str = "") -> None:
441
483
  if url:
442
484
  await self.navigate(url)
443
485
  while True:
@@ -456,45 +498,49 @@ class JsShell(ABC):
456
498
 
457
499
 
458
500
  class AutomationJsShell(JsShell):
459
- def __init__(self, driver: WebDriver):
501
+ def __init__(self, driver: WebDriver) -> None:
460
502
  super().__init__()
461
- self.driver = driver
503
+ self.driver: WebDriver = driver
462
504
 
463
505
  @classmethod
464
506
  @asynccontextmanager
465
- async def create(cls, lockdown: LockdownClient, timeout: float, open_safari: bool) -> "AutomationJsShell":
466
- inspector, application = create_webinspector_and_launch_app(lockdown, timeout, SAFARI)
467
- automation_session = inspector.automation_session(application)
507
+ async def create(
508
+ cls, lockdown: LockdownServiceProvider, timeout: float, open_safari: bool
509
+ ) -> "AsyncIterator[AutomationJsShell]":
510
+ inspector, application = await create_webinspector_and_launch_app(lockdown, timeout, SAFARI)
511
+ automation_session = await inspector.automation_session(application)
468
512
  driver = WebDriver(automation_session)
469
513
  await driver.start_session()
470
514
  try:
471
515
  yield cls(driver)
472
516
  finally:
473
- automation_session.stop_session()
474
- inspector.close()
517
+ await automation_session.stop_session()
518
+ await inspector.close()
475
519
 
476
- async def evaluate_expression(self, exp: str, return_by_value: bool = False):
520
+ async def evaluate_expression(self, exp: str, return_by_value: bool = False) -> Any:
477
521
  return await self.driver.execute_script(f"return {exp}")
478
522
 
479
- async def navigate(self, url: str):
523
+ async def navigate(self, url: str) -> None:
480
524
  await self.driver.get(url)
481
525
 
482
526
 
483
527
  class InspectorJsShell(JsShell):
484
- def __init__(self, inspector_session: InspectorSession):
528
+ def __init__(self, inspector_session: InspectorSession) -> None:
485
529
  super().__init__()
486
- self.inspector_session = inspector_session
530
+ self.inspector_session: InspectorSession = inspector_session
487
531
 
488
532
  @classmethod
489
533
  @asynccontextmanager
490
- async def create(cls, lockdown: LockdownClient, timeout: float, open_safari: bool) -> "InspectorJsShell":
534
+ async def create(
535
+ cls, lockdown: LockdownServiceProvider, timeout: float, open_safari: bool
536
+ ) -> "AsyncIterator[InspectorJsShell]":
491
537
  inspector = WebinspectorService(lockdown=lockdown)
492
538
  await inspector.connect(timeout)
493
539
  if open_safari:
494
540
  _ = await inspector.open_app(SAFARI)
495
541
  application_page = await cls.query_page(inspector, bundle_identifier=SAFARI if open_safari else None)
496
542
  if application_page is None:
497
- raise click.exceptions.Exit()
543
+ raise typer.Exit()
498
544
 
499
545
  inspector_session = await inspector.inspector_session(application_page.application, application_page.page)
500
546
  await inspector_session.console_enable()
@@ -505,7 +551,7 @@ class InspectorJsShell(JsShell):
505
551
  finally:
506
552
  await inspector.close()
507
553
 
508
- async def evaluate_expression(self, exp: str, return_by_value: bool = False):
554
+ async def evaluate_expression(self, exp: str, return_by_value: bool = False) -> Any:
509
555
  return await self.inspector_session.runtime_evaluate(exp, return_by_value=return_by_value)
510
556
 
511
557
  async def navigate(self, url: str):
@@ -166,17 +166,18 @@ class RemoteServiceDiscoveryService(LockdownServiceProvider):
166
166
 
167
167
  async def get_remoted_devices(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[RSDDevice]:
168
168
  result = []
169
- for hostname in await browse_remoted(timeout):
170
- with RemoteServiceDiscoveryService((hostname, RSD_PORT)) as rsd:
171
- properties = rsd.peer_info["Properties"]
172
- result.append(
173
- RSDDevice(
174
- hostname=hostname,
175
- udid=properties["UniqueDeviceID"],
176
- product_type=properties["ProductType"],
177
- os_version=properties["OSVersion"],
169
+ for instance in await browse_remoted(timeout):
170
+ for address in instance.addresses:
171
+ with RemoteServiceDiscoveryService((address.full_ip, RSD_PORT)) as rsd:
172
+ properties = rsd.peer_info["Properties"]
173
+ result.append(
174
+ RSDDevice(
175
+ hostname=address.full_ip,
176
+ udid=properties["UniqueDeviceID"],
177
+ product_type=properties["ProductType"],
178
+ os_version=properties["OSVersion"],
179
+ )
178
180
  )
179
- )
180
181
  return result
181
182
 
182
183
 
@@ -1,6 +1,6 @@
1
1
  from contextlib import suppress
2
2
  from functools import cached_property
3
- from typing import Optional
3
+ from typing import Optional, overload
4
4
 
5
5
  from pymobiledevice3.exceptions import MissingValueError
6
6
  from pymobiledevice3.irecv import IRecv
@@ -8,9 +8,15 @@ from pymobiledevice3.lockdown import LockdownClient
8
8
 
9
9
 
10
10
  class Device:
11
- def __init__(self, lockdown: LockdownClient = None, irecv: IRecv = None):
12
- self.lockdown = lockdown
13
- self.irecv = irecv
11
+ @overload
12
+ def __init__(self, lockdown: LockdownClient, irecv: None = None) -> None: ...
13
+
14
+ @overload
15
+ def __init__(self, lockdown: None = None, *, irecv: IRecv) -> None: ...
16
+
17
+ def __init__(self, lockdown: Optional[LockdownClient] = None, irecv: Optional[IRecv] = None) -> None:
18
+ self._lockdown: Optional[LockdownClient] = lockdown
19
+ self._irecv: Optional[IRecv] = irecv
14
20
 
15
21
  def __repr__(self) -> str:
16
22
  return (
@@ -20,6 +26,24 @@ class Device:
20
26
  f"image4-support: {self.is_image4_supported}>"
21
27
  )
22
28
 
29
+ @cached_property
30
+ def is_lockdown(self) -> bool:
31
+ return self._lockdown is not None
32
+
33
+ @cached_property
34
+ def is_irecv(self) -> bool:
35
+ return self._irecv is not None
36
+
37
+ @cached_property
38
+ def lockdown(self) -> LockdownClient:
39
+ assert self._lockdown is not None
40
+ return self._lockdown
41
+
42
+ @cached_property
43
+ def irecv(self) -> IRecv:
44
+ assert self._irecv is not None
45
+ return self._irecv
46
+
23
47
  @cached_property
24
48
  def ecid(self):
25
49
  if self.lockdown:
@@ -192,7 +192,7 @@ class ServiceConnection:
192
192
  except ssl.SSLEOFError as e:
193
193
  raise ConnectionTerminatedError from e
194
194
 
195
- def send_recv_plist(self, data: dict, endianity: str = ">", fmt: Enum = plistlib.FMT_XML) -> Any:
195
+ def send_recv_plist(self, data: Union[dict, list], endianity: str = ">", fmt: Enum = plistlib.FMT_XML) -> Any:
196
196
  """
197
197
  Send a plist to the socket and receive a plist response.
198
198
 
@@ -5,6 +5,7 @@ import uuid
5
5
  from contextlib import contextmanager, suppress
6
6
  from datetime import datetime
7
7
  from pathlib import Path
8
+ from typing import Union
8
9
 
9
10
  from pymobiledevice3.exceptions import (
10
11
  AfcException,
@@ -60,7 +61,9 @@ class Mobilebackup2Service(LockdownService):
60
61
  except LockdownError:
61
62
  return False
62
63
 
63
- def backup(self, full: bool = True, backup_directory: str = ".", progress_callback=lambda x: None) -> None:
64
+ def backup(
65
+ self, full: bool = True, backup_directory: Union[str, Path] = ".", progress_callback=lambda x: None
66
+ ) -> None:
64
67
  """
65
68
  Backup a device.
66
69
  :param full: Whether to do a full backup. If full is True, any previous backup attempts will be discarded.
@@ -1,12 +1,12 @@
1
1
  from pymobiledevice3.exceptions import PyMobileDevice3Exception
2
- from pymobiledevice3.lockdown import LockdownClient
2
+ from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
3
3
  from pymobiledevice3.services.lockdown_service import LockdownService
4
4
 
5
5
 
6
6
  class ScreenshotService(LockdownService):
7
7
  SERVICE_NAME = "com.apple.mobile.screenshotr"
8
8
 
9
- def __init__(self, lockdown: LockdownClient):
9
+ def __init__(self, lockdown: LockdownServiceProvider) -> None:
10
10
  super().__init__(lockdown, self.SERVICE_NAME, is_developer_service=True)
11
11
 
12
12
  dl_message_version_exchange = self.service.recv_plist()
@@ -1,9 +1,11 @@
1
+ import importlib.resources
1
2
  import json
2
3
  from dataclasses import dataclass
3
4
  from enum import Enum
4
- from pathlib import Path
5
5
 
6
- RESOURCES = Path(__file__).parent.parent.parent / "resources" / "webinspector"
6
+ import pymobiledevice3.resources
7
+
8
+ RESOURCES = importlib.resources.files(pymobiledevice3.resources) / "webinspector"
7
9
  FIND_NODES = (RESOURCES / "find_nodes.js").read_text()
8
10
 
9
11
 
@@ -3,6 +3,7 @@ import contextlib
3
3
  from base64 import b64decode, b64encode
4
4
  from datetime import datetime
5
5
  from io import BytesIO
6
+ from typing import Optional
6
7
 
7
8
  from PIL import Image
8
9
 
@@ -28,7 +29,7 @@ class ScreenCast:
28
29
  self.device_height = 0
29
30
  self.page_scale_factor = 0
30
31
  self._run = True
31
- self.recording_task = None # type: asyncio.Task | None
32
+ self.recording_task: Optional[asyncio.Task] = None
32
33
 
33
34
  @property
34
35
  def scale(self) -> float:
@@ -103,7 +103,7 @@ class WebElement(SeleniumApi):
103
103
  @property
104
104
  async def rect(self) -> Rect:
105
105
  """The size and location of the element."""
106
- return await self._compute_layout(scroll_if_needed=False)[0]
106
+ return (await self._compute_layout(scroll_if_needed=False))[0]
107
107
 
108
108
  @property
109
109
  async def screenshot_as_base64(self) -> str:
@@ -144,7 +144,7 @@ class WebElement(SeleniumApi):
144
144
 
145
145
  async def submit(self):
146
146
  """Submits a form."""
147
- form = self.find_element(By.XPATH, "./ancestor-or-self::form")
147
+ form = await self.find_element(By.XPATH, "./ancestor-or-self::form")
148
148
  submit_code = (
149
149
  "var e = arguments[0].ownerDocument.createEvent('Event');"
150
150
  "e.initEvent('submit', true, true);"
@@ -164,7 +164,7 @@ class WebElement(SeleniumApi):
164
164
  'function(element) { return element.innerText.replace(/^[^\\S\\xa0]+|[^\\S\\xa0]+$/g, "") }'
165
165
  )
166
166
 
167
- async def touch(self):
167
+ async def touch(self) -> None:
168
168
  """Simulate touch interaction on the element."""
169
169
  _rect, center, _is_obscured = await self._compute_layout(use_viewport=True)
170
170
  await self.session.perform_interaction_sequence(