uiautodev 0.11.0__py3-none-any.whl → 0.11.1__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.

Potentially problematic release.


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

uiautodev/__init__.py CHANGED
@@ -5,4 +5,4 @@
5
5
  """
6
6
 
7
7
  # version is auto managed by poetry
8
- __version__ = "0.11.0"
8
+ __version__ = "0.11.1"
@@ -10,7 +10,7 @@ import tempfile
10
10
  import time
11
11
  import uuid
12
12
  from pathlib import Path
13
- from typing import List, Optional, Tuple, Union, final
13
+ from typing import List, Optional, Tuple, Union, final, Dict
14
14
 
15
15
  from PIL import Image
16
16
 
@@ -31,6 +31,7 @@ def run_command(command: str, timeout: int = 60) -> str:
31
31
  capture_output=True,
32
32
  timeout=timeout,
33
33
  text=True,
34
+ errors='ignore',
34
35
  input='' # this avoid stdout: "FreeChannelContinue handle->data is nullptr"
35
36
  )
36
37
  # the hdc shell stderr is (不仅没啥用,还没办法去掉)
@@ -50,10 +51,10 @@ class HDC:
50
51
  def __init__(self):
51
52
  self.hdc = 'hdc'
52
53
  self.tmpdir = tempfile.TemporaryDirectory()
53
-
54
+
54
55
  def __del__(self):
55
56
  self.tmpdir.cleanup()
56
-
57
+
57
58
  def list_device(self) -> List[str]:
58
59
  command = f"{self.hdc} list targets"
59
60
  result = run_command(command)
@@ -65,15 +66,48 @@ class HDC:
65
66
  return devices
66
67
  else:
67
68
  return []
68
-
69
+
69
70
  def shell(self, serial: str, command: str) -> str:
70
71
  command = f"{self.hdc} -t {serial} shell \"{command}\""
71
72
  result = run_command(command)
72
73
  return result.strip()
73
-
74
+
75
+ def __split_text(self, text: str) -> str:
76
+ return text.split("\n")[0].strip() if text else ""
77
+
74
78
  def get_model(self, serial: str) -> str:
75
79
  return self.shell(serial, "param get const.product.model")
76
-
80
+
81
+ def get_name(self, serial: str) -> str:
82
+ data = self.shell(serial, "param get const.product.name")
83
+ return self.__split_text(data)
84
+
85
+ def wlan_ip(self, serial: str) -> str:
86
+ data = self.shell(serial, "ifconfig")
87
+ if not data or "not found" in data.lower() or "error" in data.lower():
88
+ logger.warning(f"ifconfig command failed or returned error for serial {serial}: {data!r}")
89
+ return ""
90
+ # Try multiple patterns for IP address
91
+ matches = re.findall(r'inet addr:(?!127)(\d+\.\d+\.\d+\.\d+)', data)
92
+ if not matches:
93
+ matches = re.findall(r'inet (?!127)(\d+\.\d+\.\d+\.\d+)', data)
94
+ if matches:
95
+ return matches[0]
96
+ logger.warning(f"No valid IP address found in ifconfig output for serial {serial}: {data!r}")
97
+ return ""
98
+
99
+ def sdk_version(self, serial: str) -> str:
100
+ data = self.shell(serial, "param get const.ohos.apiversion")
101
+ return self.__split_text(data)
102
+
103
+ def sys_version(self, serial: str) -> str:
104
+ data = self.shell(serial, "param get const.product.software.version")
105
+ return self.__split_text(data)
106
+
107
+ def brand(self, serial: str) -> str:
108
+ data = self.shell(serial, "param get const.product.brand")
109
+ return self.__split_text(data)
110
+
77
111
  def pull(self, serial: str, remote: StrOrPath, local: StrOrPath):
78
112
  if isinstance(remote, Path):
79
113
  remote = remote.as_posix()
@@ -81,13 +115,13 @@ class HDC:
81
115
  output = run_command(command)
82
116
  if not os.path.exists(local):
83
117
  raise HDCError(f"device file: {remote} not found", output)
84
-
118
+
85
119
  def push(self, serial: str, local: StrOrPath, remote: StrOrPath) -> str:
86
120
  if isinstance(remote, Path):
87
121
  remote = remote.as_posix()
88
122
  command = f"{self.hdc} -t {serial} file send {local} {remote}"
89
123
  return run_command(command)
90
-
124
+
91
125
  def screenshot(self, serial: str) -> Image.Image:
92
126
  device_path = f'/data/local/tmp/screenshot-{int(time.time()*1000)}.png'
93
127
  self.shell(serial, f"uitest screenCap -p {device_path}")
@@ -116,7 +150,7 @@ class HDC:
116
150
  raise HDCError(f"failed to dump layout: {output}")
117
151
  finally:
118
152
  self.shell(serial, f"rm {remote_path}")
119
-
153
+
120
154
 
121
155
  class HarmonyDriver(BaseDriver):
122
156
  def __init__(self, hdc: HDC, serial: str):
@@ -164,6 +198,112 @@ class HarmonyDriver(BaseDriver):
164
198
  else:
165
199
  return None
166
200
 
201
+ def get_app_info(self, package_name: str) -> Dict:
202
+ """
203
+ Get detailed information about a specific application.
204
+
205
+ Args:
206
+ package_name (str): The package name of the application to retrieve information for.
207
+
208
+ Returns:
209
+ Dict: A dictionary containing the application information. If an error occurs during parsing,
210
+ an empty dictionary is returned.
211
+ """
212
+ app_info = {}
213
+ data = self.hdc.shell(self.serial, f"bm dump -n {package_name}")
214
+ output = data
215
+ try:
216
+ json_start = output.find("{")
217
+ json_end = output.rfind("}") + 1
218
+ json_output = output[json_start:json_end]
219
+
220
+ app_info = json.loads(json_output)
221
+ except Exception as e:
222
+ logger.error(f"An error occurred: {e}")
223
+ return app_info
224
+
225
+ def get_app_abilities(self, package_name: str) -> List[Dict]:
226
+ """
227
+ Get the abilities of an application.
228
+
229
+ Args:
230
+ package_name (str): The package name of the application.
231
+
232
+ Returns:
233
+ List[Dict]: A list of dictionaries containing the abilities of the application.
234
+ """
235
+ result = []
236
+ app_info = self.get_app_info(package_name)
237
+ hap_module_infos = app_info.get("hapModuleInfos")
238
+ main_entry = app_info.get("mainEntry")
239
+ for hap_module_info in hap_module_infos:
240
+ # 尝试读取moduleInfo
241
+ try:
242
+ ability_infos = hap_module_info.get("abilityInfos")
243
+ module_main = hap_module_info["mainAbility"]
244
+ except Exception as e:
245
+ logger.warning(f"Fail to parse moduleInfo item, {repr(e)}")
246
+ continue
247
+ # 尝试读取abilityInfo
248
+ for ability_info in ability_infos:
249
+ try:
250
+ is_launcher_ability = False
251
+ skills = ability_info['skills']
252
+ if len(skills) > 0 and "action.system.home" in skills[0]["actions"]:
253
+ is_launcher_ability = True
254
+ icon_ability_info = {
255
+ "name": ability_info["name"],
256
+ "moduleName": ability_info["moduleName"],
257
+ "moduleMainAbility": module_main,
258
+ "mainModule": main_entry,
259
+ "isLauncherAbility": is_launcher_ability
260
+ }
261
+ result.append(icon_ability_info)
262
+ except Exception as e:
263
+ logger.warning(f"Fail to parse ability_info item, {repr(e)}")
264
+ continue
265
+ logger.debug(f"all abilities: {result}")
266
+ return result
267
+
268
+ def get_app_main_ability(self, package_name: str) -> Dict:
269
+ """
270
+ Get the main ability of an application.
271
+
272
+ Args:
273
+ package_name (str): The package name of the application to retrieve information for.
274
+
275
+ Returns:
276
+ Dict: A dictionary containing the main ability of the application.
277
+
278
+ """
279
+ if not (abilities := self.get_app_abilities(package_name)):
280
+ return {}
281
+ for item in abilities:
282
+ score = 0
283
+ if (name := item["name"]) and name == item["moduleMainAbility"]:
284
+ score += 1
285
+ if (module_name := item["moduleName"]) and module_name == item["mainModule"]:
286
+ score += 1
287
+ item["score"] = score
288
+ abilities.sort(key=lambda x: (not x["isLauncherAbility"], -x["score"]))
289
+ logger.debug(f"main ability: {abilities[0]}")
290
+ return abilities[0]
291
+
292
+ def app_launch(self, package: str, page_name: Optional[str] = None):
293
+ """
294
+ Start an application on the device.
295
+ If the `page_name` is empty, it will retrieve the main ability using `get_app_main_ability`.
296
+ Args:
297
+ package (str): The package name of the application.
298
+ page_name (Optional[str]): Ability Name within the application to start. If not provided, the main ability will be used.
299
+ """
300
+ if not page_name:
301
+ page_name = self.get_app_main_ability(package).get('name', 'MainAbility')
302
+ self.shell(f"aa start -a {page_name} -b {package}")
303
+
304
+ def app_terminate(self, package: str):
305
+ self.shell(f"aa force-stop {package}")
306
+
167
307
  def shell(self, command: str) -> ShellResponse:
168
308
  result = self.hdc.shell(self.serial, command)
169
309
  return ShellResponse(output=result)
uiautodev/model.py CHANGED
@@ -14,6 +14,7 @@ from pydantic import BaseModel
14
14
  class DeviceInfo(BaseModel):
15
15
  serial: str
16
16
  model: str = ""
17
+ product: str = ""
17
18
  name: str = ""
18
19
  status: str = ""
19
20
  enabled: bool = True
uiautodev/provider.py CHANGED
@@ -46,12 +46,18 @@ class AndroidProvider(BaseProvider):
46
46
  def list_devices(self) -> list[DeviceInfo]:
47
47
  adb = adbutils.AdbClient()
48
48
  ret: list[DeviceInfo] = []
49
- for d in adb.list():
49
+ for d in adb.list(extended=True):
50
50
  if d.state != "device":
51
51
  ret.append(DeviceInfo(serial=d.serial, status=d.state, enabled=False))
52
52
  else:
53
- dev = adb.device(d.serial)
54
- ret.append(DeviceInfo(serial=d.serial, model=dev.prop.model, name=dev.prop.name))
53
+ ret.append(DeviceInfo(
54
+ serial=d.serial,
55
+ status=d.state,
56
+ name=d.tags.get('device', ''),
57
+ model=d.tags.get('model', ''),
58
+ product=d.tags.get('product', ''),
59
+ enabled=True
60
+ ))
55
61
  return ret
56
62
 
57
63
  @lru_cache
@@ -76,7 +82,7 @@ class HarmonyProvider(BaseProvider):
76
82
 
77
83
  def list_devices(self) -> list[DeviceInfo]:
78
84
  devices = self.hdc.list_device()
79
- return [DeviceInfo(serial=d, model=self.hdc.get_model(d), name=self.hdc.get_model(d)) for d in devices]
85
+ return [DeviceInfo(serial=d, model=self.hdc.get_model(d), name=self.hdc.get_name(d)) for d in devices]
80
86
 
81
87
  @lru_cache
82
88
  def get_device_driver(self, serial: str) -> HarmonyDriver:
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: uiautodev
3
- Version: 0.11.0
3
+ Version: 0.11.1
4
4
  Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Author: codeskyblue
7
8
  Author-email: codeskyblue@gmail.com
8
9
  Requires-Python: >=3.8,<4.0
@@ -14,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.10
14
15
  Classifier: Programming Language :: Python :: 3.11
15
16
  Classifier: Programming Language :: Python :: 3.12
16
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
17
19
  Requires-Dist: Pillow
18
20
  Requires-Dist: adbutils (>=2.8.10,<3)
19
21
  Requires-Dist: click (>=8.1.7,<9.0.0)
@@ -1,4 +1,4 @@
1
- uiautodev/__init__.py,sha256=hgy1K91dPHmTwpLsj45Kim-m9Ghsd0tRvdB3H-FPdDo,165
1
+ uiautodev/__init__.py,sha256=Ya4pFQpAHk2du5GJjKUOuuJd0dK2UYSmEaajmyAkLDU,165
2
2
  uiautodev/__main__.py,sha256=0WZHyHW-M7FG5RexANNoIB5pkCX8xwQbTnmaOA9Y1kg,176
3
3
  uiautodev/app.py,sha256=K3u6WCobH3Q5HmXvrZff0Cq44L0j1J4eBVhSxnSNKiE,6600
4
4
  uiautodev/appium_proxy.py,sha256=yMzPnIDo50hYSaq0g5bXUpgRrFa_849wNa2o7ZpxGNY,1773
@@ -11,15 +11,15 @@ uiautodev/common.py,sha256=1A0kXfxVrp_i5mc_aRjuqSDWFFZ7DwZR9qpRLu2GMMg,1488
11
11
  uiautodev/driver/android.py,sha256=k4Mjq2gmsokwCUN68VGCuhbl7HHEEyeowuTzQFXjg30,8181
12
12
  uiautodev/driver/appium.py,sha256=U3TGpOXmu3tEa3E1ttTFoXehOfFyjavJQ3XA4CtqeBE,5308
13
13
  uiautodev/driver/base_driver.py,sha256=6MBNfEg_CtS4ed90SaSGbdqmf-JNaypqiBNaZBRKeAo,3027
14
- uiautodev/driver/harmony.py,sha256=93pwlg04wazey8MQM6DEvcBkr52REYVw4bwz321fK38,8031
14
+ uiautodev/driver/harmony.py,sha256=L3Ggd1PNlv6cPDbaaf8BA7OqtUp-G5g4Tk9cvhpnakw,13777
15
15
  uiautodev/driver/ios.py,sha256=EOi9k-Y4qQS6Htdz_ycBf5jYCnOkruB-mR0Sc8J-coo,4204
16
16
  uiautodev/driver/mock.py,sha256=0VtxBkZRMbHiQGHjqm8Ih2Ld6baXiVxX8yUTwWJQlnE,2422
17
17
  uiautodev/driver/testdata/layout.json,sha256=0z9jGJteXuGwkOhO_iINYPoDp1kCq-EaQds_iZkmiPQ,276538
18
18
  uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk,sha256=cKUVKpqEiGRXODeqpwzVWllyjdSLyowFm94a6jDTvhI,3675062
19
19
  uiautodev/driver/udt/udt.py,sha256=p6opbUtYxEGTINIX83F6m2CtzB42iSSBYRv1SjXCEFg,8351
20
20
  uiautodev/exceptions.py,sha256=lQI14vshN5qExVE23g2aw5i2fkdQFfLCJj2dO6kVLZw,591
21
- uiautodev/model.py,sha256=lCe1bAoY6-mx67dLYVR-xR-trGT81sCw93Aih0Yfi-M,1049
22
- uiautodev/provider.py,sha256=EnBI8PD2eoBjqugGS5cmy8GZ1Z7EaEF6YEQSfpOMN6s,2862
21
+ uiautodev/model.py,sha256=hrqezVn1XFXHyajFxxmoiuGDZoYAodfitWMBzrYgziA,1071
22
+ uiautodev/provider.py,sha256=f3fh9IbgTRlJmxZXXTNNZbHgHBIqfQCIBKIPz_BOOng,3054
23
23
  uiautodev/remote/android_input.py,sha256=r9y2SxnDw0GhN44emL-2Nz0UasaVaVtzh53hd-LJ710,2445
24
24
  uiautodev/remote/harmony_mjpeg.py,sha256=-J2sVfPSTuqN80BelisvwaZAimWxIk9SIzD010q3h7c,8287
25
25
  uiautodev/remote/keycode.py,sha256=RHSJVfcNY2pelQd7_tcE6T0j3n8CKBkiku7A2AJZUpk,8097
@@ -34,8 +34,8 @@ uiautodev/utils/common.py,sha256=L1qBBBS6jRgkXlGy5o6Xafo49auLXKRWyX9x8U_IKjc,482
34
34
  uiautodev/utils/envutils.py,sha256=Clyt2Hz9PXpK_fT0yWbMmixXyGvCaJO3LAgamM7aUVc,197
35
35
  uiautodev/utils/exceptions.py,sha256=lL_G_E41KWvfXnl32-E4Vgr3_HyTboxq_EwzdQMuvK4,637
36
36
  uiautodev/utils/usbmux.py,sha256=LYupLDn7U4KFKhYQJrmIroS-3040gqZQVDRDB_FNDJM,17386
37
- uiautodev-0.11.0.dist-info/LICENSE,sha256=RyeW676gBYO7AVVP2zQgfEx5rPSt46vR47xXZe7TlX4,1068
38
- uiautodev-0.11.0.dist-info/METADATA,sha256=r_WqmWQoTPFhK64k6wK3wxptY3MbP-G6Ec09tiJ1WyY,2555
39
- uiautodev-0.11.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
40
- uiautodev-0.11.0.dist-info/entry_points.txt,sha256=zBY8GgseYAAzPFA5Cf4rCCS9ivdyWsNxMVVYIaGAHJU,88
41
- uiautodev-0.11.0.dist-info/RECORD,,
37
+ uiautodev-0.11.1.dist-info/METADATA,sha256=omxcGuHP5rjLAtT0JbnhdkCq58majJ3m726u4p25t9U,2628
38
+ uiautodev-0.11.1.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
39
+ uiautodev-0.11.1.dist-info/entry_points.txt,sha256=zBY8GgseYAAzPFA5Cf4rCCS9ivdyWsNxMVVYIaGAHJU,88
40
+ uiautodev-0.11.1.dist-info/licenses/LICENSE,sha256=RyeW676gBYO7AVVP2zQgfEx5rPSt46vR47xXZe7TlX4,1068
41
+ uiautodev-0.11.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any