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.
Files changed (123) hide show
  1. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/.gitignore +1 -0
  2. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/CHANGELOG.md +9 -0
  3. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/PKG-INFO +1 -1
  4. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/PKG-INFO +1 -1
  5. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/SOURCES.txt +5 -0
  6. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/port/shell_port.py +22 -10
  7. esp_test_utils-0.3.3/esptest/common/parser.py +265 -0
  8. esp_test_utils-0.3.3/esptest/testcase/result.py +377 -0
  9. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/tools/download_bin.py +3 -3
  10. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/adapter/test_shell_port.py +20 -0
  11. esp_test_utils-0.3.3/tests/conftest.py +0 -0
  12. esp_test_utils-0.3.3/tests/test_parse_expand_list.py +106 -0
  13. esp_test_utils-0.3.3/tests/testcase/test_result.py +478 -0
  14. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/utility/test_parse_bin_path.py +1 -0
  15. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/.github/.gitkeep +0 -0
  16. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/.github/workflows/pypi-publish.yml +0 -0
  17. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/.gitlab-ci.yml +0 -0
  18. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/.pre-commit-config.yaml +0 -0
  19. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/CONTRIBUTING.md +0 -0
  20. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/LICENSE +0 -0
  21. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/README.md +0 -0
  22. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/docs/Makefile +0 -0
  23. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/docs/conf.py +0 -0
  24. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/docs/index.rst +0 -0
  25. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/docs/make.bat +0 -0
  26. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/dependency_links.txt +0 -0
  27. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/entry_points.txt +0 -0
  28. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/requires.txt +0 -0
  29. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esp_test_utils.egg-info/top_level.txt +0 -0
  30. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/__init__.py +0 -0
  31. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/__main__.py +0 -0
  32. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/__init__.py +0 -0
  33. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/__init__.py +0 -0
  34. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/create_dut.py +0 -0
  35. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/dut_base.py +0 -0
  36. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/esp_dut.py +0 -0
  37. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/esp_mixin.py +0 -0
  38. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/esp_port.py +0 -0
  39. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/mac_mixin.py +0 -0
  40. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/dut/wrapper.py +0 -0
  41. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/port/__init__.py +0 -0
  42. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/port/base_port.py +0 -0
  43. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/adapter/port/serial_port.py +0 -0
  44. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/all.py +0 -0
  45. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/__init__.py +0 -0
  46. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/compat_typing.py +0 -0
  47. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/data_monitor.py +0 -0
  48. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/decorators.py +0 -0
  49. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/encoding.py +0 -0
  50. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/generator.py +0 -0
  51. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/shell.py +0 -0
  52. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/common/timestamp.py +0 -0
  53. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/config/__init__.py +0 -0
  54. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/config/default_config.py +0 -0
  55. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/config/env_config.py +0 -0
  56. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/db/__init__.py +0 -0
  57. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/db/runners.py +0 -0
  58. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/__init__.py +0 -0
  59. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/attenuator.py +0 -0
  60. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/esp_serial.py +0 -0
  61. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/serial_dut.py +0 -0
  62. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/serial_tools.py +0 -0
  63. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/devices/switch.py +0 -0
  64. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/env/__init__.py +0 -0
  65. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/env/base_env.py +0 -0
  66. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/env/wifi_env.py +0 -0
  67. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/esp_console/__init__.py +0 -0
  68. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/esp_console/wifi_cmd.py +0 -0
  69. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/interface/__init__.py +0 -0
  70. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/interface/dut.py +0 -0
  71. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/interface/port.py +0 -0
  72. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/iperf_utility/__init__.py +0 -0
  73. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/iperf_utility/iperf_results.py +0 -0
  74. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/iperf_utility/iperf_test.py +0 -0
  75. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/iperf_utility/iperf_test.test.py +0 -0
  76. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/iperf_utility/line_chart.py +0 -0
  77. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/logger/__init__.py +0 -0
  78. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/logger/logger.py +0 -0
  79. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/network/__init__.py +0 -0
  80. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/network/mac.py +0 -0
  81. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/network/netif.py +0 -0
  82. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/network/nic.py +0 -0
  83. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/scripts/downbin.py +0 -0
  84. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/scripts/list_ports.py +0 -0
  85. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/scripts/monitor.py +0 -0
  86. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/scripts/set_att.py +0 -0
  87. {esp_test_utils-0.3.2/tests → esp_test_utils-0.3.3/esptest/testcase}/__init__.py +0 -0
  88. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/tools/copy_bin.py +0 -0
  89. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/tools/http_download.py +0 -0
  90. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/tools/pip_check.py +0 -0
  91. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/tools/uart_monitor.py +0 -0
  92. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/utility/gen_esp32part.py +0 -0
  93. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/esptest/utility/parse_bin_path.py +0 -0
  94. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/example/jap_test.py +0 -0
  95. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/example/restart_test.py +0 -0
  96. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/pyproject.toml +0 -0
  97. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/setup.cfg +0 -0
  98. /esp_test_utils-0.3.2/tests/conftest.py → /esp_test_utils-0.3.3/tests/__init__.py +0 -0
  99. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/adapter/test_Dut.py +0 -0
  100. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/basic/test_decorators.py +0 -0
  101. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/basic/test_network.py +0 -0
  102. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/db/test_db_runners.py +0 -0
  103. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/devices/test_switch.py +0 -0
  104. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/esp_console/_files/wifi_cmd_connected_1.log +0 -0
  105. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/esp_console/_files/wifi_cmd_connected_2.log +0 -0
  106. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/esp_console/conftest.py +0 -0
  107. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/esp_console/test_WifiCmd.py +0 -0
  108. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/_files/dut_iperf_rx1.log +0 -0
  109. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/_files/dut_iperf_rx2.log +0 -0
  110. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/_files/pc_iperf_rx.log +0 -0
  111. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/_files/pc_iperf_rx2.log +0 -0
  112. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/test_chart.py +0 -0
  113. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/test_iperf_results.py +0 -0
  114. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/iperf_utility/test_iperf_util.py +0 -0
  115. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/test_EnvConfig.py +0 -0
  116. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/test_common.py +0 -0
  117. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/test_import.py +0 -0
  118. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/tools/test_download_bin.py +0 -0
  119. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/tools/test_download_file.py +0 -0
  120. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/tools/test_pip_check.py +0 -0
  121. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tests/utility/_files/test-bin.zip +0 -0
  122. {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
  123. {esp_test_utils-0.3.2 → esp_test_utils-0.3.3}/tools/ci/check_dev_version.py +0 -0
@@ -21,6 +21,7 @@ gl-codequality.json
21
21
  dut_logs/
22
22
  dut.log
23
23
  tests/utility/_files/test-bin
24
+ performance_result.json
24
25
 
25
26
 
26
27
  # docs
@@ -1,3 +1,12 @@
1
+ ## v0.3.3 (2026-04-21)
2
+
3
+
4
+ - feat: parser support expand list
5
+ - feat: add index parser function
6
+ - feat: add performance result
7
+ - feat: shell_port support check is_alive
8
+ - fix: unzip bin path tmp dir
9
+
1
10
  ## v0.3.2 (2026-03-19)
2
11
 
3
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -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
- # Process has ended
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
- self._read_thread.join(timeout=0.1)
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
- return self.proc.stdout.read(size) # type: ignore
146
- # Windows: read from the queue
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.stdout.flush() # type: ignore
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
+ )