decpython 0.1.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.
decpython/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ decpython - 跨平台、支持 Web 通信的 Python 终端工具
3
+ """
4
+
5
+ from decpython.core import DecPython
6
+
7
+ __version__ = '1.0.0'
8
+ __all__ = ["DecPython"]
decpython/core.py ADDED
@@ -0,0 +1,250 @@
1
+ """
2
+ 核心执行器与 DecPython 终端类。
3
+ """
4
+
5
+ import ast
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ import threading
10
+ import traceback
11
+ import webbrowser
12
+ from io import StringIO
13
+ from subprocess import PIPE, Popen
14
+ from typing import Optional
15
+
16
+ # 子进程运行器:从 stdin 读代码(直到 __DECPYTHON_END_INPUT__),执行后向 stdout 写 __DECPYTHON_RESULT__<json>__DECPYTHON_END_RESULT__
17
+ _RUNNER_SOURCE = r'''
18
+ import ast
19
+ import json
20
+ import sys
21
+ import traceback
22
+
23
+ namespace = {"__name__": "__decpython__", "__builtins__": __import__("builtins").__dict__}
24
+ DELIM = "__DECPYTHON_END_INPUT__"
25
+ OUT_PREFIX = "__DECPYTHON_RESULT__"
26
+ OUT_SUFFIX = "__DECPYTHON_END_RESULT__"
27
+
28
+ def run(code):
29
+ code = code.strip()
30
+ if not code:
31
+ return "", ""
32
+ try:
33
+ tree = ast.parse(code)
34
+ except SyntaxError as e:
35
+ return "", "".join(traceback.format_exception_only(type(e), e))
36
+ if not tree.body:
37
+ return "", ""
38
+ try:
39
+ last = tree.body[-1]
40
+ if isinstance(last, ast.Expr):
41
+ if len(tree.body) == 1:
42
+ result = eval(compile(ast.Expression(body=last.value), "<decpython>", "eval"), namespace)
43
+ else:
44
+ exec(compile(ast.Module(body=tree.body[:-1], type_ignores=[]), "<decpython>", "exec"), namespace)
45
+ result = eval(compile(ast.Expression(body=last.value), "<decpython>", "eval"), namespace)
46
+ return (str(result) if result is not None else ""), ""
47
+ else:
48
+ exec(compile(tree, "<decpython>", "exec"), namespace)
49
+ return "", ""
50
+ except Exception as e:
51
+ return "", "".join(traceback.format_exception(type(e), e, e.__traceback__))
52
+
53
+ while True:
54
+ lines = []
55
+ while True:
56
+ line = sys.stdin.readline()
57
+ if not line:
58
+ sys.exit(0)
59
+ if line.rstrip() == DELIM:
60
+ break
61
+ lines.append(line)
62
+ code = "".join(lines)
63
+ result, err = run(code)
64
+ out = OUT_PREFIX + json.dumps({"result": result, "error": err}, ensure_ascii=False) + OUT_SUFFIX + "\n"
65
+ sys.stdout.write(out)
66
+ sys.stdout.flush()
67
+ '''
68
+
69
+
70
+ class Executor:
71
+ """在独立命名空间中执行代码,支持多行与最后表达式求值。"""
72
+
73
+ def __init__(self) -> None:
74
+ self._namespace: dict = {}
75
+ self._init_builtins()
76
+
77
+ def _init_builtins(self) -> None:
78
+ import builtins
79
+ self._namespace = {
80
+ "__name__": "__decpython__",
81
+ "__builtins__": builtins.__dict__,
82
+ }
83
+
84
+ def run(self, code: str) -> tuple[str, str]:
85
+ """
86
+ 执行代码,返回 (result, error)。
87
+ result 为最后一次表达式结果的 str,无表达式或仅为语句时为空字符串;
88
+ error 为执行出错时的错误信息,否则为空字符串。
89
+ """
90
+ code = code.strip()
91
+ if not code:
92
+ return "", ""
93
+
94
+ try:
95
+ tree = ast.parse(code)
96
+ except SyntaxError as e:
97
+ return "", "".join(traceback.format_exception_only(type(e), e))
98
+
99
+ if not tree.body:
100
+ return "", ""
101
+
102
+ try:
103
+ last = tree.body[-1]
104
+ if isinstance(last, ast.Expr):
105
+ if len(tree.body) == 1:
106
+ result = self._eval_expr(last.value)
107
+ else:
108
+ block = ast.Module(body=tree.body[:-1], type_ignores=[])
109
+ exec(compile(block, "<decpython>", "exec"), self._namespace)
110
+ result = self._eval_expr(last.value)
111
+ return self._repr_result(result), ""
112
+ else:
113
+ exec(compile(tree, "<decpython>", "exec"), self._namespace)
114
+ return "", ""
115
+ except Exception as e:
116
+ return "", "".join(traceback.format_exception(type(e), e, e.__traceback__))
117
+
118
+ def _eval_expr(self, node: ast.expr): # noqa: ANN001
119
+ """在命名空间中求值单个表达式节点。"""
120
+ expr_ast = ast.Expression(body=node)
121
+ return eval(compile(expr_ast, "<decpython>", "eval"), self._namespace)
122
+
123
+ @staticmethod
124
+ def _repr_result(value) -> str: # noqa: ANN001
125
+ """将执行结果转为字符串返回。"""
126
+ if value is None:
127
+ return ""
128
+ return str(value)
129
+
130
+ def reset(self) -> None:
131
+ """清空命名空间(保留 builtins)。"""
132
+ self._init_builtins()
133
+
134
+
135
+ class SubprocessExecutor:
136
+ """使用指定终端命令(如 python / python3.12)在子进程中执行代码,保持独立命名空间。"""
137
+
138
+ _DELIM = "__DECPYTHON_END_INPUT__"
139
+ _OUT_PREFIX = "__DECPYTHON_RESULT__"
140
+ _OUT_SUFFIX = "__DECPYTHON_END_RESULT__"
141
+
142
+ def __init__(self, python_cmd: str) -> None:
143
+ self._python_cmd = python_cmd
144
+ self._process: Optional[Popen] = None
145
+ self._runner_path: Optional[str] = None
146
+ self._start()
147
+
148
+ def _start(self) -> None:
149
+ fd, self._runner_path = tempfile.mkstemp(suffix=".py", prefix="decpython_runner_")
150
+ try:
151
+ os.write(fd, _RUNNER_SOURCE.encode("utf-8"))
152
+ finally:
153
+ os.close(fd)
154
+ self._process = Popen(
155
+ [self._python_cmd, self._runner_path],
156
+ stdin=PIPE,
157
+ stdout=PIPE,
158
+ stderr=PIPE,
159
+ text=True,
160
+ bufsize=1,
161
+ )
162
+
163
+ def run(self, code: str) -> tuple[str, str]:
164
+ if self._process is None or self._process.poll() is not None:
165
+ return "", "子进程已退出"
166
+ try:
167
+ self._process.stdin.write(code)
168
+ self._process.stdin.write("\n" + self._DELIM + "\n")
169
+ self._process.stdin.flush()
170
+ except Exception as e:
171
+ return "", str(e)
172
+ while True:
173
+ line = self._process.stdout.readline()
174
+ if not line:
175
+ return "", "子进程意外结束"
176
+ if self._OUT_SUFFIX in line:
177
+ idx = line.find(self._OUT_PREFIX)
178
+ end = line.find(self._OUT_SUFFIX)
179
+ if idx != -1 and end != -1:
180
+ import json
181
+ raw = line[idx + len(self._OUT_PREFIX) : end]
182
+ try:
183
+ data = json.loads(raw)
184
+ return data.get("result", ""), data.get("error", "")
185
+ except Exception:
186
+ return raw, ""
187
+ break
188
+ return "", ""
189
+
190
+ def close(self) -> None:
191
+ if self._process and self._process.poll() is None:
192
+ self._process.terminate()
193
+ self._process.wait()
194
+ self._process = None
195
+ if self._runner_path and os.path.exists(self._runner_path):
196
+ try:
197
+ os.unlink(self._runner_path)
198
+ except OSError:
199
+ pass
200
+ self._runner_path = None
201
+
202
+
203
+ class DecPython:
204
+ """
205
+ 跨平台、支持 Web 通信的 Python 终端。
206
+ - python: 终端中用于执行 Python 的命令,如 'python' 或 'python3.12'。
207
+ - gui=False:仅程序化调用 send(code) 获取 str 结果。
208
+ - gui=True:启动类 Jupyter 的 Web 界面,实时查看输入与输出(含通过 send 发送的内容与结果)。
209
+ """
210
+
211
+ def __init__(self, gui: bool = False, python: str = "python") -> None:
212
+ self._executor: SubprocessExecutor = SubprocessExecutor(python)
213
+ self._gui = gui
214
+ self._gui_server: Optional[object] = None
215
+ self._closed = False
216
+ self._last_result, self._last_error = "", ""
217
+ if gui:
218
+ from decpython.gui.server import start_gui_server
219
+ self._gui_server = start_gui_server(self)
220
+ threading.Timer(0.8, self._open_browser).start()
221
+
222
+ def _open_browser(self) -> None:
223
+ if self._gui_server and hasattr(self._gui_server, "url"):
224
+ webbrowser.open(self._gui_server.url)
225
+
226
+ def send(self, code: str) -> str:
227
+ """执行代码,返回 str(执行结果);无表达式或仅语句时返回空字符串;出错时返回错误信息字符串。"""
228
+ if self._closed:
229
+ raise RuntimeError("DecPython terminal is already closed")
230
+ result, error = self._executor.run(code)
231
+ self._last_result, self._last_error = result, error
232
+ if self._gui_server is not None and hasattr(self._gui_server, "append_cell"):
233
+ self._gui_server.append_cell(code, result, error)
234
+ return result if not error else error
235
+
236
+ def close(self) -> None:
237
+ """关闭终端;若曾启动 GUI 则停止 Web 服务,并结束子进程。"""
238
+ if self._closed:
239
+ return
240
+ self._closed = True
241
+ if self._gui_server is not None and hasattr(self._gui_server, "shutdown"):
242
+ self._gui_server.shutdown()
243
+ if hasattr(self._executor, "close"):
244
+ self._executor.close()
245
+
246
+ def __enter__(self) -> "DecPython":
247
+ return self
248
+
249
+ def __exit__(self, *args: object) -> None:
250
+ self.close()
@@ -0,0 +1 @@
1
+ # GUI 模块:Web 界面与本地服务
@@ -0,0 +1,395 @@
1
+ """
2
+ 本地 Web 服务:提供 Jupyter 风格的页面、/run 执行接口与 SSE 推送(使 send() 与页面同步)。
3
+ 仅使用标准库,跨平台。
4
+ """
5
+
6
+ import json
7
+ import queue
8
+ import socket
9
+ import threading
10
+ from http.server import HTTPServer, BaseHTTPRequestHandler, ThreadingHTTPServer
11
+
12
+ # 内嵌的 HTML 页面(科技感界面:深色终端风、霓虹高亮、玻璃拟态、流畅动效)
13
+ INDEX_HTML = """<!DOCTYPE html>
14
+ <html lang="zh-CN">
15
+ <head>
16
+ <meta charset="UTF-8">
17
+ <meta name="viewport" content="width=device-width, initial-scale=1">
18
+ <title>DecPython</title>
19
+ <link rel="preconnect" href="https://fonts.googleapis.com">
20
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
22
+ <style>
23
+ :root {
24
+ --bg: #0a0e14;
25
+ --bg-panel: rgba(12, 18, 28, 0.85);
26
+ --surface: rgba(18, 26, 38, 0.9);
27
+ --border: rgba(0, 212, 255, 0.25);
28
+ --text: #e6e6e6;
29
+ --text-dim: #7a8a99;
30
+ --accent: #00d4ff;
31
+ --accent-glow: rgba(0, 212, 255, 0.4);
32
+ --success: #00ff9f;
33
+ --success-glow: rgba(0, 255, 159, 0.25);
34
+ --error: #ff3366;
35
+ --error-glow: rgba(255, 51, 102, 0.25);
36
+ --radius: 10px;
37
+ --font: 'JetBrains Mono', 'Consolas', monospace;
38
+ }
39
+ * { box-sizing: border-box; }
40
+ body {
41
+ font-family: var(--font);
42
+ font-size: 14px;
43
+ background: var(--bg);
44
+ color: var(--text);
45
+ margin: 0;
46
+ min-height: 100vh;
47
+ overflow-x: hidden;
48
+ }
49
+ body::before {
50
+ content: '';
51
+ position: fixed;
52
+ inset: 0;
53
+ background-image:
54
+ linear-gradient(rgba(0, 212, 255, 0.03) 1px, transparent 1px),
55
+ linear-gradient(90deg, rgba(0, 212, 255, 0.03) 1px, transparent 1px);
56
+ background-size: 24px 24px;
57
+ pointer-events: none;
58
+ z-index: 0;
59
+ }
60
+ .app {
61
+ position: relative;
62
+ z-index: 1;
63
+ max-width: 900px;
64
+ margin: 0 auto;
65
+ padding: 24px 20px 40px;
66
+ }
67
+ .header {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 12px;
71
+ margin-bottom: 24px;
72
+ padding-bottom: 16px;
73
+ border-bottom: 1px solid var(--border);
74
+ }
75
+ .logo {
76
+ font-weight: 600;
77
+ font-size: 1.35rem;
78
+ letter-spacing: 0.08em;
79
+ color: var(--accent);
80
+ text-shadow: 0 0 20px var(--accent-glow);
81
+ }
82
+ .logo span { color: var(--text-dim); font-weight: 400; }
83
+ .live-dot {
84
+ width: 8px;
85
+ height: 8px;
86
+ border-radius: 50%;
87
+ background: var(--success);
88
+ box-shadow: 0 0 12px var(--success-glow);
89
+ animation: pulse 2s ease-in-out infinite;
90
+ }
91
+ .live-label { font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.1em; }
92
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
93
+
94
+ .input-panel {
95
+ background: var(--bg-panel);
96
+ border: 1px solid var(--border);
97
+ border-radius: var(--radius);
98
+ padding: 4px 4px 4px 16px;
99
+ margin-bottom: 24px;
100
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255,255,255,0.03);
101
+ transition: box-shadow 0.2s, border-color 0.2s;
102
+ }
103
+ .input-panel:focus-within {
104
+ border-color: rgba(0, 212, 255, 0.5);
105
+ box-shadow: 0 0 0 1px rgba(0, 212, 255, 0.15), 0 4px 24px rgba(0, 0, 0, 0.3);
106
+ }
107
+ .input-wrap {
108
+ display: flex;
109
+ gap: 12px;
110
+ align-items: flex-end;
111
+ }
112
+ .prompt {
113
+ color: var(--accent);
114
+ font-size: 0.9rem;
115
+ padding-bottom: 12px;
116
+ flex-shrink: 0;
117
+ text-shadow: 0 0 8px var(--accent-glow);
118
+ }
119
+ textarea {
120
+ flex: 1;
121
+ min-height: 100px;
122
+ background: transparent;
123
+ color: var(--text);
124
+ border: none;
125
+ padding: 10px 0 12px;
126
+ font-family: var(--font);
127
+ font-size: 13px;
128
+ line-height: 1.55;
129
+ resize: vertical;
130
+ outline: none;
131
+ }
132
+ textarea::placeholder { color: var(--text-dim); }
133
+ .run-wrap { flex-shrink: 0; }
134
+ .run-btn {
135
+ background: linear-gradient(135deg, rgba(0, 212, 255, 0.2), rgba(0, 212, 255, 0.08));
136
+ color: var(--accent);
137
+ border: 1px solid rgba(0, 212, 255, 0.4);
138
+ padding: 10px 20px;
139
+ border-radius: 8px;
140
+ font-family: var(--font);
141
+ font-size: 13px;
142
+ font-weight: 500;
143
+ cursor: pointer;
144
+ transition: all 0.2s;
145
+ box-shadow: 0 0 16px rgba(0, 212, 255, 0.1);
146
+ }
147
+ .run-btn:hover {
148
+ background: linear-gradient(135deg, rgba(0, 212, 255, 0.3), rgba(0, 212, 255, 0.15));
149
+ box-shadow: 0 0 24px var(--accent-glow);
150
+ border-color: var(--accent);
151
+ }
152
+ .run-btn:active { transform: scale(0.98); }
153
+
154
+ .outputs-title {
155
+ font-size: 0.7rem;
156
+ text-transform: uppercase;
157
+ letter-spacing: 0.12em;
158
+ color: var(--text-dim);
159
+ margin-bottom: 12px;
160
+ }
161
+ #outputs {
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: 16px;
165
+ }
166
+ .cell {
167
+ animation: cellIn 0.35s ease-out;
168
+ border-radius: var(--radius);
169
+ overflow: hidden;
170
+ border: 1px solid var(--border);
171
+ background: var(--surface);
172
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
173
+ }
174
+ @keyframes cellIn {
175
+ from { opacity: 0; transform: translateY(-8px); }
176
+ to { opacity: 1; transform: translateY(0); }
177
+ }
178
+ .cell-in {
179
+ padding: 12px 16px;
180
+ border-left: 3px solid var(--accent);
181
+ background: rgba(0, 212, 255, 0.04);
182
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
183
+ }
184
+ .cell-in pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.5; }
185
+ .cell-out {
186
+ margin: 0;
187
+ padding: 12px 16px;
188
+ min-height: 24px;
189
+ border-left: 3px solid transparent;
190
+ }
191
+ .cell-out.result {
192
+ background: rgba(0, 255, 159, 0.06);
193
+ border-left-color: var(--success);
194
+ box-shadow: inset 0 0 20px var(--success-glow);
195
+ }
196
+ .cell-out.error {
197
+ background: rgba(255, 51, 102, 0.06);
198
+ border-left-color: var(--error);
199
+ box-shadow: inset 0 0 20px var(--error-glow);
200
+ }
201
+ .cell-out pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.5; }
202
+ .cell-out.error pre { color: #ff6b8a; }
203
+ </style>
204
+ </head>
205
+ <body>
206
+ <div class="app">
207
+ <header class="header">
208
+ <div class="logo">Dec<span>Python</span></div>
209
+ <div class="live-dot" title="已连接"></div>
210
+ <span class="live-label">Live</span>
211
+ </header>
212
+ <div class="input-panel">
213
+ <div class="input-wrap">
214
+ <span class="prompt">&gt;&gt;&gt;</span>
215
+ <textarea id="code" placeholder="# 输入代码,Shift+Enter 运行" spellcheck="false"></textarea>
216
+ <div class="run-wrap"><button type="button" class="run-btn" id="run">Run</button></div>
217
+ </div>
218
+ </div>
219
+ <div class="outputs-title">Output</div>
220
+ <div id="outputs"></div>
221
+ </div>
222
+ <script>
223
+ const codeEl = document.getElementById('code');
224
+ const runBtn = document.getElementById('run');
225
+ const outputsEl = document.getElementById('outputs');
226
+
227
+ function addCell(input, result, error) {
228
+ const cell = document.createElement('div');
229
+ cell.className = 'cell';
230
+ const text = error ? error : (result || '');
231
+ const isError = !!error;
232
+ cell.innerHTML = '<div class="cell-in"><pre>' + escapeHtml(input) + '</pre></div><div class="cell-out ' + (isError ? 'error' : 'result') + '"><pre>' + escapeHtml(text) + '</pre></div>';
233
+ outputsEl.appendChild(cell);
234
+ outputsEl.scrollTop = outputsEl.scrollHeight;
235
+ }
236
+ function escapeHtml(s) {
237
+ if (s == null) return '';
238
+ const div = document.createElement('div');
239
+ div.textContent = s;
240
+ return div.innerHTML;
241
+ }
242
+ // SSE:同步历史与后续通过 send() 或页面运行产生的新 cell
243
+ const evtSource = new EventSource('/stream');
244
+ evtSource.addEventListener('history', function(e) {
245
+ const cells = JSON.parse(e.data || '[]');
246
+ cells.forEach(function(c) { addCell(c.code, c.result, c.error); });
247
+ });
248
+ evtSource.addEventListener('cell', function(e) {
249
+ const c = JSON.parse(e.data || '{}');
250
+ addCell(c.code, c.result, c.error);
251
+ });
252
+ evtSource.onerror = function() { evtSource.close(); };
253
+
254
+ async function run() {
255
+ const code = codeEl.value.trim();
256
+ if (!code) return;
257
+ codeEl.value = '';
258
+ try {
259
+ await fetch('/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) });
260
+ } catch (e) {
261
+ addCell(code, null, String(e));
262
+ }
263
+ }
264
+ runBtn.addEventListener('click', run);
265
+ codeEl.addEventListener('keydown', function(e) {
266
+ if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); run(); }
267
+ });
268
+ </script>
269
+ </body>
270
+ </html>
271
+ """
272
+
273
+
274
+ def _find_free_port() -> int:
275
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
276
+ s.bind(("", 0))
277
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
278
+ return s.getsockname()[1]
279
+
280
+
281
+ def _sse_send(wfile, event: str, data: str) -> None:
282
+ wfile.write(f"event: {event}\ndata: {data}\n\n".encode("utf-8"))
283
+ wfile.flush()
284
+
285
+
286
+ class _DecPythonHandler(BaseHTTPRequestHandler):
287
+ decpython_instance = None # 由服务器注入
288
+
289
+ def log_message(self, format: str, *args: object) -> None:
290
+ pass
291
+
292
+ def do_GET(self) -> None:
293
+ if self.path == "/" or self.path == "/index.html":
294
+ self.send_response(200)
295
+ self.send_header("Content-Type", "text/html; charset=utf-8")
296
+ self.end_headers()
297
+ self.wfile.write(INDEX_HTML.encode("utf-8"))
298
+ return
299
+ if self.path == "/stream":
300
+ state = getattr(self.server, "state", None)
301
+ if not state:
302
+ self.send_response(500)
303
+ self.end_headers()
304
+ return
305
+ self.send_response(200)
306
+ self.send_header("Content-Type", "text/event-stream")
307
+ self.send_header("Cache-Control", "no-cache")
308
+ self.send_header("Connection", "keep-alive")
309
+ self.end_headers()
310
+ client_queue = queue.Queue()
311
+ state["queues"].append(client_queue)
312
+ try:
313
+ history_json = json.dumps(state["cells"], ensure_ascii=False)
314
+ _sse_send(self.wfile, "history", history_json)
315
+ while True:
316
+ item = client_queue.get()
317
+ if item is None:
318
+ break
319
+ _sse_send(self.wfile, "cell", json.dumps(item, ensure_ascii=False))
320
+ except (BrokenPipeError, ConnectionResetError, OSError):
321
+ pass
322
+ finally:
323
+ try:
324
+ state["queues"].remove(client_queue)
325
+ except ValueError:
326
+ pass
327
+ return
328
+ self.send_response(404)
329
+ self.end_headers()
330
+
331
+ def do_POST(self) -> None:
332
+ if self.path == "/run":
333
+ length = int(self.headers.get("Content-Length", 0))
334
+ body = self.rfile.read(length).decode("utf-8") if length else "{}"
335
+ try:
336
+ data = json.loads(body)
337
+ code = data.get("code", "")
338
+ except Exception:
339
+ code = ""
340
+ result = ""
341
+ err = ""
342
+ dp = _DecPythonHandler.decpython_instance
343
+ if dp and not getattr(dp, "_closed", True):
344
+ try:
345
+ dp.send(code)
346
+ result = getattr(dp, "_last_result", "")
347
+ err = getattr(dp, "_last_error", "")
348
+ except Exception as e:
349
+ err = str(e)
350
+ else:
351
+ err = "终端已关闭"
352
+ # dp.send() already calls append_cell(); do not append/push again here
353
+ self.send_response(200)
354
+ self.send_header("Content-Type", "application/json; charset=utf-8")
355
+ self.end_headers()
356
+ self.wfile.write(json.dumps({"result": result, "error": err}, ensure_ascii=False).encode("utf-8"))
357
+ return
358
+ self.send_response(404)
359
+ self.end_headers()
360
+
361
+
362
+ class _GUIServer:
363
+ def __init__(self, decpython_instance: object, port: int) -> None:
364
+ self._server = ThreadingHTTPServer(("127.0.0.1", port), _DecPythonHandler)
365
+ self._server.state = {"cells": [], "queues": []}
366
+ _DecPythonHandler.decpython_instance = decpython_instance
367
+ self._decpython = decpython_instance
368
+ self.port = port
369
+ self.url = f"http://127.0.0.1:{port}/"
370
+ self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
371
+ self._thread.start()
372
+
373
+ def append_cell(self, code: str, result: str, error: str) -> None:
374
+ cell = {"code": code, "result": result or "", "error": error or ""}
375
+ self._server.state["cells"].append(cell)
376
+ for q in self._server.state["queues"]:
377
+ try:
378
+ q.put(cell)
379
+ except Exception:
380
+ pass
381
+
382
+ def shutdown(self) -> None:
383
+ if self._server:
384
+ for q in self._server.state.get("queues", []):
385
+ try:
386
+ q.put(None)
387
+ except Exception:
388
+ pass
389
+ self._server.shutdown()
390
+ self._server = None
391
+
392
+
393
+ def start_gui_server(decpython_instance: object) -> _GUIServer:
394
+ port = _find_free_port()
395
+ return _GUIServer(decpython_instance, port)
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.2
2
+ Name: decpython
3
+ Version: 0.1.0
4
+ Summary: 跨平台、支持 Web 通信的 Python 终端工具
5
+ Home-page: https://github.com/your-username/decpython_maskter
6
+ Author: decpython
7
+ Author-email: decrule@outlook.com
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.9.0
18
+ Description-Content-Type: text/markdown
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7; extra == "dev"
21
+ Requires-Dist: ruff>=0.1; extra == "dev"
22
+ Dynamic: author-email
23
+ Dynamic: home-page
24
+ Dynamic: requires-python
25
+
26
+ # DecPython
27
+
28
+ Run Python in a subprocess from your code and get string results, or open a Jupyter-style web terminal. Cross-platform, stdlib only.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install -e .
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from decpython import DecPython
40
+
41
+ dp = DecPython(gui=False, python='python')
42
+ result = dp.send('a = 1\nb = 2\na + b') # -> '3'
43
+ dp.close()
44
+ ```
45
+
46
+ With a web UI: use `gui=True` and optionally `python='python3.12'`. Code and results from `send()` appear in the browser. Press Shift+Enter or click Run in the page.
47
+
48
+ ## API
49
+
50
+ - **`send(code)`** — Run code; returns the last expression as a string (or the error message).
51
+ - **`close()`** — Stop the subprocess and, if used, the web server.
52
+
53
+ ## License
54
+
55
+ MIT
@@ -0,0 +1,8 @@
1
+ decpython/__init__.py,sha256=PgsSpDlvZ34ycErzqzOE9G9MrF1v9hP5EAx3ND2Ncd4,166
2
+ decpython/core.py,sha256=UouGYUyJlTIuVEyOsnI7vdQVQICdLpBQBFGx3X3NgAM,9155
3
+ decpython/gui/__init__.py,sha256=q5u9WEKx4xy-bdEuriETiWYMXADfKnMJ5qE7ryOJcNU,42
4
+ decpython/gui/server.py,sha256=BRMjukVa6JExPLrfpBjWg3gtIUzrnRpNId2CKtAPyqg,13879
5
+ decpython-0.1.0.dist-info/METADATA,sha256=usGq5Knp6kVJMVDqQT2mGRFM3ET3Ue--dhKt5-qIBIg,1638
6
+ decpython-0.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
7
+ decpython-0.1.0.dist-info/top_level.txt,sha256=qOqKKkyP_FcGBUMmD0sJP4f595XX-_xNUNQdJmAkcIY,10
8
+ decpython-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ decpython