uiautodev 0.3.5__tar.gz → 0.3.7__tar.gz

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.

Potentially problematic release.


This version of uiautodev might be problematic. Click here for more details.

Files changed (29) hide show
  1. {uiautodev-0.3.5 → uiautodev-0.3.7}/PKG-INFO +2 -1
  2. {uiautodev-0.3.5 → uiautodev-0.3.7}/pyproject.toml +2 -1
  3. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/app.py +1 -1
  4. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/android.py +39 -51
  5. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/model.py +8 -0
  6. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/router/device.py +8 -4
  7. {uiautodev-0.3.5 → uiautodev-0.3.7}/LICENSE +0 -0
  8. {uiautodev-0.3.5 → uiautodev-0.3.7}/README.md +0 -0
  9. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/__init__.py +0 -0
  10. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/__main__.py +0 -0
  11. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/appium_proxy.py +0 -0
  12. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/case.py +0 -0
  13. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/cli.py +0 -0
  14. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/command_proxy.py +0 -0
  15. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/command_types.py +0 -0
  16. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/common.py +0 -0
  17. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/appium.py +0 -0
  18. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/base_driver.py +0 -0
  19. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/ios.py +0 -0
  20. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/mock.py +0 -0
  21. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
  22. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/udt/udt.py +0 -0
  23. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/exceptions.py +0 -0
  24. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/provider.py +0 -0
  25. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/router/xml.py +0 -0
  26. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/static/demo.html +0 -0
  27. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/utils/common.py +0 -0
  28. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/utils/exceptions.py +0 -0
  29. {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/utils/usbmux.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiautodev
3
- Version: 0.3.5
3
+ Version: 0.3.7
4
4
  Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
5
5
  Home-page: https://uiauto.dev
6
6
  License: MIT
@@ -25,6 +25,7 @@ Requires-Dist: httpx
25
25
  Requires-Dist: lxml
26
26
  Requires-Dist: pillow
27
27
  Requires-Dist: poetry (>=1.8.2,<2.0.0)
28
+ Requires-Dist: pydantic (>=2.6,<3.0)
28
29
  Requires-Dist: pygments (>=2)
29
30
  Requires-Dist: uiautomator2 (>=2)
30
31
  Requires-Dist: uvicorn[standard]
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "uiautodev"
3
- version = "0.3.5"
3
+ version = "0.3.7"
4
4
  description = "Mobile UI Automation, include UI hierarchy inspector, script recorder"
5
5
  homepage = "https://uiauto.dev"
6
6
  authors = ["codeskyblue <codeskyblue@gmail.com>"]
@@ -22,6 +22,7 @@ httpx = "*"
22
22
  fastapi = "^0.111.0"
23
23
  uvicorn = {version = "*", extras = ["standard"]}
24
24
  poetry = "^1.8.2"
25
+ pydantic = "^2.6"
25
26
 
26
27
  [tool.poetry.extras]
27
28
  appium = ["appium-python-client", "httppretty"]
@@ -68,7 +68,7 @@ def info() -> InfoResponse:
68
68
  platform=platform.system(), # Linux | Darwin | Windows
69
69
  code_language="Python",
70
70
  cwd=os.getcwd(),
71
- drivers=["android"],
71
+ drivers=["android", "ios"],
72
72
  )
73
73
 
74
74
 
@@ -9,11 +9,10 @@ import logging
9
9
  import re
10
10
  import time
11
11
  from functools import cached_property, partial
12
- from typing import List, Tuple
12
+ from typing import List, Optional, Tuple
13
13
  from xml.etree import ElementTree
14
14
 
15
15
  import adbutils
16
- import requests
17
16
  import uiautomator2 as u2
18
17
  from PIL import Image
19
18
 
@@ -21,7 +20,7 @@ from uiautodev.command_types import CurrentAppResponse
21
20
  from uiautodev.driver.base_driver import BaseDriver
22
21
  from uiautodev.driver.udt.udt import UDT, UDTError
23
22
  from uiautodev.exceptions import AndroidDriverException, RequestError
24
- from uiautodev.model import Node, ShellResponse, WindowSize
23
+ from uiautodev.model import Node, Rect, ShellResponse, WindowSize
25
24
  from uiautodev.utils.common import fetch_through_socket
26
25
 
27
26
  logger = logging.getLogger(__name__)
@@ -29,16 +28,16 @@ logger = logging.getLogger(__name__)
29
28
  class AndroidDriver(BaseDriver):
30
29
  def __init__(self, serial: str):
31
30
  super().__init__(serial)
32
- self.device = adbutils.device(serial)
31
+ self.adb_device = adbutils.device(serial)
33
32
  self._try_dump_list = [
34
33
  self._get_u2_hierarchy,
35
- self._get_appium_hierarchy,
36
34
  self._get_udt_dump_hierarchy,
35
+ # self._get_appium_hierarchy,
37
36
  ]
38
37
 
39
38
  @cached_property
40
39
  def udt(self) -> UDT:
41
- return UDT(self.device)
40
+ return UDT(self.adb_device)
42
41
 
43
42
  @cached_property
44
43
  def ud(self) -> u2.Device:
@@ -46,8 +45,7 @@ class AndroidDriver(BaseDriver):
46
45
 
47
46
  def screenshot(self, id: int) -> Image.Image:
48
47
  try:
49
- img = self.device.screenshot(display_id=id)
50
- return img.convert("RGB")
48
+ return self.adb_device.screenshot() # display_id is not OK now
51
49
  except adbutils.AdbError as e:
52
50
  logger.warning("screenshot error: %s", str(e))
53
51
  if id > 0:
@@ -56,7 +54,7 @@ class AndroidDriver(BaseDriver):
56
54
 
57
55
  def shell(self, command: str) -> ShellResponse:
58
56
  try:
59
- ret = self.device.shell2(command, rstrip=True, timeout=20)
57
+ ret = self.adb_device.shell2(command, rstrip=True, timeout=20)
60
58
  if ret.returncode == 0:
61
59
  return ShellResponse(output=ret.output, error=None)
62
60
  else:
@@ -66,16 +64,16 @@ class AndroidDriver(BaseDriver):
66
64
  except Exception as e:
67
65
  return ShellResponse(output="", error=f"adb error: {str(e)}")
68
66
 
69
- def dump_hierarchy(self) -> Tuple[str, Node]:
67
+ def dump_hierarchy(self, display_id: Optional[int] = 0) -> Tuple[str, Node]:
70
68
  """returns xml string and hierarchy object"""
71
- wsize = self.device.window_size()
72
- logger.debug("window size: %s", wsize)
73
69
  start = time.time()
74
70
  xml_data = self._dump_hierarchy_raw()
75
71
  logger.debug("dump_hierarchy cost: %s", time.time() - start)
76
72
 
73
+ wsize = self.adb_device.window_size()
74
+ logger.debug("window size: %s", wsize)
77
75
  return xml_data, parse_xml(
78
- xml_data, WindowSize(width=wsize[0], height=wsize[1])
76
+ xml_data, WindowSize(width=wsize[0], height=wsize[1]), display_id
79
77
  )
80
78
 
81
79
  def _dump_hierarchy_raw(self) -> str:
@@ -101,31 +99,9 @@ class AndroidDriver(BaseDriver):
101
99
  def _get_u2_hierarchy(self) -> str:
102
100
  d = u2.connect_usb(self.serial)
103
101
  return d.dump_hierarchy()
104
- # c = self.device.create_connection(adbutils.Network.TCP, 9008)
105
- # try:
106
- # compressed = False
107
- # payload = {
108
- # "jsonrpc": "2.0",
109
- # "method": "dumpWindowHierarchy",
110
- # "params": [compressed],
111
- # "id": 1,
112
- # }
113
- # content = fetch_through_socket(
114
- # c, "/jsonrpc/0", method="POST", json=payload, timeout=5
115
- # )
116
- # json_resp = json.loads(content)
117
- # if "error" in json_resp:
118
- # raise AndroidDriverException(json_resp["error"])
119
- # return json_resp["result"]
120
- # except adbutils.AdbError as e:
121
- # raise AndroidDriverException(
122
- # f"Failed to get hierarchy from u2 server: {str(e)}"
123
- # )
124
- # finally:
125
- # c.close()
126
102
 
127
103
  def _get_appium_hierarchy(self) -> str:
128
- c = self.device.create_connection(adbutils.Network.TCP, 6790)
104
+ c = self.adb_device.create_connection(adbutils.Network.TCP, 6790)
129
105
  try:
130
106
  content = fetch_through_socket(c, "/wd/hub/session/0/source", timeout=10)
131
107
  return json.loads(content)["value"]
@@ -140,56 +116,64 @@ class AndroidDriver(BaseDriver):
140
116
  return self.udt.dump_hierarchy()
141
117
 
142
118
  def tap(self, x: int, y: int):
143
- self.device.click(x, y)
119
+ self.adb_device.click(x, y)
144
120
 
145
121
  def window_size(self) -> Tuple[int, int]:
146
- w, h = self.device.window_size()
122
+ w, h = self.adb_device.window_size()
147
123
  return (w, h)
148
124
 
149
125
  def app_install(self, app_path: str):
150
- self.device.install(app_path)
126
+ self.adb_device.install(app_path)
151
127
 
152
128
  def app_current(self) -> CurrentAppResponse:
153
- info = self.device.app_current()
129
+ info = self.adb_device.app_current()
154
130
  return CurrentAppResponse(
155
131
  package=info.package, activity=info.activity, pid=info.pid
156
132
  )
157
133
 
158
134
  def app_launch(self, package: str):
159
- if self.device.package_info(package) is None:
135
+ if self.adb_device.package_info(package) is None:
160
136
  raise AndroidDriverException(f"App not installed: {package}")
161
- self.device.app_start(package)
137
+ self.adb_device.app_start(package)
162
138
 
163
139
  def app_terminate(self, package: str):
164
- self.device.app_stop(package)
140
+ self.adb_device.app_stop(package)
165
141
 
166
142
  def home(self):
167
- self.device.keyevent("HOME")
143
+ self.adb_device.keyevent("HOME")
168
144
 
169
145
  def wake_up(self):
170
- self.device.keyevent("WAKEUP")
146
+ self.adb_device.keyevent("WAKEUP")
171
147
 
172
148
 
173
- def parse_xml(xml_data: str, wsize: WindowSize) -> Node:
149
+ def parse_xml(xml_data: str, wsize: WindowSize, display_id: Optional[int] = None) -> Node:
174
150
  root = ElementTree.fromstring(xml_data)
175
- return parse_xml_element(root, wsize)
151
+ node = parse_xml_element(root, wsize, display_id)
152
+ if node is None:
153
+ raise AndroidDriverException("Failed to parse xml")
154
+ return node
176
155
 
177
156
 
178
- def parse_xml_element(
179
- element, wsize: WindowSize, indexes: List[int] = [0]
180
- ) -> Node:
157
+ def parse_xml_element(element, wsize: WindowSize, display_id: Optional[int], indexes: List[int] = [0]) -> Optional[Node]:
181
158
  """
182
159
  Recursively parse an XML element into a dictionary format.
183
160
  """
184
161
  name = element.tag
185
162
  if name == "node":
186
163
  name = element.attrib.get("class", "node")
164
+ if display_id is not None:
165
+ elem_display_id = int(element.attrib.get("display-id", display_id))
166
+ if elem_display_id != display_id:
167
+ return
168
+
187
169
  bounds = None
170
+ rect = None
188
171
  # eg: bounds="[883,2222][1008,2265]"
189
172
  if "bounds" in element.attrib:
190
173
  bounds = element.attrib["bounds"]
191
174
  bounds = list(map(int, re.findall(r"\d+", bounds)))
192
175
  assert len(bounds) == 4
176
+ rect = Rect(x=bounds[0], y=bounds[1], width=bounds[2] - bounds[0], height=bounds[3] - bounds[1])
193
177
  bounds = (
194
178
  bounds[0] / wsize.width,
195
179
  bounds[1] / wsize.height,
@@ -197,16 +181,20 @@ def parse_xml_element(
197
181
  bounds[3] / wsize.height,
198
182
  )
199
183
  bounds = map(partial(round, ndigits=4), bounds)
184
+
200
185
  elem = Node(
201
186
  key="-".join(map(str, indexes)),
202
187
  name=name,
203
188
  bounds=bounds,
189
+ rect=rect,
204
190
  properties={key: element.attrib[key] for key in element.attrib},
205
191
  children=[],
206
192
  )
207
193
 
208
194
  # Construct xpath for children
209
195
  for index, child in enumerate(element):
210
- elem.children.append(parse_xml_element(child, wsize, indexes + [index]))
196
+ child_node = parse_xml_element(child, wsize, display_id, indexes + [index])
197
+ if child_node:
198
+ elem.children.append(child_node)
211
199
 
212
200
  return elem
@@ -24,10 +24,18 @@ class ShellResponse(BaseModel):
24
24
  error: Optional[str] = ""
25
25
 
26
26
 
27
+ class Rect(BaseModel):
28
+ x: int
29
+ y: int
30
+ width: int
31
+ height: int
32
+
33
+
27
34
  class Node(BaseModel):
28
35
  key: str
29
36
  name: str
30
37
  bounds: Optional[Tuple[float, float, float, float]] = None
38
+ rect: Optional[Rect] = None
31
39
  properties: Dict[str, Union[str, bool]] = []
32
40
  children: List[Node] = []
33
41
 
@@ -16,7 +16,6 @@ from uiautodev.command_types import Command, CurrentAppResponse, InstallAppReque
16
16
  from uiautodev.model import DeviceInfo, Node, ShellResponse
17
17
  from uiautodev.provider import BaseProvider
18
18
 
19
-
20
19
  logger = logging.getLogger(__name__)
21
20
 
22
21
  class AndroidShellPayload(BaseModel):
@@ -58,7 +57,7 @@ def make_router(provider: BaseProvider) -> APIRouter:
58
57
  """Take a screenshot of device"""
59
58
  try:
60
59
  driver = provider.get_device_driver(serial)
61
- pil_img = driver.screenshot(id)
60
+ pil_img = driver.screenshot(id).convert("RGB")
62
61
  buf = io.BytesIO()
63
62
  pil_img.save(buf, format="JPEG")
64
63
  image_bytes = buf.getvalue()
@@ -68,12 +67,17 @@ def make_router(provider: BaseProvider) -> APIRouter:
68
67
  return Response(content=str(e), media_type="text/plain", status_code=500)
69
68
 
70
69
  @router.get("/{serial}/hierarchy")
71
- def dump_hierarchy(serial: str) -> Node:
70
+ def dump_hierarchy(serial: str, format: str = "json") -> Node:
72
71
  """Dump the view hierarchy of an Android device"""
73
72
  try:
74
73
  driver = provider.get_device_driver(serial)
75
74
  xml_data, hierarchy = driver.dump_hierarchy()
76
- return hierarchy
75
+ if format == "xml":
76
+ return Response(content=xml_data, media_type="text/xml")
77
+ elif format == "json":
78
+ return hierarchy
79
+ else:
80
+ return Response(content=f"Invalid format: {format}", media_type="text/plain", status_code=400)
77
81
  except Exception as e:
78
82
  logger.exception("dump_hierarchy failed")
79
83
  return Response(content=str(e), media_type="text/plain", status_code=500)
File without changes
File without changes
File without changes
File without changes
File without changes