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/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