w1x-cli 3.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- w1x/__init__.py +5 -0
- w1x/app.py +754 -0
- w1x/config.py +189 -0
- w1x/engine.py +565 -0
- w1x/memory.py +272 -0
- w1x/models.py +458 -0
- w1x/puller.py +955 -0
- w1x/terminal.py +661 -0
- w1x/testpy.py +325 -0
- w1x/tools.py +897 -0
- w1x_cli-3.5.0.dist-info/METADATA +178 -0
- w1x_cli-3.5.0.dist-info/RECORD +16 -0
- w1x_cli-3.5.0.dist-info/WHEEL +5 -0
- w1x_cli-3.5.0.dist-info/entry_points.txt +2 -0
- w1x_cli-3.5.0.dist-info/licenses/LICENSE +21 -0
- w1x_cli-3.5.0.dist-info/top_level.txt +1 -0
w1x/testpy.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Модуль изолированного интерактивного тестирования Python-кода в отдельном окне CMD."""
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import ast
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .terminal import console, render_message
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_valid_python(code_str: str) -> bool:
|
|
16
|
+
"""Проверить, является ли строка валидным и осмысленным Python-кодом."""
|
|
17
|
+
cleaned = code_str.strip()
|
|
18
|
+
if not cleaned:
|
|
19
|
+
return False
|
|
20
|
+
lines = [l.strip() for l in cleaned.splitlines() if l.strip()]
|
|
21
|
+
if len(lines) <= 2 and any(
|
|
22
|
+
lines[0].lower().startswith(cmd)
|
|
23
|
+
for cmd in ("python ", "python3 ", "py ", "pip ", "cd ", "npm ", "git ")
|
|
24
|
+
):
|
|
25
|
+
return False
|
|
26
|
+
try:
|
|
27
|
+
tree = ast.parse(cleaned)
|
|
28
|
+
has_logic = any(
|
|
29
|
+
isinstance(
|
|
30
|
+
node,
|
|
31
|
+
(
|
|
32
|
+
ast.FunctionDef,
|
|
33
|
+
ast.AsyncFunctionDef,
|
|
34
|
+
ast.ClassDef,
|
|
35
|
+
ast.For,
|
|
36
|
+
ast.While,
|
|
37
|
+
ast.If,
|
|
38
|
+
ast.Assign,
|
|
39
|
+
ast.Expr,
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
for node in ast.walk(tree)
|
|
43
|
+
)
|
|
44
|
+
return has_logic
|
|
45
|
+
except SyntaxError:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def extract_latest_python_code(messages):
|
|
50
|
+
"""Извлечь последний реальный блок Python-кода из ответов ассистента."""
|
|
51
|
+
for message in reversed(messages):
|
|
52
|
+
if message.get("role") != "assistant":
|
|
53
|
+
continue
|
|
54
|
+
content = str(message.get("content") or "")
|
|
55
|
+
|
|
56
|
+
# 1. Ищем блоки в тройных кавычках ``` ... ```
|
|
57
|
+
blocks = list(re.finditer(r"```(?:[a-zA-Z0-9_-]+)?\r?\n([\s\S]*?)```", content))
|
|
58
|
+
for match in reversed(blocks):
|
|
59
|
+
candidate = match.group(1).strip()
|
|
60
|
+
if is_valid_python(candidate):
|
|
61
|
+
return candidate
|
|
62
|
+
|
|
63
|
+
# 2. Если тройных кавычек нет, ищем непрерывный блок кода по ключевым словам
|
|
64
|
+
lines = content.splitlines()
|
|
65
|
+
code_candidates = []
|
|
66
|
+
current_chunk = []
|
|
67
|
+
for line in lines:
|
|
68
|
+
if re.match(
|
|
69
|
+
r"^\s*(?:def\s|import\s|from\s|class\s|if\s|for\s|while\s|try:|#|print\(|input\()",
|
|
70
|
+
line,
|
|
71
|
+
) or (
|
|
72
|
+
current_chunk
|
|
73
|
+
and (
|
|
74
|
+
line.startswith(" ")
|
|
75
|
+
or line.startswith("\t")
|
|
76
|
+
or line.strip() == ""
|
|
77
|
+
or line.strip().startswith("#")
|
|
78
|
+
)
|
|
79
|
+
):
|
|
80
|
+
current_chunk.append(line)
|
|
81
|
+
else:
|
|
82
|
+
if current_chunk:
|
|
83
|
+
chunk_text = "\n".join(current_chunk).strip()
|
|
84
|
+
if is_valid_python(chunk_text):
|
|
85
|
+
code_candidates.append(chunk_text)
|
|
86
|
+
current_chunk = []
|
|
87
|
+
if current_chunk:
|
|
88
|
+
chunk_text = "\n".join(current_chunk).strip()
|
|
89
|
+
if is_valid_python(chunk_text):
|
|
90
|
+
code_candidates.append(chunk_text)
|
|
91
|
+
|
|
92
|
+
if code_candidates:
|
|
93
|
+
return max(code_candidates, key=len)
|
|
94
|
+
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run_interactive_python_test(code: str) -> tuple[int, str]:
|
|
99
|
+
"""Запустить интерактивное тестирование Python-кода в отдельном окне CMD."""
|
|
100
|
+
temp_dir = Path(tempfile.mkdtemp(prefix="w1x-testpy-"))
|
|
101
|
+
script_file = temp_dir / "user_script.py"
|
|
102
|
+
runner_file = temp_dir / "runner.py"
|
|
103
|
+
log_file = temp_dir / "session_log.txt"
|
|
104
|
+
exit_code_file = temp_dir / "exit_code.txt"
|
|
105
|
+
|
|
106
|
+
script_file.write_text(code, encoding="utf-8")
|
|
107
|
+
|
|
108
|
+
runner_code = """# -*- coding: utf-8 -*-
|
|
109
|
+
import os
|
|
110
|
+
import sys
|
|
111
|
+
import traceback
|
|
112
|
+
import builtins
|
|
113
|
+
|
|
114
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
115
|
+
log_file_path = os.path.join(base_dir, "session_log.txt")
|
|
116
|
+
exit_code_path = os.path.join(base_dir, "exit_code.txt")
|
|
117
|
+
script_file_path = os.path.join(base_dir, "user_script.py")
|
|
118
|
+
|
|
119
|
+
# Инициализируем файлы по умолчанию
|
|
120
|
+
try:
|
|
121
|
+
with open(exit_code_path, "w", encoding="utf-8") as f:
|
|
122
|
+
f.write("1")
|
|
123
|
+
with open(log_file_path, "w", encoding="utf-8") as f:
|
|
124
|
+
f.write("")
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TeeStream:
|
|
130
|
+
def __init__(self, original, collector):
|
|
131
|
+
self.original = original
|
|
132
|
+
self.collector = collector
|
|
133
|
+
|
|
134
|
+
def write(self, data):
|
|
135
|
+
try:
|
|
136
|
+
self.original.write(data)
|
|
137
|
+
self.original.flush()
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
self.collector.append(data)
|
|
141
|
+
|
|
142
|
+
def flush(self):
|
|
143
|
+
try:
|
|
144
|
+
self.original.flush()
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
def isatty(self):
|
|
149
|
+
return getattr(self.original, "isatty", lambda: True)()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def clear_input_buffer():
|
|
153
|
+
if os.name == "nt":
|
|
154
|
+
try:
|
|
155
|
+
import msvcrt
|
|
156
|
+
while msvcrt.kbhit():
|
|
157
|
+
msvcrt.getch()
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main():
|
|
163
|
+
if os.name == "nt":
|
|
164
|
+
os.system("chcp 65001 >nul 2>&1")
|
|
165
|
+
os.system("title W1x — Интерактивное тестирование Python")
|
|
166
|
+
|
|
167
|
+
run_count = 0
|
|
168
|
+
all_sessions_log = []
|
|
169
|
+
final_exit_code = 0
|
|
170
|
+
|
|
171
|
+
with open(script_file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
172
|
+
code_text = f.read()
|
|
173
|
+
|
|
174
|
+
orig_input = builtins.input
|
|
175
|
+
|
|
176
|
+
while True:
|
|
177
|
+
run_count += 1
|
|
178
|
+
if run_count == 1:
|
|
179
|
+
if os.name == "nt":
|
|
180
|
+
os.system("cls")
|
|
181
|
+
else:
|
|
182
|
+
os.system("clear")
|
|
183
|
+
|
|
184
|
+
print("=" * 64)
|
|
185
|
+
print(f" [W1x Chat] Интерактивное тестирование (Запуск #{run_count})")
|
|
186
|
+
print("=" * 64 + "\\n")
|
|
187
|
+
|
|
188
|
+
session_log = []
|
|
189
|
+
inputs_called = 0
|
|
190
|
+
|
|
191
|
+
def logged_input(prompt=""):
|
|
192
|
+
nonlocal inputs_called
|
|
193
|
+
inputs_called += 1
|
|
194
|
+
val = orig_input(prompt)
|
|
195
|
+
session_log.append(str(val) + "\\n")
|
|
196
|
+
return val
|
|
197
|
+
|
|
198
|
+
builtins.input = logged_input
|
|
199
|
+
tee_out = TeeStream(sys.__stdout__, session_log)
|
|
200
|
+
tee_err = TeeStream(sys.__stderr__, session_log)
|
|
201
|
+
|
|
202
|
+
sys.stdout = tee_out
|
|
203
|
+
sys.stderr = tee_err
|
|
204
|
+
exit_code = 0
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
global_scope = {
|
|
208
|
+
"__name__": "__main__",
|
|
209
|
+
"__file__": script_file_path,
|
|
210
|
+
"__doc__": None,
|
|
211
|
+
}
|
|
212
|
+
exec(compile(code_text, script_file_path, "exec"), global_scope)
|
|
213
|
+
|
|
214
|
+
# Если модель только объявила def main() / calculator() и забыла вызвать внизу
|
|
215
|
+
if not session_log and not inputs_called:
|
|
216
|
+
entry_points = [
|
|
217
|
+
"main",
|
|
218
|
+
"calculator",
|
|
219
|
+
"simple_calculator",
|
|
220
|
+
"calc",
|
|
221
|
+
"run",
|
|
222
|
+
"app",
|
|
223
|
+
"start",
|
|
224
|
+
"cli",
|
|
225
|
+
]
|
|
226
|
+
for ep in entry_points:
|
|
227
|
+
if ep in global_scope and callable(global_scope[ep]):
|
|
228
|
+
global_scope[ep]()
|
|
229
|
+
break
|
|
230
|
+
except SystemExit as exc:
|
|
231
|
+
exit_code = exc.code if isinstance(exc.code, int) else 0
|
|
232
|
+
except KeyboardInterrupt:
|
|
233
|
+
exit_code = 130
|
|
234
|
+
print("\\n[Выполнение остановлено пользователем (Ctrl+C)]")
|
|
235
|
+
except Exception:
|
|
236
|
+
exit_code = 1
|
|
237
|
+
traceback.print_exc()
|
|
238
|
+
finally:
|
|
239
|
+
sys.stdout = sys.__stdout__
|
|
240
|
+
sys.stderr = sys.__stderr__
|
|
241
|
+
builtins.input = orig_input
|
|
242
|
+
|
|
243
|
+
final_exit_code = exit_code
|
|
244
|
+
run_text = "".join(session_log)
|
|
245
|
+
all_sessions_log.append(f"--- Запуск #{run_count} (код {exit_code}) ---\\n{run_text}")
|
|
246
|
+
|
|
247
|
+
# Сохраняем общий лог после каждого запуска
|
|
248
|
+
with open(log_file_path, "w", encoding="utf-8", errors="replace") as f:
|
|
249
|
+
f.write("\\n\\n".join(all_sessions_log))
|
|
250
|
+
with open(exit_code_path, "w", encoding="utf-8") as f:
|
|
251
|
+
f.write(str(final_exit_code))
|
|
252
|
+
|
|
253
|
+
print("\\n" + "=" * 64)
|
|
254
|
+
if exit_code == 0:
|
|
255
|
+
print(f" ✓ Запуск #{run_count} успешно завершён (код 0).")
|
|
256
|
+
else:
|
|
257
|
+
print(f" ⚠ Запуск #{run_count} завершился с ошибкой (код {exit_code}).")
|
|
258
|
+
|
|
259
|
+
if inputs_called == 0:
|
|
260
|
+
print(" ℹ В этом коде нет функции input(): он выполнился со своими данными.")
|
|
261
|
+
|
|
262
|
+
print(" ─────────────────────────────────────────────────────────────")
|
|
263
|
+
print(" [Enter] или [R] — Запустить этот код ещё раз")
|
|
264
|
+
print(" [Q] — Завершить тестирование и передать лог в чат")
|
|
265
|
+
print("=" * 64)
|
|
266
|
+
|
|
267
|
+
while True:
|
|
268
|
+
clear_input_buffer()
|
|
269
|
+
try:
|
|
270
|
+
choice = orig_input(" Ваш выбор (Enter/R/Q) › ").strip().lower()
|
|
271
|
+
except (KeyboardInterrupt, EOFError):
|
|
272
|
+
choice = "q"
|
|
273
|
+
|
|
274
|
+
if choice in {"q", "quit", "exit", "й", "выход"}:
|
|
275
|
+
return exit_code
|
|
276
|
+
elif choice in {"", "r", "к", "run", "restart"}:
|
|
277
|
+
break
|
|
278
|
+
else:
|
|
279
|
+
print(f" [Неизвестная команда '{choice}'. Введите Q для выхода или Enter для перезапуска]")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
try:
|
|
284
|
+
main()
|
|
285
|
+
except Exception as exc:
|
|
286
|
+
print(f"\\n[Критическая ошибка раннера: {exc}]")
|
|
287
|
+
traceback.print_exc()
|
|
288
|
+
try:
|
|
289
|
+
input("\\n[Нажмите Enter для выхода]...")
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
"""
|
|
293
|
+
runner_file.write_text(runner_code, encoding="utf-8")
|
|
294
|
+
|
|
295
|
+
console.print()
|
|
296
|
+
render_message("Открываю отдельное окно CMD для тестирования…", role="tool")
|
|
297
|
+
console.print("[dim]Окно открыто. Протестируйте программу и завершите её для возврата в чат.[/dim]")
|
|
298
|
+
|
|
299
|
+
creation_flags = getattr(subprocess, "CREATE_NEW_CONSOLE", 0) if os.name == "nt" else 0
|
|
300
|
+
subprocess.run(
|
|
301
|
+
[sys.executable, str(runner_file)],
|
|
302
|
+
creationflags=creation_flags,
|
|
303
|
+
check=False,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
exit_code = 0
|
|
307
|
+
if exit_code_file.exists():
|
|
308
|
+
try:
|
|
309
|
+
exit_code = int(exit_code_file.read_text(encoding="utf-8").strip())
|
|
310
|
+
except Exception:
|
|
311
|
+
exit_code = 0
|
|
312
|
+
|
|
313
|
+
log_content = ""
|
|
314
|
+
if log_file.exists():
|
|
315
|
+
try:
|
|
316
|
+
log_content = log_file.read_text(encoding="utf-8", errors="replace").strip()
|
|
317
|
+
except Exception:
|
|
318
|
+
log_content = ""
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
return exit_code, log_content
|