From 021c391182765a9ae8371c1862d8646b66af518e Mon Sep 17 00:00:00 2001 From: codeskyblue Date: Wed, 20 Mar 2024 15:32:03 +0800 Subject: [PATCH] Poetry (#945) limit python version >= 3.8 use poetry instead of pbr, setup.py remove some useless deps add github.actions.test matrix, include mac, linux, windows (3.8-3.11) remove extra option [image] for pip install uiautomator[image] --- .coveragerc | 26 +++++++++ .github/workflows/main.yml | 55 ++++++++++++++++++ .github/workflows/pythonapp.yml | 38 ------------ .github/workflows/release.yml | 33 +++++++++++ .gitignore | 1 + DEVELOP.md | 22 ++----- MANIFEST.in | 10 ---- Makefile | 20 +++++++ README.md | 18 +++--- docs/conf.py | 1 - examples/apk_install.py | 1 + examples/batteryweb/main.py | 1 - .../com.codeskyblue.remotecamera/main_test.py | 1 - examples/com.netease.cloudmusic/xpath_test.py | 1 + examples/minitouch.py | 4 +- examples/multi-thread-example.py | 6 +- examples/runyaml/run.py | 8 +-- poetry.toml | 3 + pyproject.toml | 42 +++++++++++++ requirements.txt | 19 ------ setup.cfg | 36 ------------ setup.py | 8 --- tests/{ => real_device}/conftest.py | 6 +- tests/{ => real_device}/runtest.sh | 0 tests/{ => real_device}/skip_test_image.py | 0 tests/{ => real_device}/test_push_pull.py | 3 +- tests/{ => real_device}/test_screenrecord.py | 8 ++- tests/{ => real_device}/test_session.py | 5 -- tests/{ => real_device}/test_settings.py | 6 +- tests/{ => real_device}/test_simple.py | 5 +- tests/{ => real_device}/test_swipe.py | 2 +- tests/{ => real_device}/test_utils.py | 5 +- tests/{ => real_device}/test_watcher.py | 0 tests/{ => real_device}/test_xpath.py | 8 +-- tests/{ => real_device}/testdata/AE86.jpg | Bin tests/unittests/test_import.py | 30 ++++++++++ uiautomator2/__init__.py | 34 ++++------- uiautomator2/__main__.py | 2 +- uiautomator2/_selector.py | 5 +- uiautomator2/exceptions.py | 3 - uiautomator2/ext-archived/aircv/__init__.py | 6 +- uiautomator2/ext-archived/ocr/__init__.py | 5 +- uiautomator2/ext-archived/ocr/baiduOCR.py | 3 +- uiautomator2/ext/htmlreport/__init__.py | 4 +- .../ext/htmlreport/assets/simplehttpserver.py | 12 +--- uiautomator2/ext/info/__init__.py | 4 +- uiautomator2/ext/perf/__init__.py | 20 ++++--- uiautomator2/ext/xpath/__init__.py | 1 + uiautomator2/init.py | 3 +- uiautomator2/screenrecord.py | 2 +- uiautomator2/settings.py | 1 - uiautomator2/swipe.py | 1 + uiautomator2/utils.py | 15 +---- uiautomator2/version.py | 7 +-- uiautomator2/webview.py | 5 +- uiautomator2/xpath.py | 2 +- 56 files changed, 317 insertions(+), 250 deletions(-) create mode 100644 .coveragerc create mode 100644 .github/workflows/main.yml delete mode 100644 .github/workflows/pythonapp.yml create mode 100644 .github/workflows/release.yml delete mode 100644 MANIFEST.in create mode 100644 Makefile create mode 100644 poetry.toml create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py rename tests/{ => real_device}/conftest.py (95%) rename tests/{ => real_device}/runtest.sh (100%) rename tests/{ => real_device}/skip_test_image.py (100%) rename tests/{ => real_device}/test_push_pull.py (99%) rename tests/{ => real_device}/test_screenrecord.py (91%) rename tests/{ => real_device}/test_session.py (87%) rename tests/{ => real_device}/test_settings.py (99%) rename tests/{ => real_device}/test_simple.py (96%) rename tests/{ => real_device}/test_swipe.py (93%) rename tests/{ => real_device}/test_utils.py (99%) rename tests/{ => real_device}/test_watcher.py (100%) rename tests/{ => real_device}/test_xpath.py (91%) rename tests/{ => real_device}/testdata/AE86.jpg (100%) create mode 100644 tests/unittests/test_import.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..17b79984 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,26 @@ +[run] +branch = True + +[report] +; Regexes for lines to exclude from consideration +exclude_also = + ; Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + ; Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + ; Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + ; Don't complain about abstract methods, they aren't run: + @(abc\.)?abstractmethod + +ignore_errors = True + +omit = + "tests/*" + "docs/*" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..418370d2 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,55 @@ +name: Python application + +on: [push] + +jobs: + build-and-publish: + name: ${{ matrix.os }} / ${{ matrix.python-version }} + runs-on: ${{ matrix.image }} + strategy: + matrix: + os: [Ubuntu, macOS, Windows] + python-version: ["3.8", "3.11"] + include: + - os: Ubuntu + image: ubuntu-latest + - os: Windows + image: windows-2022 + - os: macOS + image: macos-12 + fail-fast: false + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Get full Python version + id: full-python-version + run: echo version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") >> $GITHUB_OUTPUT + + - name: Update PATH + if: ${{ matrix.os != 'Windows' }} + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Update Path for Windows + if: ${{ matrix.os == 'Windows' }} + run: echo "$APPDATA\Python\Scripts" >> $GITHUB_PATH + + - name: Enable long paths for git on Windows + if: ${{ matrix.os == 'Windows' }} + # Enable handling long path names (+260 char) on the Windows platform + # https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation + run: git config --system core.longpaths true + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + poetry install + + - name: Run tests with coverage + run: | + make cov diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml deleted file mode 100644 index f0e5a5b0..00000000 --- a/.github/workflows/pythonapp.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Python application - -on: [push] - -jobs: - build-and-publish: - name: Build and publish Python 🐍 distributions 📦 to PyPI - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - with: - fetch-depth: 10 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 - with: - python-version: 3.7 - - name: Install dependencies - run: | - ./uiautomator2/assets/sync.sh - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Test with pytest - run: | - pip install pytest - pip install -e ".[image]" - pytest tests/test_utils.py - - name: Install pypa/build - run: >- - python3 -m pip install wheel - - name: Build targz and wheel - run: >- - python3 setup.py sdist bdist_wheel - - name: Publish distribution 📦 to PyPI - if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master - with: - skip_existing: true - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ed86995a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release + +on: + push: + tags: + - *.*.* + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + + - name: Build + run: | + make build + + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c718bad7..df54b8cc 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,4 @@ docs/*.rst .DS_Store apk_version.txt +*.lock diff --git a/DEVELOP.md b/DEVELOP.md index 043f876b..e75ff18c 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -2,27 +2,17 @@ ``` git clone https://github.com/openatx/uiautomator2 -pip3 install -e uiautomator2 -``` - -`-e`这个选项可以将该目录以软连接的形式添加到Python `site-packages` - -## 生成CHANGELOG -See changelog from git history +cd uiautomator2 -``` -git log --graph --date-order -C -M --pretty=format:"<%h> %ad [%an] %Cgreen%d%Creset %s" --all --date=short +pip install poetry +poetry install ``` -## 使用Sphinx生成文档 -```bash -pip3 install -e . -cd docs -make publish -``` +项目使用poetry做包管理和打包发布功能 + ## ViewConfiguration -一些默认的配置,从 `/android/view/ViewConfiguration.java`中可以查到 +一些默认的配置,从 [/android/view/ViewConfiguration.java](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewConfiguration.java)中可以查到 > 单位: 毫秒 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 828a79d8..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,10 +0,0 @@ -# Format -# https://packaging.python.org/guides/using-manifest-in/ - -graft uiautomator2 - -prune tests/ -prune docs/ -prune examples/ -prune .github/ -global-exclude *.py[cod] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..258fe049 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +.PHONY: build + +format: + poetry run isort . -m HANGING_INDENT -l 120 + +test: + poetry run pytest -v tests + +cov: + poetry run pytest -v tests/unittests --cov=. --cov-report xml --cov-report term + +build: + rm -fr dist + poetry build + +init: + if [ ! -f "ApiDemos-debug.apk" ]; then \ + wget https://github.com/appium/appium/raw/master/packages/appium/sample-code/apps/ApiDemos-debug.apk; \ + fi + poetry run python -m adbutils -i ./ApiDemos-debug.apk diff --git a/README.md b/README.md index a0c91681..c266b608 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,7 @@ QQ交流群: 815453846 ## Requirements - Android版本 4.4+ -- Python 3.6+ (社区反馈3.8.0不支持, 但是3.8.2支持) - ->如果用python2的pip安装,会安装本库的老版本0.2.3;如果用python3.5的pip安装,会安装本库的老版本0.3.3;两者均已经不会再维护;PYPI上的最近版本是这个:https://pypi.org/project/uiautomator2/ +- Python 3.8+ ## QUICK START 先准备一台(不要两台)开启了`开发者选项`的安卓手机,连接上电脑,确保执行`adb devices`可以看到连接上的设备。 @@ -1468,8 +1466,8 @@ for elem in d.xpath("//android.widget.TextView").all(): 点击查看[其他XPath常见用法](XPATH.md) -### Screenrecord -视频录制 +### Screenrecord (Deprecated) +视频录制(废弃) 这里没有使用手机中自带的screenrecord命令,是通过获取手机图片合成视频的方法,所以需要安装一些其他的依赖,如imageio, imageio-ffmpeg, numpy等 因为有些依赖比较大,推荐使用镜像安装。直接运行下面的命令即可。 @@ -1491,7 +1489,7 @@ d.screenrecord.stop() # 停止录制后,output.mp4文件才能打开 录制的时候也可以指定fps(当前是20),这个值是率低于minicap输出图片的速度,感觉已经很好了,不建议你修改。 -### Image match +### Image match (3.x开始移除该功能) 图像匹配,在使用这个功能之前你需要先把依赖安装上 ```bash @@ -1564,6 +1562,12 @@ https://www.cnblogs.com/insist8089/p/6898181.html ## [CHANGELOG (generated by pbr)](CHANGELOG) 重大更新 +- 3.x + + 最低Python从3.5改为3.8 + 使用poetry代替pbr管理包依赖 + 移除图片匹配和视频录制功能 + - 1.0.0 移除 `d.watchers.watched` (会拖慢自动化的执行速度并且还会降低稳定性) @@ -1592,7 +1596,7 @@ https://www.cnblogs.com/insist8089/p/6898181.html Other [contributors](../../graphs/contributors) -## 其他优秀的项目 +## 其他优秀的项目 (好久没更新了,去谷歌吧) - [google/mobly](https://github.com/google/mobly) 谷歌内部的测试框架,虽然我不太懂,但是感觉很好用 - https://www.appetizer.io/ 包含一个很好用的IDE,快速编写脚本,也可以插桩采集性能。 - https://github.com/atinfo/awesome-test-automation 所有优秀测试框架的集合,包罗万象 diff --git a/docs/conf.py b/docs/conf.py index 86db64cd..8ca23a33 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,6 @@ import uiautomator2 - # -- Project information ----------------------------------------------------- master_doc = 'index' diff --git a/examples/apk_install.py b/examples/apk_install.py index 5f5c0732..104e7924 100644 --- a/examples/apk_install.py +++ b/examples/apk_install.py @@ -5,6 +5,7 @@ # OPPO need password import time + import uiautomator2 as u2 diff --git a/examples/batteryweb/main.py b/examples/batteryweb/main.py index 03c355e8..24353d67 100644 --- a/examples/batteryweb/main.py +++ b/examples/batteryweb/main.py @@ -4,7 +4,6 @@ import flask import requests - app = flask.Flask(__name__) diff --git a/examples/com.codeskyblue.remotecamera/main_test.py b/examples/com.codeskyblue.remotecamera/main_test.py index 4eac762b..f595220c 100644 --- a/examples/com.codeskyblue.remotecamera/main_test.py +++ b/examples/com.codeskyblue.remotecamera/main_test.py @@ -2,7 +2,6 @@ import uiautomator2 as u2 - pkg_name = 'com.codeskyblue.remotecamera' d = u2.connect() diff --git a/examples/com.netease.cloudmusic/xpath_test.py b/examples/com.netease.cloudmusic/xpath_test.py index 902685b0..1d2bfb88 100644 --- a/examples/com.netease.cloudmusic/xpath_test.py +++ b/examples/com.netease.cloudmusic/xpath_test.py @@ -2,6 +2,7 @@ # import unittest + from logzero import logger import uiautomator2 as u2 diff --git a/examples/minitouch.py b/examples/minitouch.py index 5d850ac7..2ae03e53 100644 --- a/examples/minitouch.py +++ b/examples/minitouch.py @@ -2,9 +2,11 @@ # # 半成品 import json -from . import Device + from websocket import create_connection +from . import Device + class Minitouch: # TODO: need test diff --git a/examples/multi-thread-example.py b/examples/multi-thread-example.py index a1a7271a..5d4599ec 100644 --- a/examples/multi-thread-example.py +++ b/examples/multi-thread-example.py @@ -4,11 +4,13 @@ # But is seems fine, because these operation have so many socket IO # So it seems no need to use multiprocess # -import uiautomator2 as u2 -import adbutils import threading + +import adbutils from logzero import logger +import uiautomator2 as u2 + def worker(d: u2.Device): d.app_start("io.appium.android.apis", stop=True) diff --git a/examples/runyaml/run.py b/examples/runyaml/run.py index 4f28046f..9eba868b 100755 --- a/examples/runyaml/run.py +++ b/examples/runyaml/run.py @@ -2,16 +2,16 @@ # coding: utf-8 # -import re +import argparse import os +import re import time -import argparse -import yaml import bunch -import uiautomator2 as u2 +import yaml from logzero import logger +import uiautomator2 as u2 CLICK = "click" # swipe diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 00000000..53b35d37 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +create = true +in-project = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..b42b872b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[tool.poetry] +name = "uiautomator2" +version = "0.0.0" +description = "automator for anroid device" +homepage = "https://github.com/openatx/uiautomator2" +authors = ["codeskyblue "] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.8" +requests = "*" +lxml = "*" +adbutils = ">=2.2" +retry = ">=0,<1" +packaging = ">=20.3" + +# TODO: remove later +Deprecated = "*" +logzero = "*" +filelock = ">=3,<4" +progress = "^1.6" +pillow = "*" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.1.1" +isort = "^5.13.2" +pytest-cov = "^4.1.0" + +[tool.poetry.scripts] +uiautomator2 = "uiautomator2.__main__:main" + +[tool.poetry-dynamic-versioning] # 根据tag来动态配置版本号 +enable = true +pattern = "^((?P\\d+)!)?(?P\\d+(\\.\\d+)*)" + +[tool.poetry-dynamic-versioning.substitution] +files = ["uiautomator2/version.py"] + +[build-system] +requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] +build-backend = "poetry_dynamic_versioning.backend" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5c0741ac..00000000 --- a/requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -six -requests>=2 -whichcraft - -logzero -progress~=1.3 -retry~=0.9 -adbutils>=1.2.3 -Deprecated~=1.2.6 - -# Not supported in QPython -Pillow -lxml>=4.3 -cached-property>=1.5.1,<2.0 -packaging>=20.3 - -#websocket-client - -filelock>=3.0.12,<4.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index cf1f5cb9..00000000 --- a/setup.cfg +++ /dev/null @@ -1,36 +0,0 @@ -[metadata] -name = uiautomator2 -author = codeskyblue -author_email = codeskyblue@gmail.com -summary = Python Wrapper for Google Android UiAutomator2 test tool -license = MIT -description_file = ABOUT.rst -home_page = https://github.com/openatx/uiautomator2 -# all classifier can be found in https://pypi.python.org/pypi?%3Aaction=list_classifiers -classifier = - Development Status :: 4 - Beta - Environment :: Console - Intended Audience :: Developers - Operating System :: POSIX - Programming Language :: Python :: 3 - Topic :: Software Development :: Libraries :: Python Modules - Topic :: Software Development :: Testing - -[files] -packages = - uiautomator2 - -# support: pip3 install uiautomator2[image] -[extras] -image = - scikit-image - imutils - findit - imageio>=2.8.0,<3.0 - imageio-ffmpeg>=0.4.2,<1.0 - websocket_client>=0.57.0,<1.0 - -[entry_points] -# https://docs.openstack.org/pbr/3.1.1/#entry-points -console_scripts = - uiautomator2 = uiautomator2.__main__:main diff --git a/setup.py b/setup.py deleted file mode 100644 index 7977a0e0..00000000 --- a/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 -# -# Licensed under MIT -# - -import setuptools -setuptools.setup(setup_requires=['pbr'], python_requires='>=3.6', pbr=True) diff --git a/tests/conftest.py b/tests/real_device/conftest.py similarity index 95% rename from tests/conftest.py rename to tests/real_device/conftest.py index 2ac22450..e16caba2 100644 --- a/tests/conftest.py +++ b/tests/real_device/conftest.py @@ -1,14 +1,14 @@ # coding: utf-8 import adbutils -import uiautomator2 as u2 import pytest +import uiautomator2 as u2 + @pytest.fixture(scope="module") def d(device): _d = device - #_d = u2.connect() _d.settings['operation_delay'] = (0.2, 0.2) _d.settings['operation_delay_methods'] = ['click', 'swipe'] return _d @@ -20,7 +20,7 @@ def package_name(): @pytest.fixture(scope="function") -def sess(d, package_name) -> u2.Device: +def sess(d, package_name) -> u2.Device: # type: ignore d.watcher.reset() d.app_start(package_name, stop=True) diff --git a/tests/runtest.sh b/tests/real_device/runtest.sh similarity index 100% rename from tests/runtest.sh rename to tests/real_device/runtest.sh diff --git a/tests/skip_test_image.py b/tests/real_device/skip_test_image.py similarity index 100% rename from tests/skip_test_image.py rename to tests/real_device/skip_test_image.py diff --git a/tests/test_push_pull.py b/tests/real_device/test_push_pull.py similarity index 99% rename from tests/test_push_pull.py rename to tests/real_device/test_push_pull.py index 7c355e7f..7fea051a 100644 --- a/tests/test_push_pull.py +++ b/tests/real_device/test_push_pull.py @@ -1,8 +1,9 @@ # coding: utf-8 # -import os import io +import os + import uiautomator2 as u2 diff --git a/tests/test_screenrecord.py b/tests/real_device/test_screenrecord.py similarity index 91% rename from tests/test_screenrecord.py rename to tests/real_device/test_screenrecord.py index 8f68ba29..7c9a2465 100644 --- a/tests/test_screenrecord.py +++ b/tests/real_device/test_screenrecord.py @@ -2,13 +2,15 @@ # import time -import uiautomator2 as u2 + import pytest -import imageio + +import uiautomator2 as u2 -# @pytest.mark.skip("Too long") +@pytest.mark.skip("deprecated") def test_screenrecord(d: u2.Device): + import imageio with pytest.raises(RuntimeError): d.screenrecord.stop() diff --git a/tests/test_session.py b/tests/real_device/test_session.py similarity index 87% rename from tests/test_session.py rename to tests/real_device/test_session.py index f839849c..68b00f43 100644 --- a/tests/test_session.py +++ b/tests/real_device/test_session.py @@ -1,20 +1,15 @@ # coding: utf-8 # -from collections import namedtuple - def test_session(sess): sess.wlan_ip - # sess.widget sess.watcher - sess.image sess.jsonrpc sess.open_identify sess.shell sess.set_new_command_timeout sess.settings - sess.taobao sess.xpath diff --git a/tests/test_settings.py b/tests/real_device/test_settings.py similarity index 99% rename from tests/test_settings.py rename to tests/real_device/test_settings.py index 09417e7c..4fd9385a 100644 --- a/tests/test_settings.py +++ b/tests/real_device/test_settings.py @@ -1,11 +1,13 @@ # coding: utf-8 # -import uiautomator2 as u2 -import pytest import logging import time +import pytest + +import uiautomator2 as u2 + def test_set_xpath_debug(sess): with pytest.raises(TypeError): diff --git a/tests/test_simple.py b/tests/real_device/test_simple.py similarity index 96% rename from tests/test_simple.py rename to tests/real_device/test_simple.py index a3e15e96..04f9b7fe 100644 --- a/tests/test_simple.py +++ b/tests/real_device/test_simple.py @@ -87,7 +87,7 @@ def test_get_text(sess): d = sess text = d(resourceId="android:id/list").child( className="android.widget.TextView", instance=2).get_text() - assert text == "App" + assert text == "Animation" def test_xpath(sess): @@ -96,7 +96,7 @@ def test_xpath(sess): assert len(d.xpath("//*[@text='Media']").all()) == 1 assert len(d.xpath("//*[@text='MediaNotExists']").all()) == 0 d.xpath("//*[@text='Media']").click() - assert d.xpath('//*[contains(@text, "Audio")]').wait(5) + assert d.xpath('//*[contains(@text, "VideoView")]').wait(5) @pytest.mark.skip("Need fix") @@ -141,6 +141,7 @@ def test_send_keys(sess): d.xpath("App").click() d.xpath("Search").click() d.xpath('//*[@text="Invoke Search"]').click() + d.xpath('@io.appium.android.apis:id/txt_query_prefill').click() d.send_keys("hello", clear=True) assert d.xpath('io.appium.android.apis:id/txt_query_prefill').info['text'] == 'hello' diff --git a/tests/test_swipe.py b/tests/real_device/test_swipe.py similarity index 93% rename from tests/test_swipe.py rename to tests/real_device/test_swipe.py index 74e5ac4c..bee1afff 100644 --- a/tests/test_swipe.py +++ b/tests/real_device/test_swipe.py @@ -2,13 +2,13 @@ # import time + import uiautomator2 as u2 def test_swipe_duration(d: u2.Device): w, h = d.window_size() start = time.time() - d.debug = True d.swipe(w//2, h//2, w-1, h//2, 2.0) duration = time.time() - start assert duration >= 1.5 # actually duration is about 7s in my TT diff --git a/tests/test_utils.py b/tests/real_device/test_utils.py similarity index 99% rename from tests/test_utils.py rename to tests/real_device/test_utils.py index 845da9ca..e6a5e60d 100644 --- a/tests/test_utils.py +++ b/tests/real_device/test_utils.py @@ -1,11 +1,14 @@ # coding: utf-8 # -import time import threading +import time + import pytest + from uiautomator2 import utils + def test_list2cmdline(): testdata = [ [("echo", "hello"), "echo hello"], diff --git a/tests/test_watcher.py b/tests/real_device/test_watcher.py similarity index 100% rename from tests/test_watcher.py rename to tests/real_device/test_watcher.py diff --git a/tests/test_xpath.py b/tests/real_device/test_xpath.py similarity index 91% rename from tests/test_xpath.py rename to tests/real_device/test_xpath.py index b8ac5e7d..7f5a4bf1 100644 --- a/tests/test_xpath.py +++ b/tests/real_device/test_xpath.py @@ -5,9 +5,10 @@ from functools import partial from pprint import pprint -import uiautomator2 as u2 import pytest +import uiautomator2 as u2 + def test_get_text(sess: u2.Session): assert sess.xpath("App").get_text() == "App" @@ -39,11 +40,6 @@ def test_element_all(sess: u2.Session): assert len(app.all()) == 1 assert app.exists - elements = sess.xpath('//*[@resource-id="android:id/list"]/android.widget.TextView').all() - assert len(elements) == 11 - el = elements[0] - assert el.text == 'Accessibility' - def test_watcher(sess: u2.Session, request): sess.xpath.when("App").click() diff --git a/tests/testdata/AE86.jpg b/tests/real_device/testdata/AE86.jpg similarity index 100% rename from tests/testdata/AE86.jpg rename to tests/real_device/testdata/AE86.jpg diff --git a/tests/unittests/test_import.py b/tests/unittests/test_import.py new file mode 100644 index 00000000..4a46d44d --- /dev/null +++ b/tests/unittests/test_import.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""Created on Wed Mar 20 2024 14:51:03 by codeskyblue +""" + +import uiautomator2 as u2 + + +def test_import(): + u2.Device + u2.connect + u2.connect_usb + u2.connect_wifi + u2.connect_adb_wifi + u2.Device.app_install + u2.Device.app_uninstall + u2.Device.app_current + u2.Device.app_list + u2.Device.shell + u2.Device.send_keys + u2.Device.click + u2.Device.swipe + u2.Device.dump_hierarchy + u2.Device.freeze_rotation + u2.Device.open_notification + u2.Device.info + u2.Device.xpath + u2.Device.clipboard + u2.Device.orientation \ No newline at end of file diff --git a/uiautomator2/__init__.py b/uiautomator2/__init__.py index cf6672dc..db975d49 100644 --- a/uiautomator2/__init__.py +++ b/uiautomator2/__init__.py @@ -26,13 +26,13 @@ import re import shutil import subprocess -import sys -import threading import time +import urllib.parse as urlparse import warnings import xml.dom.minidom -from collections import defaultdict, namedtuple +from collections import namedtuple from datetime import datetime +from functools import cached_property from pathlib import Path from typing import List, Optional, Tuple, Union @@ -40,13 +40,10 @@ import adbutils import filelock import logzero -from packaging import version as packaging_version import requests -import six -import six.moves.urllib.parse as urlparse -from cached_property import cached_property from deprecated import deprecated from logzero import setup_logger +from packaging import version as packaging_version from PIL import Image from retry import retry from urllib3.util.retry import Retry @@ -54,11 +51,9 @@ from . import xpath from ._proto import HTTP_TIMEOUT, SCROLL_STEPS, Direction from ._selector import Selector, UiObject -from .exceptions import (BaseError, ConnectError, GatewayError, JSONRPCError, - NullObjectExceptionError, NullPointerExceptionError, - RetryError, ServerError, SessionBrokenError, - StaleObjectExceptionError, - UiAutomationNotConnectedError, UiObjectNotFoundError) +from .exceptions import BaseError, ConnectError, GatewayError, JSONRPCError, NullObjectExceptionError, \ + NullPointerExceptionError, RetryError, ServerError, SessionBrokenError, StaleObjectExceptionError, \ + UiAutomationNotConnectedError, UiObjectNotFoundError from .init import Initer # from .session import Session # noqa: F401 from .settings import Settings @@ -67,9 +62,6 @@ from .version import __apk_version__, __atx_agent_version__ from .watcher import WatchContext, Watcher -if six.PY2: - FileNotFoundError = OSError - DEBUG = False WAIT_FOR_DEVICE_TIMEOUT = int(os.getenv("WAIT_FOR_DEVICE_TIMEOUT", 20)) @@ -534,9 +526,7 @@ def _jsonrpc_call(self, method, params=[], http_timeout=60): def is_exception(err, exception_name): return err.exception_name == exception_name or exception_name in err.message - if isinstance( - err.data, - six.string_types) and 'UiAutomation not connected' in err.data: + if isinstance(err.data, str) and 'UiAutomation not connected' in err.data: err.__class__ = UiAutomationNotConnectedError elif err.message: if is_exception(err, 'uiautomator.UiObjectNotFoundException'): @@ -734,7 +724,7 @@ def _is_apk_outdated(self): def _package_exists(self, package_name: str) -> bool: return self.shell(['pm', 'path', package_name]).exit_code == 0 - def _package_version(self, package_name: str) -> Optional[packaging.version.Version]: + def _package_version(self, package_name: str) -> Optional[packaging_version.Version]: dump_output = self.shell(['dumpsys', 'package', package_name]).output m = re.compile(r'versionName=(?P[\d.]+)').search(dump_output) if m is None: @@ -819,7 +809,7 @@ def push(self, src, dst: str, mode=0o644, show_progress=False): modestr = oct(mode).replace('o', '') pathname = '/upload/' + dst.lstrip('/') filesize = 0 - if isinstance(src, six.string_types): + if isinstance(src, str): if re.match(r"^https?://", src): r = requests.get(src, stream=True) if r.status_code != 200: @@ -1751,7 +1741,7 @@ def send_action(self, code): "done": 6, "previous": 7, } - if isinstance(code, six.string_types): + if isinstance(code, str): code = __alias.get(code, code) self.shell([ 'am', 'broadcast', '-a', 'ADB_EDITOR_CODE', '--ei', 'code', @@ -1952,7 +1942,7 @@ def connect_adb_wifi(addr) -> Device: Raises: ConnectError """ - assert isinstance(addr, six.string_types) + assert isinstance(addr, str) subprocess.call([adbutils.adb_path(), "connect", addr]) try: diff --git a/uiautomator2/__main__.py b/uiautomator2/__main__.py index ea6a22bb..31c171d1 100644 --- a/uiautomator2/__main__.py +++ b/uiautomator2/__main__.py @@ -10,12 +10,12 @@ import os import re +import adbutils import progress.bar import requests from logzero import logger from retry import retry -import adbutils import uiautomator2 as u2 from .init import Initer diff --git a/uiautomator2/_selector.py b/uiautomator2/_selector.py index cc82559d..dd23f912 100644 --- a/uiautomator2/_selector.py +++ b/uiautomator2/_selector.py @@ -3,13 +3,12 @@ import warnings import requests -import six from PIL import Image from retry import retry +from ._proto import SCROLL_STEPS from .exceptions import UiObjectNotFoundError from .utils import Exists, intersect -from ._proto import SCROLL_STEPS class Selector(dict): @@ -413,7 +412,7 @@ def __getitem__(self, instance: int): Raises: IndexError """ - if isinstance(self.selector, six.string_types): + if isinstance(self.selector, str): raise IndexError( "Index is not supported when UiObject returned by child_by_xxx" ) diff --git a/uiautomator2/exceptions.py b/uiautomator2/exceptions.py index 1a1207ff..951b777f 100644 --- a/uiautomator2/exceptions.py +++ b/uiautomator2/exceptions.py @@ -1,9 +1,6 @@ # coding: utf-8 # -# class ATXError(Exception): -# pass - import json diff --git a/uiautomator2/ext-archived/aircv/__init__.py b/uiautomator2/ext-archived/aircv/__init__.py index ff9836f4..11644676 100644 --- a/uiautomator2/ext-archived/aircv/__init__.py +++ b/uiautomator2/ext-archived/aircv/__init__.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import cv2 -import numpy as np +import threading import time +import cv2 +import numpy as np import requests import websocket -import threading __version__ = "0.0.1" diff --git a/uiautomator2/ext-archived/ocr/__init__.py b/uiautomator2/ext-archived/ocr/__init__.py index a4951406..95989e65 100644 --- a/uiautomator2/ext-archived/ocr/__init__.py +++ b/uiautomator2/ext-archived/ocr/__init__.py @@ -10,9 +10,10 @@ d.ext_ocr("对战模式").click() """ -import requests import time +import requests + API = "" @@ -95,8 +96,8 @@ def click(self, timeout=10): if __name__ == '__main__': - import uiautomator2.ext.ocr as ocr import uiautomator2 as u2 + import uiautomator2.ext.ocr as ocr d = u2.connect() print(ocr.OCR(d)("王者峡谷").click()) \ No newline at end of file diff --git a/uiautomator2/ext-archived/ocr/baiduOCR.py b/uiautomator2/ext-archived/ocr/baiduOCR.py index 6cba9f34..1d512667 100644 --- a/uiautomator2/ext-archived/ocr/baiduOCR.py +++ b/uiautomator2/ext-archived/ocr/baiduOCR.py @@ -6,9 +6,10 @@ @description: 使用百度OCR实现截屏选取元素 """ +from aip import AipOcr + from uiautomator2.ext.ocr import OCR as u2OCR from uiautomator2.ext.ocr import OCRSelector as u2OCRSelector -from aip import AipOcr class OCR(u2OCR): diff --git a/uiautomator2/ext/htmlreport/__init__.py b/uiautomator2/ext/htmlreport/__init__.py index 7d18e2c2..8bf7d7ee 100644 --- a/uiautomator2/ext/htmlreport/__init__.py +++ b/uiautomator2/ext/htmlreport/__init__.py @@ -11,9 +11,11 @@ import sys import time import types -import uiautomator2 + from PIL import ImageDraw +import uiautomator2 + def mark_point(im, x, y): """ diff --git a/uiautomator2/ext/htmlreport/assets/simplehttpserver.py b/uiautomator2/ext/htmlreport/assets/simplehttpserver.py index 7df15dcb..54471d92 100644 --- a/uiautomator2/ext/htmlreport/assets/simplehttpserver.py +++ b/uiautomator2/ext/htmlreport/assets/simplehttpserver.py @@ -1,17 +1,11 @@ #!/usr/bin/env python # coding: utf-8 -import six +import http.server as SimpleHTTPServer import socket -from contextlib import closing +import socketserver as SocketServer import webbrowser - -if six.PY2: - import SimpleHTTPServer - import SocketServer -else: - import http.server as SimpleHTTPServer - import socketserver as SocketServer +from contextlib import closing def is_port_avaiable(port): diff --git a/uiautomator2/ext/info/__init__.py b/uiautomator2/ext/info/__init__.py index 3b0a0216..e7ac3726 100644 --- a/uiautomator2/ext/info/__init__.py +++ b/uiautomator2/ext/info/__init__.py @@ -1,7 +1,7 @@ +import atexit +import datetime import json import os -import datetime -import atexit from uiautomator2 import UIAutomatorServer from uiautomator2.ext.info import conf diff --git a/uiautomator2/ext/perf/__init__.py b/uiautomator2/ext/perf/__init__.py index 5ffec2ec..d3482db0 100644 --- a/uiautomator2/ext/perf/__init__.py +++ b/uiautomator2/ext/perf/__init__.py @@ -3,14 +3,14 @@ from __future__ import absolute_import, print_function -import threading -import re -import time -import datetime +import atexit import csv -import sys +import datetime import os -import atexit +import re +import sys +import threading +import time from collections import namedtuple _MEM_PATTERN = re.compile(r'TOTAL[:\s]+(\d+)') @@ -291,11 +291,13 @@ def csv2images(self, src=None, target_dir='.'): src: csv file, default to perf record csv path target_dir: images store dir """ - import pandas as pd - import matplotlib.pyplot as plt - import matplotlib.ticker as ticker import datetime import os + + import matplotlib.pyplot as plt + import matplotlib.ticker as ticker + import pandas as pd + from uiautomator2.utils import natualsize src = src or self.csv_output diff --git a/uiautomator2/ext/xpath/__init__.py b/uiautomator2/ext/xpath/__init__.py index de586ff7..e412d77f 100644 --- a/uiautomator2/ext/xpath/__init__.py +++ b/uiautomator2/ext/xpath/__init__.py @@ -2,6 +2,7 @@ # import warnings + from uiautomator2.xpath import * warnings.simplefilter("always", DeprecationWarning) diff --git a/uiautomator2/init.py b/uiautomator2/init.py index 5f8ecec9..0284c5f8 100644 --- a/uiautomator2/init.py +++ b/uiautomator2/init.py @@ -14,9 +14,8 @@ from logzero import logger, setup_logger from retry import retry -from uiautomator2.version import (__apk_version__, __atx_agent_version__, - __jar_version__, __version__) from uiautomator2.utils import natualsize +from uiautomator2.version import __apk_version__, __atx_agent_version__, __jar_version__, __version__ appdir = os.path.join(os.path.expanduser("~"), '.uiautomator2') diff --git a/uiautomator2/screenrecord.py b/uiautomator2/screenrecord.py index 5535eb93..7840dde5 100644 --- a/uiautomator2/screenrecord.py +++ b/uiautomator2/screenrecord.py @@ -2,8 +2,8 @@ # import re -import time import threading +import time import cv2 import imageio diff --git a/uiautomator2/settings.py b/uiautomator2/settings.py index d8623277..df9058cc 100644 --- a/uiautomator2/settings.py +++ b/uiautomator2/settings.py @@ -6,7 +6,6 @@ import pprint from typing import Any - logger = logging.getLogger("uiautomator2") class Settings(object): diff --git a/uiautomator2/swipe.py b/uiautomator2/swipe.py index 00e71c8a..0c131911 100644 --- a/uiautomator2/swipe.py +++ b/uiautomator2/swipe.py @@ -1,6 +1,7 @@ # coding: utf-8 from typing import Union + from ._proto import Direction diff --git a/uiautomator2/utils.py b/uiautomator2/utils.py index 820a90f2..08c1a17b 100644 --- a/uiautomator2/utils.py +++ b/uiautomator2/utils.py @@ -10,24 +10,11 @@ from typing import Union import filelock -import six from ._proto import Direction from .exceptions import SessionBrokenError, UiObjectNotFoundError -def U(x): - if six.PY3: - return x - return x.decode('utf-8') if type(x) is str else x - - -def E(x): - if six.PY3: - return x - return x.encode('utf-8') if type(x) is unicode else x # noqa: F821 - - def check_alive(fn): @functools.wraps(fn) def inner(self, *args, **kwargs): @@ -74,7 +61,7 @@ def inner(self, *args, **kwargs): if not self.wait(timeout=timeout): raise UiObjectNotFoundError({ 'code': -32002, - 'message': E(self.selector.__str__()) + 'message': self.selector.__str__() }) return fn(self, *args, **kwargs) diff --git a/uiautomator2/version.py b/uiautomator2/version.py index 486411ef..8d534586 100644 --- a/uiautomator2/version.py +++ b/uiautomator2/version.py @@ -1,11 +1,8 @@ # coding: utf-8 # -import pkg_resources -try: - __version__ = pkg_resources.get_distribution("uiautomator2").version -except pkg_resources.DistributionNotFound: - __version__ = "unknown" +# version managed by poetry +__version__ = "0.0.0" # See ChangeLog for details diff --git a/uiautomator2/webview.py b/uiautomator2/webview.py index 87e1d291..d277b217 100644 --- a/uiautomator2/webview.py +++ b/uiautomator2/webview.py @@ -11,7 +11,6 @@ import adbutils import pychrome import requests - from logzero import logger @@ -134,9 +133,11 @@ def screenshot(self): raise NotImplementedError() -from selenium import webdriver from contextlib import contextmanager +from selenium import webdriver + + @contextmanager def driver(package_name): serial = adbutils.adb.device().serial diff --git a/uiautomator2/xpath.py b/uiautomator2/xpath.py index 23268329..4e5773db 100644 --- a/uiautomator2/xpath.py +++ b/uiautomator2/xpath.py @@ -25,7 +25,7 @@ from ._proto import Direction from .abcd import BasicUIMeta from .exceptions import XPathElementNotFoundError -from .utils import U, inject_call, swipe_in_bounds +from .utils import inject_call, swipe_in_bounds try: from lxml import etree