esp-test-utils 0.3.0__tar.gz → 0.3.2__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.
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/.gitignore +1 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/.gitlab-ci.yml +71 -4
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/CHANGELOG.md +15 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/PKG-INFO +1 -1
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/docs/conf.py +3 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/PKG-INFO +1 -1
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/SOURCES.txt +1 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/dut_base.py +2 -2
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/wrapper.py +6 -1
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/port/base_port.py +38 -25
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/port/serial_port.py +60 -18
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/port/shell_port.py +100 -11
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/compat_typing.py +9 -3
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/data_monitor.py +2 -2
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/decorators.py +42 -9
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/shell.py +18 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/config/env_config.py +4 -2
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/db/runners.py +4 -1
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/interface/port.py +6 -5
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/iperf_utility/iperf_results.py +27 -24
- esp_test_utils-0.3.2/esptest/iperf_utility/line_chart.py +244 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/network/netif.py +35 -11
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/scripts/downbin.py +2 -1
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/tools/copy_bin.py +2 -2
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/tools/download_bin.py +70 -14
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/utility/parse_bin_path.py +75 -13
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/pyproject.toml +1 -1
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/adapter/test_Dut.py +62 -1
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/adapter/test_shell_port.py +41 -11
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/basic/test_network.py +39 -3
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/esp_console/test_WifiCmd.py +56 -1
- esp_test_utils-0.3.2/tests/iperf_utility/test_chart.py +101 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/test_iperf_results.py +8 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/test_common.py +7 -2
- esp_test_utils-0.3.2/tests/tools/test_download_bin.py +81 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/utility/test_parse_bin_path.py +31 -0
- esp_test_utils-0.3.0/esptest/iperf_utility/line_chart.py +0 -86
- esp_test_utils-0.3.0/tests/iperf_utility/test_chart.py +0 -46
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/.github/.gitkeep +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/.github/workflows/pypi-publish.yml +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/.pre-commit-config.yaml +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/CONTRIBUTING.md +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/LICENSE +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/README.md +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/docs/Makefile +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/docs/index.rst +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/docs/make.bat +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/dependency_links.txt +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/entry_points.txt +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/requires.txt +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/top_level.txt +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/__main__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/create_dut.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/esp_dut.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/esp_mixin.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/esp_port.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/mac_mixin.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/port/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/all.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/encoding.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/generator.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/timestamp.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/config/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/config/default_config.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/db/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/attenuator.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/esp_serial.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/serial_dut.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/serial_tools.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/switch.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/env/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/env/base_env.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/env/wifi_env.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/esp_console/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/esp_console/wifi_cmd.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/interface/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/interface/dut.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/iperf_utility/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/iperf_utility/iperf_test.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/iperf_utility/iperf_test.test.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/logger/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/logger/logger.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/network/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/network/mac.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/network/nic.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/scripts/list_ports.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/scripts/monitor.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/scripts/set_att.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/tools/http_download.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/tools/pip_check.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/tools/uart_monitor.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/utility/gen_esp32part.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/example/jap_test.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/example/restart_test.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/setup.cfg +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/__init__.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/basic/test_decorators.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/conftest.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/db/test_db_runners.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/devices/test_switch.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/esp_console/_files/wifi_cmd_connected_1.log +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/esp_console/_files/wifi_cmd_connected_2.log +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/esp_console/conftest.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/dut_iperf_rx1.log +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/dut_iperf_rx2.log +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/pc_iperf_rx.log +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/pc_iperf_rx2.log +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/test_iperf_util.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/test_EnvConfig.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/test_import.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/tools/test_download_file.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/tools/test_pip_check.py +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/utility/_files/test-bin.zip +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig +0 -0
- {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tools/ci/check_dev_version.py +0 -0
|
@@ -30,20 +30,87 @@ pre-commit-check:
|
|
|
30
30
|
# ------------------------------------------------------------------------------------------------------
|
|
31
31
|
# Pytest
|
|
32
32
|
# ------------------------------------------------------------------------------------------------------
|
|
33
|
-
pytest-
|
|
33
|
+
pytest-linux:
|
|
34
34
|
stage: test
|
|
35
35
|
needs: []
|
|
36
|
+
image: ${IMG}
|
|
36
37
|
before_script:
|
|
37
38
|
- pip install -e '.[test]'
|
|
38
39
|
script:
|
|
39
|
-
- pytest --junitxml=reports/junit.xml --cov=esptest --cov-report=term --cov-report xml:reports/coverage.xml
|
|
40
|
-
|
|
40
|
+
# - pytest --junitxml=reports/junit.xml --cov=esptest --cov-report=term --cov-report xml:reports/coverage.xml
|
|
41
|
+
- if [ "$IMG" = "python:3.11-bookworm" ]; then pip install pyecharts; fi # test with pyecharts installed
|
|
42
|
+
- export COVERAGE_FILE=.coverage.${CI_JOB_ID}
|
|
43
|
+
- pytest --junitxml=reports/junit.xml --cov=esptest --cov-report=term
|
|
44
|
+
# coverage: '/TOTAL.*\s+(\d+)\%/'
|
|
41
45
|
artifacts:
|
|
42
46
|
paths:
|
|
43
|
-
- reports/
|
|
47
|
+
# - reports/
|
|
44
48
|
- '.coverage*'
|
|
45
49
|
reports:
|
|
46
50
|
junit: reports/junit.xml
|
|
51
|
+
expire_in: 2 days
|
|
52
|
+
parallel:
|
|
53
|
+
matrix:
|
|
54
|
+
- IMG: "python:3.7-bullseye"
|
|
55
|
+
- IMG: "python:3.11-bookworm"
|
|
56
|
+
- IMG: "python:3.14-trixie"
|
|
57
|
+
tags:
|
|
58
|
+
- host_test
|
|
59
|
+
|
|
60
|
+
pytest-win32:
|
|
61
|
+
stage: test
|
|
62
|
+
needs:
|
|
63
|
+
- job: pytest-linux
|
|
64
|
+
artifacts: false
|
|
65
|
+
before_script:
|
|
66
|
+
- pymanager exec -V:$env:PYTHON_VER -m venv venv
|
|
67
|
+
- 'venv\Scripts\Activate.ps1'
|
|
68
|
+
- python -V
|
|
69
|
+
- pip install '.[test]'
|
|
70
|
+
script:
|
|
71
|
+
# - pytest --junitxml=reports/junit.xml --cov=esptest --cov-report=term --cov-report xml:reports/coverage.xml
|
|
72
|
+
- $env:COVERAGE_FILE = ".coverage.$env:CI_JOB_ID"
|
|
73
|
+
- pytest --junitxml=reports/junit.xml --cov=esptest --cov-report=term
|
|
74
|
+
# coverage: '/TOTAL.*\s+(\d+)\%/'
|
|
75
|
+
artifacts:
|
|
76
|
+
paths:
|
|
77
|
+
# - reports/
|
|
78
|
+
- '.coverage*'
|
|
79
|
+
reports:
|
|
80
|
+
junit: reports/junit.xml
|
|
81
|
+
expire_in: 2 days
|
|
82
|
+
parallel:
|
|
83
|
+
matrix:
|
|
84
|
+
- PYTHON_VER: "3.7"
|
|
85
|
+
- PYTHON_VER: "3.11"
|
|
86
|
+
- PYTHON_VER: "3.14"
|
|
87
|
+
tags:
|
|
88
|
+
- windows
|
|
89
|
+
- pymanager
|
|
90
|
+
|
|
91
|
+
pytest-coverage:
|
|
92
|
+
stage: test
|
|
93
|
+
needs:
|
|
94
|
+
- job: pytest-linux
|
|
95
|
+
artifacts: true
|
|
96
|
+
- job: pytest-win32
|
|
97
|
+
artifacts: false # Do not merge coverage from win32
|
|
98
|
+
before_script:
|
|
99
|
+
- pip install coverage
|
|
100
|
+
script:
|
|
101
|
+
- coverage combine .coverage*
|
|
102
|
+
- coverage report
|
|
103
|
+
- coverage xml
|
|
104
|
+
- coverage html
|
|
105
|
+
coverage: '/TOTAL.*\s+(\d+)\%/'
|
|
106
|
+
artifacts:
|
|
107
|
+
paths:
|
|
108
|
+
- htmlcov/
|
|
109
|
+
- '.coverage'
|
|
110
|
+
- coverage.xml
|
|
111
|
+
# reports:
|
|
112
|
+
# coverage: 'coverage.xml'
|
|
113
|
+
expire_in: 1 week
|
|
47
114
|
tags:
|
|
48
115
|
- host_test
|
|
49
116
|
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## v0.3.2 (2026-03-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
- feat: add downbin with configs
|
|
5
|
+
- feat: add secure boot match check
|
|
6
|
+
- fix: parse partition for read-only dir
|
|
7
|
+
- change: add debug logs for downbin
|
|
8
|
+
|
|
9
|
+
## v0.3.1 (2026-01-16)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
- feat: add diff values for pyecharts
|
|
13
|
+
- feat: add secure boot check
|
|
14
|
+
- fix: pass pytest on windows
|
|
15
|
+
|
|
1
16
|
## v0.3.0 (2025-12-10)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -20,6 +20,9 @@ extensions = [
|
|
|
20
20
|
'sphinx.ext.napoleon',
|
|
21
21
|
]
|
|
22
22
|
|
|
23
|
+
# Use type hints from signature in parameter descriptions (avoids duplicating complex types in docstrings)
|
|
24
|
+
autodoc_typehints = 'description'
|
|
25
|
+
|
|
23
26
|
templates_path = ['_templates']
|
|
24
27
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
25
28
|
|
|
@@ -107,6 +107,7 @@ tests/iperf_utility/_files/dut_iperf_rx1.log
|
|
|
107
107
|
tests/iperf_utility/_files/dut_iperf_rx2.log
|
|
108
108
|
tests/iperf_utility/_files/pc_iperf_rx.log
|
|
109
109
|
tests/iperf_utility/_files/pc_iperf_rx2.log
|
|
110
|
+
tests/tools/test_download_bin.py
|
|
110
111
|
tests/tools/test_download_file.py
|
|
111
112
|
tests/tools/test_pip_check.py
|
|
112
113
|
tests/utility/test_parse_bin_path.py
|
|
@@ -258,9 +258,9 @@ class DutBase(VariablesMixin, DutInterface): # pylint: disable=too-many-public-
|
|
|
258
258
|
@overload
|
|
259
259
|
def expect(self, pattern: bytes, timeout: float = 30) -> None: ...
|
|
260
260
|
@overload
|
|
261
|
-
def expect(self, pattern: re.Pattern[str], timeout: float = 30) -> re.Match[str]: ...
|
|
261
|
+
def expect(self, pattern: 're.Pattern[str]', timeout: float = 30) -> 're.Match[str]': ...
|
|
262
262
|
@overload
|
|
263
|
-
def expect(self, pattern: re.Pattern[bytes], timeout: float = 30) -> re.Match[bytes]: ...
|
|
263
|
+
def expect(self, pattern: 're.Pattern[bytes]', timeout: float = 30) -> 're.Match[bytes]': ...
|
|
264
264
|
|
|
265
265
|
def expect(self, pattern, timeout=30): # type: ignore
|
|
266
266
|
if self._base_port_proxy:
|
|
@@ -6,7 +6,7 @@ from ...common.generator import get_next_index
|
|
|
6
6
|
from ...interface.dut import DutInterface
|
|
7
7
|
from ...logger import get_logger
|
|
8
8
|
from ..port.base_port import RawPort
|
|
9
|
-
from ..port.serial_port import SerialExt, SerialPort
|
|
9
|
+
from ..port.serial_port import SerialExt, SerialPort, serial_add_mixin
|
|
10
10
|
from .create_dut import create_dut
|
|
11
11
|
from .dut_base import DutBase, DutConfig
|
|
12
12
|
from .esp_dut import EspDut
|
|
@@ -62,6 +62,11 @@ def dut_wrapper(dut, name='', log_file='', wrap_cls=None): # type: ignore
|
|
|
62
62
|
dut.__class__ = SerialExt
|
|
63
63
|
dut_config = DutConfig(opened_port=dut, name=_name, log_file=log_file)
|
|
64
64
|
wrap_dut = wrap_cls(dut_config=dut_config)
|
|
65
|
+
elif isinstance(dut, serial.SerialBase):
|
|
66
|
+
_name = name or dut.port.split('/')[-1]
|
|
67
|
+
dut.__class__ = serial_add_mixin(dut.__class__)
|
|
68
|
+
dut_config = DutConfig(opened_port=dut, name=_name, log_file=log_file)
|
|
69
|
+
wrap_dut = wrap_cls(dut_config=dut_config)
|
|
65
70
|
elif isinstance(dut, RawPort):
|
|
66
71
|
_name = name
|
|
67
72
|
if not _name:
|
|
@@ -5,13 +5,12 @@ import logging
|
|
|
5
5
|
import os
|
|
6
6
|
import queue
|
|
7
7
|
import re
|
|
8
|
+
import sys
|
|
8
9
|
import threading
|
|
9
10
|
import time
|
|
10
11
|
from dataclasses import dataclass
|
|
11
12
|
from typing import overload
|
|
12
13
|
|
|
13
|
-
import pexpect.spawnbase
|
|
14
|
-
|
|
15
14
|
import esptest.common.compat_typing as t
|
|
16
15
|
|
|
17
16
|
from ...common import timestamp_str, to_bytes, to_str
|
|
@@ -19,6 +18,18 @@ from ...common.decorators import deprecated
|
|
|
19
18
|
from ...interface.port import PortInterface
|
|
20
19
|
from ...logger import get_logger
|
|
21
20
|
|
|
21
|
+
if sys.platform == 'win32':
|
|
22
|
+
import pexpect
|
|
23
|
+
from pexpect.exceptions import ExceptionPexpect
|
|
24
|
+
from pexpect.spawnbase import SpawnBase
|
|
25
|
+
# from wexpect import SpawnPipe as SpawnBase
|
|
26
|
+
# from wexpect import ExceptionPexpect
|
|
27
|
+
else:
|
|
28
|
+
import pexpect
|
|
29
|
+
from pexpect.exceptions import ExceptionPexpect
|
|
30
|
+
from pexpect.spawnbase import SpawnBase
|
|
31
|
+
|
|
32
|
+
|
|
22
33
|
logger = get_logger('port')
|
|
23
34
|
NEVER_MATCHED_MAGIC_STRING = 'o6K,Q.(w+~yr~N9R'
|
|
24
35
|
|
|
@@ -81,7 +92,7 @@ class SpawnConfig:
|
|
|
81
92
|
# TODO: monitors
|
|
82
93
|
|
|
83
94
|
|
|
84
|
-
class PortSpawn(
|
|
95
|
+
class PortSpawn(SpawnBase, t.Generic[T]):
|
|
85
96
|
"""Create a new class for pexpect with port read()/write() method.
|
|
86
97
|
|
|
87
98
|
There's some reason that we can not use pyserial with pexpect.fdpexpect directly:
|
|
@@ -268,6 +279,27 @@ class PortSpawn(pexpect.spawnbase.SpawnBase, t.Generic[T]):
|
|
|
268
279
|
self._line_cache = b''
|
|
269
280
|
|
|
270
281
|
|
|
282
|
+
def handle_expect_timeout(func: t.Callable) -> t.Callable:
|
|
283
|
+
"""Raise same type exception ExpectTimeout for ports from different frameworks"""
|
|
284
|
+
|
|
285
|
+
@functools.wraps(func)
|
|
286
|
+
def wrap(obj: 'BasePort', *args, **kwargs): # type: ignore
|
|
287
|
+
try:
|
|
288
|
+
result = func(obj, *args, **kwargs)
|
|
289
|
+
except obj.expect_timeout_exceptions as e:
|
|
290
|
+
data_in_buffer = ''
|
|
291
|
+
try:
|
|
292
|
+
if obj._pexpect_spawn: # pylint: disable=protected-access
|
|
293
|
+
data_in_buffer = obj._pexpect_spawn.before # pylint: disable=protected-access
|
|
294
|
+
except AttributeError:
|
|
295
|
+
pass # ignore
|
|
296
|
+
obj.logger.debug(f'ExpectTimeout: {str(e)}, data_in_buffer={repr(data_in_buffer)}')
|
|
297
|
+
raise ExpectTimeout(str(e), data_in_buffer=data_in_buffer) from e
|
|
298
|
+
return result
|
|
299
|
+
|
|
300
|
+
return wrap
|
|
301
|
+
|
|
302
|
+
|
|
271
303
|
class BasePort(PortInterface, t.Generic[T]):
|
|
272
304
|
"""A class to simply port methods for all devices / shell / sockets to similar usage
|
|
273
305
|
|
|
@@ -278,7 +310,7 @@ class BasePort(PortInterface, t.Generic[T]):
|
|
|
278
310
|
|
|
279
311
|
EXPECT_TIMEOUT_EXCEPTIONS: t.Tuple[t.Type[Exception], ...] = (
|
|
280
312
|
TimeoutError,
|
|
281
|
-
|
|
313
|
+
ExceptionPexpect,
|
|
282
314
|
)
|
|
283
315
|
INIT_START_REDIRECT_THREAD: bool = True
|
|
284
316
|
PEXPECT_DEFAULT_TIMEOUT: float = 30
|
|
@@ -406,25 +438,6 @@ class BasePort(PortInterface, t.Generic[T]):
|
|
|
406
438
|
if stopped:
|
|
407
439
|
self.start_redirect_thread()
|
|
408
440
|
|
|
409
|
-
@staticmethod
|
|
410
|
-
def handle_expect_timeout(func: t.Callable) -> t.Callable:
|
|
411
|
-
"""Raise same type exception ExpectTimeout for ports from different frameworks"""
|
|
412
|
-
|
|
413
|
-
@functools.wraps(func)
|
|
414
|
-
def wrap(self, *args, **kwargs): # type: ignore
|
|
415
|
-
try:
|
|
416
|
-
result = func(self, *args, **kwargs)
|
|
417
|
-
except self.expect_timeout_exceptions as e:
|
|
418
|
-
try:
|
|
419
|
-
data_in_buffer = self._pexpect_spawn.before # pylint: disable=protected-access
|
|
420
|
-
except AttributeError:
|
|
421
|
-
data_in_buffer = ''
|
|
422
|
-
self.logger.debug(f'ExpectTimeout: {str(e)}, data_in_buffer={repr(data_in_buffer)}')
|
|
423
|
-
raise ExpectTimeout(str(e), data_in_buffer=data_in_buffer) from e
|
|
424
|
-
return result
|
|
425
|
-
|
|
426
|
-
return wrap
|
|
427
|
-
|
|
428
441
|
def write(self, data: t.AnyStr) -> None:
|
|
429
442
|
if self._pexpect_spawn:
|
|
430
443
|
return self._pexpect_spawn.write(data)
|
|
@@ -446,9 +459,9 @@ class BasePort(PortInterface, t.Generic[T]):
|
|
|
446
459
|
@overload
|
|
447
460
|
def expect(self, pattern: bytes, timeout: float = 30) -> None: ...
|
|
448
461
|
@overload
|
|
449
|
-
def expect(self, pattern: re.Pattern[str], timeout: float = 30) -> re.Match[str]: ...
|
|
462
|
+
def expect(self, pattern: 're.Pattern[str]', timeout: float = 30) -> 're.Match[str]': ...
|
|
450
463
|
@overload
|
|
451
|
-
def expect(self, pattern: re.Pattern[bytes], timeout: float = 30) -> re.Match[bytes]: ...
|
|
464
|
+
def expect(self, pattern: 're.Pattern[bytes]', timeout: float = 30) -> 're.Match[bytes]': ...
|
|
452
465
|
|
|
453
466
|
@handle_expect_timeout
|
|
454
467
|
def expect(self, pattern, timeout=PEXPECT_DEFAULT_TIMEOUT): # type: ignore
|
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
import time
|
|
2
|
-
from typing import TYPE_CHECKING
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
import serial
|
|
5
|
-
from serial import Serial
|
|
5
|
+
from serial import Serial, SerialBase
|
|
6
|
+
|
|
7
|
+
import esptest.common.compat_typing as t
|
|
6
8
|
|
|
7
9
|
from ...common import to_bytes
|
|
8
10
|
from ...logger import get_logger
|
|
9
11
|
from .base_port import BasePort
|
|
10
12
|
|
|
11
13
|
if TYPE_CHECKING:
|
|
12
|
-
MixinBase: TypeAlias = 'BasePort'
|
|
14
|
+
MixinBase: t.TypeAlias = 'BasePort'
|
|
13
15
|
else:
|
|
14
16
|
MixinBase = object
|
|
15
17
|
|
|
16
18
|
logger = get_logger('ser_port')
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
class
|
|
20
|
-
|
|
21
|
+
class SerialBaseProtocol(t.Protocol):
|
|
22
|
+
@property
|
|
23
|
+
def port(self) -> t.Optional[str]: ...
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def baudrate(self) -> t.Optional[int]: ...
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def timeout(self) -> t.Optional[float]: ...
|
|
30
|
+
|
|
31
|
+
def read(self, size: int = 1) -> bytes: ...
|
|
32
|
+
|
|
33
|
+
def write(self, data: t.AnyStr) -> int: ...
|
|
21
34
|
|
|
35
|
+
|
|
36
|
+
class SerMixin(SerialBaseProtocol):
|
|
22
37
|
@property
|
|
23
38
|
def read_timeout(self) -> float:
|
|
24
39
|
# For PortSpawn
|
|
25
|
-
return
|
|
40
|
+
return self.timeout or 0.001 # type: ignore
|
|
26
41
|
|
|
27
42
|
def read_bytes(self, timeout: float = 0.001) -> bytes:
|
|
28
43
|
# For PortSpawn
|
|
@@ -30,25 +45,52 @@ class SerialExt(Serial):
|
|
|
30
45
|
assert self.timeout >= 0.001
|
|
31
46
|
if timeout > self.timeout:
|
|
32
47
|
time.sleep(timeout - self.timeout)
|
|
33
|
-
return
|
|
48
|
+
return self.read(1024) # type: ignore
|
|
34
49
|
|
|
35
|
-
def write_bytes(self, data: AnyStr) ->
|
|
50
|
+
def write_bytes(self, data: t.AnyStr) -> int:
|
|
36
51
|
# For PortSpawn
|
|
37
|
-
|
|
52
|
+
self.write(to_bytes(data))
|
|
53
|
+
return len(to_bytes(data))
|
|
38
54
|
|
|
39
55
|
def __str__(self) -> str:
|
|
40
|
-
"""
|
|
41
|
-
return f'
|
|
56
|
+
"""SerialExt<device=xxx,baudrate=xxx,timeout=xxx>"""
|
|
57
|
+
return f'SerialExt<device={self.port},baudrate={self.baudrate},timeout={self.timeout}>'
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SerialExt(Serial, SerMixin):
|
|
61
|
+
"""Add RawPort methods to serial.Serial"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def serial_add_mixin(cls: t.Type[t.Any]) -> t.Type[t.Any]:
|
|
65
|
+
"""动态为类添加 SerMixin"""
|
|
66
|
+
# 创建一个新的类,继承自原始类和 SerMixin
|
|
67
|
+
# 基类顺序与 SerialExt(Serial, SerMixin) 保持一致
|
|
68
|
+
return type(f'{cls.__name__}Ext', (cls, SerMixin), {})
|
|
42
69
|
|
|
43
70
|
|
|
44
71
|
class SerialPortMixin(MixinBase):
|
|
45
72
|
"""Add RawPort methods to serial.Serial"""
|
|
46
73
|
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _add_mixin_by_type(raw_port: t.Any) -> None:
|
|
76
|
+
"""根据原始类型添加对应的 mixin"""
|
|
77
|
+
if raw_port is None:
|
|
78
|
+
return
|
|
79
|
+
original_type = type(raw_port)
|
|
80
|
+
# If the original type already includes SerMixin, do nothing.
|
|
81
|
+
# This prevents repeatedly nesting mixin classes when the serial
|
|
82
|
+
# object is reassigned and _add_mixin_by_type is called multiple times.
|
|
83
|
+
if issubclass(original_type, SerMixin):
|
|
84
|
+
return
|
|
85
|
+
if issubclass(original_type, Serial):
|
|
49
86
|
raw_port.__class__ = SerialExt
|
|
87
|
+
elif issubclass(original_type, SerialBase):
|
|
88
|
+
raw_port.__class__ = serial_add_mixin(original_type)
|
|
89
|
+
|
|
90
|
+
def __init__(self, raw_port: t.Any, name: str, log_file: str = '') -> None:
|
|
91
|
+
self._add_mixin_by_type(raw_port)
|
|
50
92
|
super().__init__(raw_port, name, log_file)
|
|
51
|
-
self._serial_config: Dict[str, Any] = {}
|
|
93
|
+
self._serial_config: t.Dict[str, t.Any] = {}
|
|
52
94
|
|
|
53
95
|
def start_redirect_thread(self) -> None:
|
|
54
96
|
if not self.serial:
|
|
@@ -72,12 +114,12 @@ class SerialPortMixin(MixinBase):
|
|
|
72
114
|
super().start_redirect_thread()
|
|
73
115
|
|
|
74
116
|
@property
|
|
75
|
-
def serial(self) -> Optional[SerialExt]:
|
|
117
|
+
def serial(self) -> t.Optional[SerialExt]:
|
|
76
118
|
"""Get Current serial instance."""
|
|
77
119
|
return self._raw_port # type: ignore
|
|
78
120
|
|
|
79
121
|
@serial.setter
|
|
80
|
-
def serial(self, serial_instance: Optional[Serial]) -> None:
|
|
122
|
+
def serial(self, serial_instance: t.Optional[Serial]) -> None:
|
|
81
123
|
"""Set serial instance, will close and clean up the old serial resources"""
|
|
82
124
|
if self._raw_port:
|
|
83
125
|
# Close pexpect proc
|
|
@@ -87,7 +129,7 @@ class SerialPortMixin(MixinBase):
|
|
|
87
129
|
# self._port.close()
|
|
88
130
|
if serial_instance:
|
|
89
131
|
self._raw_port = serial_instance
|
|
90
|
-
self.
|
|
132
|
+
self._add_mixin_by_type(serial_instance)
|
|
91
133
|
self.start_redirect_thread()
|
|
92
134
|
|
|
93
135
|
def close(self) -> None:
|
|
@@ -107,7 +149,7 @@ class SerialPort(SerialPortMixin, BasePort):
|
|
|
107
149
|
This class using serial with pexpect.
|
|
108
150
|
"""
|
|
109
151
|
|
|
110
|
-
def __init__(self, dut: Serial, name: str, log_file: str = '', **kwargs: Any) -> None:
|
|
152
|
+
def __init__(self, dut: Serial, name: str, log_file: str = '', **kwargs: t.Any) -> None:
|
|
111
153
|
if not dut:
|
|
112
154
|
self.INIT_START_REDIRECT_THREAD = False # pylint: disable=invalid-name
|
|
113
155
|
super().__init__(dut, name, log_file, **kwargs)
|
|
@@ -1,47 +1,101 @@
|
|
|
1
|
+
import io
|
|
1
2
|
import os
|
|
3
|
+
import queue
|
|
2
4
|
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
3
7
|
import time
|
|
4
8
|
|
|
5
|
-
import pexpect
|
|
6
9
|
import psutil
|
|
7
10
|
|
|
8
11
|
import esptest.common.compat_typing as t
|
|
9
12
|
|
|
13
|
+
from ...common.shell import ensure_windows_env
|
|
10
14
|
from ...logger import get_logger
|
|
11
15
|
from .base_port import BasePort, RawPort
|
|
12
16
|
|
|
13
17
|
logger = get_logger('shell_port')
|
|
14
18
|
|
|
15
19
|
|
|
20
|
+
if sys.platform == 'win32':
|
|
21
|
+
# windows does not support pexpect.spawn
|
|
22
|
+
# import wexpect as pexpect # pexpect.spawn
|
|
23
|
+
import pexpect # pexpect.spawn
|
|
24
|
+
|
|
25
|
+
DEFAULT_SHELL = 'cmd.exe'
|
|
26
|
+
ensure_windows_env()
|
|
27
|
+
else:
|
|
28
|
+
import pexpect
|
|
29
|
+
|
|
30
|
+
DEFAULT_SHELL = '/bin/bash'
|
|
31
|
+
|
|
32
|
+
|
|
16
33
|
class ShellRaw(RawPort):
|
|
17
34
|
"""A subprocess Raw Port class that supports shell read, write
|
|
18
35
|
|
|
19
36
|
is a subclass of RawPort
|
|
20
37
|
"""
|
|
21
38
|
|
|
22
|
-
def __init__(self, cmd: str = '
|
|
39
|
+
def __init__(self, cmd: t.Union[str, t.List[str]] = '', env: t.Optional[t.Dict[str, str]] = None) -> None:
|
|
40
|
+
ensure_windows_env()
|
|
23
41
|
self.env = env or os.environ.copy()
|
|
24
42
|
self.env['PYTHONUNBUFFERED'] = 'true' # for python scripts, disable output buffering
|
|
25
|
-
self.cmd = cmd
|
|
43
|
+
self.cmd = cmd or DEFAULT_SHELL
|
|
26
44
|
self.proc: t.Optional[subprocess.Popen] = None
|
|
27
45
|
self.read_timeout = 0.002 # default read_timeout
|
|
46
|
+
# For Windows: use a thread and queue for non-blocking reads
|
|
47
|
+
self._read_queue: t.Optional[queue.Queue] = None
|
|
48
|
+
self._read_thread: t.Optional[threading.Thread] = None
|
|
49
|
+
self._read_thread_stop = threading.Event()
|
|
28
50
|
self.open()
|
|
29
51
|
|
|
30
52
|
def open(self) -> None:
|
|
31
53
|
if not self.proc:
|
|
32
54
|
self.proc = subprocess.Popen( # pylint: disable=consider-using-with
|
|
33
55
|
self.cmd,
|
|
34
|
-
shell=
|
|
56
|
+
shell=bool(not isinstance(self.cmd, list)),
|
|
35
57
|
env=self.env,
|
|
36
58
|
stdin=subprocess.PIPE,
|
|
37
59
|
stdout=subprocess.PIPE,
|
|
38
60
|
stderr=subprocess.STDOUT,
|
|
39
61
|
)
|
|
40
62
|
# Set stdout to non-blocking
|
|
41
|
-
|
|
63
|
+
if sys.platform != 'win32':
|
|
64
|
+
os.set_blocking(self.proc.stdout.fileno(), False) # type: ignore
|
|
65
|
+
else:
|
|
66
|
+
# Windows: subprocess pipes are blocking by default and cannot be set to non-blocking
|
|
67
|
+
# Use a background thread to read from the pipe
|
|
68
|
+
self._read_queue = queue.Queue()
|
|
69
|
+
self._read_thread_stop.clear()
|
|
70
|
+
self._read_thread = threading.Thread(target=self._read_stdout_thread, daemon=True)
|
|
71
|
+
self._read_thread.start()
|
|
72
|
+
|
|
73
|
+
def _read_stdout_thread(self) -> None:
|
|
74
|
+
"""Background thread to read from stdout on Windows (where pipes are blocking)."""
|
|
75
|
+
if not self.proc or not self._read_queue:
|
|
76
|
+
return
|
|
77
|
+
try:
|
|
78
|
+
while not self._read_thread_stop.is_set():
|
|
79
|
+
try:
|
|
80
|
+
# Read line by line to avoid blocking too long on Windows
|
|
81
|
+
data = self.proc.stdout.readline() # type: ignore
|
|
82
|
+
if data:
|
|
83
|
+
self._read_queue.put(data)
|
|
84
|
+
elif self.proc.poll() is not None:
|
|
85
|
+
# Process has ended
|
|
86
|
+
break
|
|
87
|
+
except (OSError, ValueError):
|
|
88
|
+
break
|
|
89
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
90
|
+
logger.error(f'Error in read_stdout_thread {type(e)}: {str(e)}')
|
|
42
91
|
|
|
43
92
|
def close(self) -> None:
|
|
44
93
|
"""Close subprocess."""
|
|
94
|
+
# Stop the read thread on Windows
|
|
95
|
+
if self._read_thread_stop:
|
|
96
|
+
self._read_thread_stop.set()
|
|
97
|
+
if self._read_thread:
|
|
98
|
+
self._read_thread.join(timeout=0.1)
|
|
45
99
|
if self.proc:
|
|
46
100
|
if self.proc.pid:
|
|
47
101
|
try:
|
|
@@ -84,10 +138,34 @@ class ShellRaw(RawPort):
|
|
|
84
138
|
|
|
85
139
|
def read_bytes_nonblocking(self, size: int = -1) -> bytes:
|
|
86
140
|
"""non-blocking read bytes"""
|
|
87
|
-
if self.proc:
|
|
141
|
+
if not self.proc:
|
|
142
|
+
return b''
|
|
143
|
+
if sys.platform != 'win32':
|
|
88
144
|
self.proc.stdout.flush() # type: ignore
|
|
89
145
|
return self.proc.stdout.read(size) # type: ignore
|
|
90
|
-
|
|
146
|
+
# Windows: read from the queue
|
|
147
|
+
try:
|
|
148
|
+
if not self._read_queue:
|
|
149
|
+
return b''
|
|
150
|
+
# On Windows, use the queue from the background thread
|
|
151
|
+
data = b''
|
|
152
|
+
while True:
|
|
153
|
+
try:
|
|
154
|
+
self.proc.stdout.flush() # type: ignore
|
|
155
|
+
chunk = self._read_queue.get_nowait()
|
|
156
|
+
data += chunk
|
|
157
|
+
# If we have enough data and size is specified, stop reading
|
|
158
|
+
if size > 0 and len(data) >= size: # pylint: disable=chained-comparison
|
|
159
|
+
break
|
|
160
|
+
except queue.Empty:
|
|
161
|
+
break
|
|
162
|
+
# Return the requested amount or all available data
|
|
163
|
+
if size > 0:
|
|
164
|
+
return data[:size]
|
|
165
|
+
return data
|
|
166
|
+
except (OSError, ValueError) as e:
|
|
167
|
+
logger.error(f'Error in read_bytes_nonblocking {type(e)}: {str(e)}')
|
|
168
|
+
raise
|
|
91
169
|
|
|
92
170
|
|
|
93
171
|
class ShellPort(BasePort[ShellRaw]):
|
|
@@ -96,7 +174,7 @@ class ShellPort(BasePort[ShellRaw]):
|
|
|
96
174
|
def __init__(
|
|
97
175
|
self,
|
|
98
176
|
cmd: str = '/bin/bash',
|
|
99
|
-
env: t.Optional[
|
|
177
|
+
env: t.Optional[t.Dict[str, str]] = None,
|
|
100
178
|
name: str = '',
|
|
101
179
|
log_file: str = '',
|
|
102
180
|
**kwargs: t.Any,
|
|
@@ -130,10 +208,17 @@ class PexpectPort(BasePort[InvalidRaw]):
|
|
|
130
208
|
log_file: str = '',
|
|
131
209
|
**kwargs: t.Any,
|
|
132
210
|
) -> None:
|
|
211
|
+
if sys.platform == 'win32':
|
|
212
|
+
raise NotImplementedError('PexpectPort is not supported on Windows now.')
|
|
133
213
|
self._cmd = cmd
|
|
134
214
|
raw_port = InvalidRaw()
|
|
135
215
|
self._pexpect_spawn: t.Optional[pexpect.spawn] = None # change type
|
|
136
|
-
self.log_file_f =
|
|
216
|
+
self.log_file_f: t.Optional[io.BufferedWriter] = None
|
|
217
|
+
if log_file:
|
|
218
|
+
os.makedirs(os.path.dirname(log_file) or '.', exist_ok=True)
|
|
219
|
+
self.log_file_f = open(log_file, 'wb') # pylint: disable=consider-using-with
|
|
220
|
+
else:
|
|
221
|
+
self.log_file_f = None
|
|
137
222
|
super().__init__(raw_port, name, log_file, **kwargs)
|
|
138
223
|
|
|
139
224
|
@property
|
|
@@ -152,13 +237,17 @@ class PexpectPort(BasePort[InvalidRaw]):
|
|
|
152
237
|
if self._pexpect_spawn:
|
|
153
238
|
self._pexpect_spawn.logfile = None # type: ignore
|
|
154
239
|
self.log_file_f.close()
|
|
240
|
+
if new_log_file:
|
|
241
|
+
os.makedirs(os.path.dirname(new_log_file) or '.', exist_ok=True)
|
|
242
|
+
self.log_file_f = open(new_log_file, 'wb') # pylint: disable=consider-using-with
|
|
243
|
+
else:
|
|
244
|
+
self.log_file_f = None
|
|
155
245
|
if self._pexpect_spawn:
|
|
156
|
-
self.log_file_f = open(new_log_file, 'wb') if new_log_file else None # pylint: disable=consider-using-with
|
|
157
246
|
self._pexpect_spawn.logfile = self.log_file_f # type: ignore
|
|
158
247
|
self._log_file = new_log_file
|
|
159
248
|
|
|
160
249
|
@property
|
|
161
|
-
def spawn(self) -> t.Optional[pexpect.spawn]: # type: ignore
|
|
250
|
+
def spawn(self) -> 't.Optional[pexpect.spawn]': # type: ignore
|
|
162
251
|
"""Allow the use of pexpect spawn enhancements, if pexpect process is available"""
|
|
163
252
|
return self._pexpect_spawn
|
|
164
253
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pylint: disable=unused-import
|
|
2
|
-
# flake8: noqa: F401
|
|
3
|
-
# ruff: noqa: F401
|
|
2
|
+
# flake8: noqa: F401,F404
|
|
3
|
+
# ruff: noqa: F401,F404
|
|
4
4
|
import sys
|
|
5
5
|
from typing import (
|
|
6
6
|
IO,
|
|
@@ -15,7 +15,7 @@ from typing import (
|
|
|
15
15
|
Iterator,
|
|
16
16
|
List,
|
|
17
17
|
Optional,
|
|
18
|
-
|
|
18
|
+
Sequence,
|
|
19
19
|
Set,
|
|
20
20
|
Tuple,
|
|
21
21
|
Type,
|
|
@@ -25,6 +25,12 @@ from typing import (
|
|
|
25
25
|
overload,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
+
if sys.version_info >= (3, 8):
|
|
29
|
+
from typing import Protocol
|
|
30
|
+
else:
|
|
31
|
+
from typing_extensions import Protocol
|
|
32
|
+
|
|
33
|
+
|
|
28
34
|
if sys.version_info >= (3, 9):
|
|
29
35
|
from contextlib import AbstractContextManager as ContextManager
|
|
30
36
|
else:
|