esp-test-utils 0.3.2__tar.gz → 0.3.3__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.2 → esp_test_utils-0.3.3}/.gitignore +1 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/CHANGELOG.md +9 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/PKG-INFO +1 -1
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/PKG-INFO +1 -1
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/SOURCES.txt +5 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/port/shell_port.py +22 -10
- esp_test_utils-0.3.3/esptest/common/parser.py +265 -0
- esp_test_utils-0.3.3/esptest/testcase/result.py +377 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/tools/download_bin.py +3 -3
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/adapter/test_shell_port.py +20 -0
- esp_test_utils-0.3.3/tests/conftest.py +0 -0
- esp_test_utils-0.3.3/tests/test_parse_expand_list.py +106 -0
- esp_test_utils-0.3.3/tests/testcase/test_result.py +478 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/utility/test_parse_bin_path.py +1 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/.github/.gitkeep +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/.github/workflows/pypi-publish.yml +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/.gitlab-ci.yml +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/.pre-commit-config.yaml +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/CONTRIBUTING.md +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/LICENSE +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/README.md +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/docs/Makefile +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/docs/conf.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/docs/index.rst +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/docs/make.bat +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/dependency_links.txt +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/entry_points.txt +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/requires.txt +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/top_level.txt +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/__main__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/create_dut.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/dut_base.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/esp_dut.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/esp_mixin.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/esp_port.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/mac_mixin.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/wrapper.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/port/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/port/base_port.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/port/serial_port.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/all.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/compat_typing.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/data_monitor.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/decorators.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/encoding.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/generator.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/shell.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/timestamp.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/config/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/config/default_config.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/config/env_config.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/db/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/db/runners.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/attenuator.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/esp_serial.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/serial_dut.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/serial_tools.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/switch.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/env/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/env/base_env.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/env/wifi_env.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/esp_console/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/esp_console/wifi_cmd.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/interface/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/interface/dut.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/interface/port.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/iperf_utility/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/iperf_utility/iperf_results.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/iperf_utility/iperf_test.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/iperf_utility/iperf_test.test.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/iperf_utility/line_chart.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/logger/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/logger/logger.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/network/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/network/mac.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/network/netif.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/network/nic.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/scripts/downbin.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/scripts/list_ports.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/scripts/monitor.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/scripts/set_att.py +0 -0
- {esp_test_utils-0.3.2/tests → esp_test_utils-0.3.3/esptest/testcase}/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/tools/copy_bin.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/tools/http_download.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/tools/pip_check.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/tools/uart_monitor.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/utility/gen_esp32part.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/utility/parse_bin_path.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/example/jap_test.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/example/restart_test.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/pyproject.toml +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/setup.cfg +0 -0
- /esp_test_utils-0.3.2/tests/conftest.py → /esp_test_utils-0.3.3/tests/__init__.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/adapter/test_Dut.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/basic/test_decorators.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/basic/test_network.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/db/test_db_runners.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/devices/test_switch.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/esp_console/_files/wifi_cmd_connected_1.log +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/esp_console/_files/wifi_cmd_connected_2.log +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/esp_console/conftest.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/esp_console/test_WifiCmd.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/_files/dut_iperf_rx1.log +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/_files/dut_iperf_rx2.log +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/_files/pc_iperf_rx.log +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/_files/pc_iperf_rx2.log +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/test_chart.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/test_iperf_results.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/test_iperf_util.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/test_EnvConfig.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/test_common.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/test_import.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/tools/test_download_bin.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/tools/test_download_file.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/tools/test_pip_check.py +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/utility/_files/test-bin.zip +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig +0 -0
- {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tools/ci/check_dev_version.py +0 -0
|
@@ -40,6 +40,7 @@ esptest/common/data_monitor.py
|
|
|
40
40
|
esptest/common/decorators.py
|
|
41
41
|
esptest/common/encoding.py
|
|
42
42
|
esptest/common/generator.py
|
|
43
|
+
esptest/common/parser.py
|
|
43
44
|
esptest/common/shell.py
|
|
44
45
|
esptest/common/timestamp.py
|
|
45
46
|
esptest/config/__init__.py
|
|
@@ -76,6 +77,8 @@ esptest/scripts/downbin.py
|
|
|
76
77
|
esptest/scripts/list_ports.py
|
|
77
78
|
esptest/scripts/monitor.py
|
|
78
79
|
esptest/scripts/set_att.py
|
|
80
|
+
esptest/testcase/__init__.py
|
|
81
|
+
esptest/testcase/result.py
|
|
79
82
|
esptest/tools/copy_bin.py
|
|
80
83
|
esptest/tools/download_bin.py
|
|
81
84
|
esptest/tools/http_download.py
|
|
@@ -90,6 +93,7 @@ tests/conftest.py
|
|
|
90
93
|
tests/test_EnvConfig.py
|
|
91
94
|
tests/test_common.py
|
|
92
95
|
tests/test_import.py
|
|
96
|
+
tests/test_parse_expand_list.py
|
|
93
97
|
tests/adapter/test_Dut.py
|
|
94
98
|
tests/adapter/test_shell_port.py
|
|
95
99
|
tests/basic/test_decorators.py
|
|
@@ -107,6 +111,7 @@ tests/iperf_utility/_files/dut_iperf_rx1.log
|
|
|
107
111
|
tests/iperf_utility/_files/dut_iperf_rx2.log
|
|
108
112
|
tests/iperf_utility/_files/pc_iperf_rx.log
|
|
109
113
|
tests/iperf_utility/_files/pc_iperf_rx2.log
|
|
114
|
+
tests/testcase/test_result.py
|
|
110
115
|
tests/tools/test_download_bin.py
|
|
111
116
|
tests/tools/test_download_file.py
|
|
112
117
|
tests/tools/test_pip_check.py
|
|
@@ -82,7 +82,8 @@ class ShellRaw(RawPort):
|
|
|
82
82
|
if data:
|
|
83
83
|
self._read_queue.put(data)
|
|
84
84
|
elif self.proc.poll() is not None:
|
|
85
|
-
|
|
85
|
+
logger.info(f'shell command [{self.cmd}] was ended with code {self.proc.poll()}')
|
|
86
|
+
self.close()
|
|
86
87
|
break
|
|
87
88
|
except (OSError, ValueError):
|
|
88
89
|
break
|
|
@@ -95,7 +96,10 @@ class ShellRaw(RawPort):
|
|
|
95
96
|
if self._read_thread_stop:
|
|
96
97
|
self._read_thread_stop.set()
|
|
97
98
|
if self._read_thread:
|
|
98
|
-
|
|
99
|
+
try:
|
|
100
|
+
self._read_thread.join(timeout=0.1)
|
|
101
|
+
except RuntimeError:
|
|
102
|
+
pass
|
|
99
103
|
if self.proc:
|
|
100
104
|
if self.proc.pid:
|
|
101
105
|
try:
|
|
@@ -138,20 +142,24 @@ class ShellRaw(RawPort):
|
|
|
138
142
|
|
|
139
143
|
def read_bytes_nonblocking(self, size: int = -1) -> bytes:
|
|
140
144
|
"""non-blocking read bytes"""
|
|
141
|
-
if not self.proc:
|
|
142
|
-
return b''
|
|
143
145
|
if sys.platform != 'win32':
|
|
146
|
+
if not self.proc:
|
|
147
|
+
return b''
|
|
144
148
|
self.proc.stdout.flush() # type: ignore
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
data = self.proc.stdout.read(size) # type: ignore
|
|
150
|
+
if self.proc.poll() is not None:
|
|
151
|
+
logger.info(f'shell command [{self.cmd}] was ended with code {self.proc.poll()}')
|
|
152
|
+
self.close()
|
|
153
|
+
return data # type: ignore
|
|
154
|
+
# Windows: read from the queue (may still have data after process exit)
|
|
155
|
+
if not self._read_queue:
|
|
156
|
+
return b''
|
|
147
157
|
try:
|
|
148
|
-
if not self._read_queue:
|
|
149
|
-
return b''
|
|
150
|
-
# On Windows, use the queue from the background thread
|
|
151
158
|
data = b''
|
|
152
159
|
while True:
|
|
153
160
|
try:
|
|
154
|
-
self.proc
|
|
161
|
+
if self.proc:
|
|
162
|
+
self.proc.stdout.flush() # type: ignore
|
|
155
163
|
chunk = self._read_queue.get_nowait()
|
|
156
164
|
data += chunk
|
|
157
165
|
# If we have enough data and size is specified, stop reading
|
|
@@ -182,6 +190,10 @@ class ShellPort(BasePort[ShellRaw]):
|
|
|
182
190
|
raw_port = ShellRaw(cmd=cmd, env=env)
|
|
183
191
|
super().__init__(raw_port, name, log_file, **kwargs)
|
|
184
192
|
|
|
193
|
+
@property
|
|
194
|
+
def is_alive(self) -> bool:
|
|
195
|
+
return self.raw_port.proc is not None
|
|
196
|
+
|
|
185
197
|
|
|
186
198
|
class InvalidRaw(RawPort):
|
|
187
199
|
"""A invalid Raw Port class that always raise NotImplementedError to pass type check"""
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import string
|
|
3
|
+
|
|
4
|
+
import esptest.common.compat_typing as t
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _optional_int(value: t.Optional[str]) -> t.Optional[int]:
|
|
8
|
+
"""Convert a string to ``int``; return ``None`` for ``None`` or empty string.
|
|
9
|
+
|
|
10
|
+
Used to parse omissible fields inside slice expressions.
|
|
11
|
+
"""
|
|
12
|
+
return int(value) if value not in (None, '') else None # type: ignore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _require_max_len(max_len: t.Optional[int], part: str) -> int:
|
|
16
|
+
if max_len is None or max_len <= 0:
|
|
17
|
+
raise ValueError(f'`max_len` is required for slice `{part}`')
|
|
18
|
+
return max_len
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_zfill_len(data: str, force: bool = False) -> int:
|
|
22
|
+
"""Resolve the zero-fill width for the given expression.
|
|
23
|
+
|
|
24
|
+
- If the expression ends with ``#<N>``, return ``N`` (explicitly declared width).
|
|
25
|
+
- Otherwise, when ``force=True``, return the length of the longest numeric run
|
|
26
|
+
found in the expression.
|
|
27
|
+
- Return ``0`` in all other cases (no zero-fill).
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
"2-7#3" -> 3
|
|
31
|
+
"02-07" -> 2 (force=True)
|
|
32
|
+
"1,2" -> 0
|
|
33
|
+
"""
|
|
34
|
+
m = re.search(r'#(\d+)$', data)
|
|
35
|
+
if m:
|
|
36
|
+
return int(m.group(1))
|
|
37
|
+
if force:
|
|
38
|
+
return max(map(len, re.findall(r'\d+', data)))
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _expand_to_list_ex( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
43
|
+
data: str,
|
|
44
|
+
max_len: t.Optional[int] = None,
|
|
45
|
+
sort: bool = False,
|
|
46
|
+
dedup: bool = False,
|
|
47
|
+
zfilled: bool = False,
|
|
48
|
+
is_index: bool = False,
|
|
49
|
+
strip: bool = False,
|
|
50
|
+
valid_chars: str = '',
|
|
51
|
+
) -> t.List[t.Union[int, str]]:
|
|
52
|
+
"""Expand a compact expression into a list (low-level implementation).
|
|
53
|
+
|
|
54
|
+
Prefer using :func:`expand_to_list` or :func:`parse_param_idx` from the outside.
|
|
55
|
+
|
|
56
|
+
Supported syntax (``,`` is the separator; an optional ``#N`` suffix declares the
|
|
57
|
+
zero-fill width):
|
|
58
|
+
|
|
59
|
+
- Single point: ``3`` -> [3]
|
|
60
|
+
- Range: ``2-5`` -> [2, 3, 4, 5]
|
|
61
|
+
- Python slice: ``start:[stop[:step]]``, semantics align with list slicing
|
|
62
|
+
(e.g. ``1:-1``, ``::-1``)
|
|
63
|
+
- Exclusion: ``!<expr>`` remove the expansion of ``<expr>``
|
|
64
|
+
from the current result set
|
|
65
|
+
- Composition: ``0,2-7,!5`` -> [0, 2, 3, 4, 6, 7]
|
|
66
|
+
- Zero-fill: ``2-7#3`` -> ['002', ..., '007']
|
|
67
|
+
- String list: ``a,b,c`` -> ['a', 'b', 'c'] (``is_index=False``)
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
data: The expression to parse. Empty string or ``None`` is treated as invalid.
|
|
71
|
+
max_len: Total length of the target sequence. Required only when the slice or
|
|
72
|
+
negative indexing depends on it.
|
|
73
|
+
sort: Whether to sort the final result in ascending order.
|
|
74
|
+
dedup: Whether to de-duplicate the final result (implemented via ``set``,
|
|
75
|
+
which does not preserve input order).
|
|
76
|
+
zfilled: Whether to force zero-fill the output to the longest numeric width
|
|
77
|
+
(and emit strings).
|
|
78
|
+
is_index: Whether to parse with index semantics (enables slice, negative index
|
|
79
|
+
and ``!`` exclusion).
|
|
80
|
+
strip: Whether to ``strip()`` whitespace around each comma-separated segment.
|
|
81
|
+
valid_chars: Character whitelist for segments that may be preserved as raw
|
|
82
|
+
string tokens.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
The parsed list; elements may be ``int`` or ``str`` depending on whether
|
|
86
|
+
zero-fill or string-token handling was triggered.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If the input is empty, the format is invalid, or a slice requires
|
|
90
|
+
``max_len`` but none was supplied.
|
|
91
|
+
"""
|
|
92
|
+
if not data:
|
|
93
|
+
raise ValueError('Invalid input')
|
|
94
|
+
|
|
95
|
+
# Parse the zero-fill width, then strip the `#N` suffix.
|
|
96
|
+
zfill_len = get_zfill_len(data, zfilled)
|
|
97
|
+
data = data.split('#')[0]
|
|
98
|
+
|
|
99
|
+
all_idx: t.List[int] = []
|
|
100
|
+
if is_index:
|
|
101
|
+
# Pre-build the full index pool; filled only when max_len is given, and
|
|
102
|
+
# consulted on demand during slice evaluation.
|
|
103
|
+
all_idx = list(range(max_len)) if max_len else []
|
|
104
|
+
# Backward compatibility: accept `/` as an alternative separator.
|
|
105
|
+
data = data.replace('/', ',')
|
|
106
|
+
|
|
107
|
+
parse_results: t.List[t.Union[int, str]] = []
|
|
108
|
+
for part in data.split(','):
|
|
109
|
+
if strip:
|
|
110
|
+
part = part.strip()
|
|
111
|
+
|
|
112
|
+
if not part:
|
|
113
|
+
raise ValueError('format error: empty segment')
|
|
114
|
+
|
|
115
|
+
# `!` exclusion: valid only in index mode; drop the expansion of the
|
|
116
|
+
# sub-expression from the current result set.
|
|
117
|
+
if part.startswith('!'):
|
|
118
|
+
if not is_index:
|
|
119
|
+
raise ValueError(f'format error: `!` exclusion is only valid in index mode (`{part}`)')
|
|
120
|
+
# When nothing has been accumulated yet, fall back to the full index
|
|
121
|
+
# pool so that pure `!xxx` expressions still work.
|
|
122
|
+
parse_results = parse_results or list(all_idx)
|
|
123
|
+
excluded = _expand_to_list_ex(part[1:], max_len, is_index=is_index)
|
|
124
|
+
parse_results = [i for i in parse_results if i not in excluded]
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
# String token: keep the segment as-is only if every character is in the
|
|
128
|
+
# `valid_chars` whitelist.
|
|
129
|
+
if valid_chars and all(char in valid_chars for char in part):
|
|
130
|
+
parse_results.append(part)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
if is_index and part.isdigit():
|
|
134
|
+
parse_results.append(int(part))
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Range: `<start>-<end>`, non-negative integers on both sides, inclusive.
|
|
138
|
+
match = re.match(r'^(\d+)-(\d+)$', part)
|
|
139
|
+
if match:
|
|
140
|
+
start, end = map(int, match.groups())
|
|
141
|
+
new_values = range(start, end + 1) if is_index else list(map(str, range(start, end + 1)))
|
|
142
|
+
parse_results.extend(new_values)
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if is_index:
|
|
146
|
+
# Python slice: `start:[stop[:step]]`; each field may be empty or negative.
|
|
147
|
+
match = re.match(r'^(?P<start>-?\d*):(?P<stop>-?\d*)(:(?P<step>-?\d*))?$', part)
|
|
148
|
+
if match:
|
|
149
|
+
start, stop, step = map(_optional_int, match.groupdict().values()) # type: ignore
|
|
150
|
+
if step is None:
|
|
151
|
+
step = 1
|
|
152
|
+
if step == 0:
|
|
153
|
+
raise ValueError(f'Invalid slice format `{part}`, step cannot be 0')
|
|
154
|
+
# For step > 0 the business rule clamps a negative `start` to 0;
|
|
155
|
+
# other cases follow Python slice semantics.
|
|
156
|
+
if start is None:
|
|
157
|
+
start = 0 if step > 0 else _require_max_len(max_len, part) - 1
|
|
158
|
+
elif start < 0:
|
|
159
|
+
start = 0 if step > 0 else max(0, _require_max_len(max_len, part) + start)
|
|
160
|
+
if stop is None:
|
|
161
|
+
stop = _require_max_len(max_len, part) if step > 0 else -1
|
|
162
|
+
elif stop < 0:
|
|
163
|
+
stop = _require_max_len(max_len, part) + stop
|
|
164
|
+
|
|
165
|
+
new_values = range(start, stop, step) if is_index else list(map(str, range(start, stop, step)))
|
|
166
|
+
parse_results.extend(new_values)
|
|
167
|
+
continue
|
|
168
|
+
if ':' in part:
|
|
169
|
+
raise ValueError(f'Invalid slice format `{part}`, use `start:[stop[:step]]`')
|
|
170
|
+
|
|
171
|
+
raise ValueError(f'format error: unrecognized segment `{part}`')
|
|
172
|
+
|
|
173
|
+
# Normalize to int in index mode to avoid mixing string tokens with numbers.
|
|
174
|
+
if is_index:
|
|
175
|
+
int_results: t.List[int] = [int(x) for x in parse_results]
|
|
176
|
+
if max_len is not None and any(x < 0 or x >= max_len for x in int_results):
|
|
177
|
+
raise ValueError(f'index out of range, max_len={max_len}')
|
|
178
|
+
parse_results = list(int_results)
|
|
179
|
+
|
|
180
|
+
if dedup:
|
|
181
|
+
parse_results = list(set(parse_results))
|
|
182
|
+
if sort:
|
|
183
|
+
parse_results.sort() # type: ignore[call-overload]
|
|
184
|
+
if zfill_len:
|
|
185
|
+
parse_results = [str(item).zfill(zfill_len) for item in parse_results]
|
|
186
|
+
|
|
187
|
+
return parse_results
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def expand_to_list(
|
|
191
|
+
data: str,
|
|
192
|
+
valid_chars: str = '',
|
|
193
|
+
) -> t.List[str]:
|
|
194
|
+
"""Expand a string expression into a list of tokens (string mode).
|
|
195
|
+
|
|
196
|
+
Index / slice / ``!`` exclusion semantics are not enabled in this mode.
|
|
197
|
+
|
|
198
|
+
The default ``valid_chars`` is ``letters + digits + whitespace``; a comma-
|
|
199
|
+
separated segment is kept as a raw string token only if every one of its
|
|
200
|
+
characters falls inside this set.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
data: The expression to parse.
|
|
204
|
+
valid_chars: Character whitelist for raw string tokens; the default set is
|
|
205
|
+
used when this is empty.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
A list of string tokens.
|
|
209
|
+
|
|
210
|
+
Examples:
|
|
211
|
+
"a,b,c" -> ['a', 'b', 'c']
|
|
212
|
+
"a1,b2" -> ['a1', 'b2']
|
|
213
|
+
"2-5" -> [2, 3, 4, 5] # range syntax still applies
|
|
214
|
+
"""
|
|
215
|
+
if not valid_chars:
|
|
216
|
+
valid_chars = string.digits + string.ascii_letters + string.whitespace
|
|
217
|
+
expended = _expand_to_list_ex(data, is_index=False, valid_chars=valid_chars)
|
|
218
|
+
if len(expended) <= 1:
|
|
219
|
+
raise ValueError(f'format error: invalid expression `{data}`')
|
|
220
|
+
# In string mode `_expand_to_list_ex` only ever appends strings, so the cast is safe.
|
|
221
|
+
return [str(x) for x in expended]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def parse_param_idx(
|
|
225
|
+
data: str,
|
|
226
|
+
max_len: t.Optional[int] = None,
|
|
227
|
+
sort: bool = False,
|
|
228
|
+
dedup: bool = False,
|
|
229
|
+
zfilled: bool = False,
|
|
230
|
+
) -> t.List[t.Union[int, str]]:
|
|
231
|
+
"""Expand a parameter-index expression into an index list (index mode).
|
|
232
|
+
|
|
233
|
+
Supports Python-style slices, negative indices and ``!`` exclusion. Equivalent
|
|
234
|
+
to ``_expand_to_list_ex(data, ..., is_index=True, strip=True)``.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
data: The index expression, e.g. ``'0,2-7,!5'``, ``'::-1'``, ``'02-07'``.
|
|
238
|
+
max_len: Total length of the target sequence; required when the expression
|
|
239
|
+
contains open-ended slices or negative indices.
|
|
240
|
+
sort: Whether to sort the result in ascending order.
|
|
241
|
+
dedup: Whether to de-duplicate the result (does not preserve order).
|
|
242
|
+
zfilled: Whether to zero-fill the output to the longest numeric width and
|
|
243
|
+
emit strings; the element type becomes ``str`` only in this case.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
``List[int]`` when ``zfilled=False``; ``List[str]`` when ``zfilled=True``.
|
|
247
|
+
|
|
248
|
+
Examples (max_len=10):
|
|
249
|
+
"3:,11-14" -> [3, 4, 5, 6, 7, 8, 9]
|
|
250
|
+
"4::-1" -> [4, 3, 2, 1, 0]
|
|
251
|
+
"0,2-7,!5" -> [0, 2, 3, 4, 6, 7]
|
|
252
|
+
"!3,!7" -> [0, 1, 2, 4, 5, 6, 8, 9]
|
|
253
|
+
"!3-7" -> [0, 1, 2, 8, 9]
|
|
254
|
+
"02-07" -> ['02', '03', '04', '05', '06', '07'] (zfilled=True)
|
|
255
|
+
"2-7#3" -> ['002', '003', '004', '005', '006', '007']
|
|
256
|
+
"""
|
|
257
|
+
return _expand_to_list_ex(
|
|
258
|
+
data,
|
|
259
|
+
max_len=max_len,
|
|
260
|
+
sort=sort,
|
|
261
|
+
dedup=dedup,
|
|
262
|
+
zfilled=zfilled,
|
|
263
|
+
is_index=True,
|
|
264
|
+
strip=True,
|
|
265
|
+
)
|