Tôi đã thực hiện python shell bằng cách sử dụng code.InteractiveConsole
để thực hiện các lệnh cho một dự án. Dưới đây là một phiên bản đơn giản hóa, mặc dù vẫn còn khá dài vì tôi đã viết các ràng buộc cho các phím đặc biệt (như Return, Tab ...) để hoạt động như trong bảng điều khiển python. Có thể thêm nhiều tính năng hơn như tự động hoàn thành với jedi và cú pháp nâng cao cú pháp với pygments.
Ý tưởng chính là tôi sử dụng push()
phương thức của lệnh code.InteractiveConsole
để thực thi các lệnh. Phương thức này trả về True
nếu đó là lệnh một phần, ví dụ def test(x):
, và tôi sử dụng phản hồi này để chèn ...
lời nhắc, nếu không, đầu ra được hiển thị và mới>>>
dấu nhắc được hiển thị. Tôi chụp đầu ra bằng cách sử dụng contextlib.redirect_stdout
.
Ngoài ra, có rất nhiều mã liên quan đến các dấu và so sánh các chỉ mục vì tôi ngăn người dùng chèn văn bản bên trong các lệnh đã thực hiện trước đó. Ý tưởng là tôi đã tạo một dấu 'đầu vào' cho tôi biết nơi bắt đầu của dấu nhắc hoạt động và self.compare('insert', '<', 'input')
tôi có thể biết khi nào người dùng đang cố gắng chèn văn bản lên trên dấu nhắc hoạt động.
import tkinter as tk
import sys
import re
from code import InteractiveConsole
from contextlib import redirect_stderr, redirect_stdout
from io import StringIO
class History(list):
def __getitem__(self, index):
try:
return list.__getitem__(self, index)
except IndexError:
return
class TextConsole(tk.Text):
def __init__(self, master, **kw):
kw.setdefault('width', 50)
kw.setdefault('wrap', 'word')
kw.setdefault('prompt1', '>>> ')
kw.setdefault('prompt2', '... ')
banner = kw.pop('banner', 'Python %s\n' % sys.version)
self._prompt1 = kw.pop('prompt1')
self._prompt2 = kw.pop('prompt2')
tk.Text.__init__(self, master, **kw)
# --- history
self.history = History()
self._hist_item = 0
self._hist_match = ''
# --- initialization
self._console = InteractiveConsole() # python console to execute commands
self.insert('end', banner, 'banner')
self.prompt()
self.mark_set('input', 'insert')
self.mark_gravity('input', 'left')
# --- bindings
self.bind('<Control-Return>', self.on_ctrl_return)
self.bind('<Shift-Return>', self.on_shift_return)
self.bind('<KeyPress>', self.on_key_press)
self.bind('<KeyRelease>', self.on_key_release)
self.bind('<Tab>', self.on_tab)
self.bind('<Down>', self.on_down)
self.bind('<Up>', self.on_up)
self.bind('<Return>', self.on_return)
self.bind('<BackSpace>', self.on_backspace)
self.bind('<Control-c>', self.on_ctrl_c)
self.bind('<<Paste>>', self.on_paste)
def on_ctrl_c(self, event):
"""Copy selected code, removing prompts first"""
sel = self.tag_ranges('sel')
if sel:
txt = self.get('sel.first', 'sel.last').splitlines()
lines = []
for i, line in enumerate(txt):
if line.startswith(self._prompt1):
lines.append(line[len(self._prompt1):])
elif line.startswith(self._prompt2):
lines.append(line[len(self._prompt2):])
else:
lines.append(line)
self.clipboard_clear()
self.clipboard_append('\n'.join(lines))
return 'break'
def on_paste(self, event):
"""Paste commands"""
if self.compare('insert', '<', 'input'):
return "break"
sel = self.tag_ranges('sel')
if sel:
self.delete('sel.first', 'sel.last')
txt = self.clipboard_get()
self.insert("insert", txt)
self.insert_cmd(self.get("input", "end"))
return 'break'
def prompt(self, result=False):
"""Insert a prompt"""
if result:
self.insert('end', self._prompt2, 'prompt')
else:
self.insert('end', self._prompt1, 'prompt')
self.mark_set('input', 'end-1c')
def on_key_press(self, event):
"""Prevent text insertion in command history"""
if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']:
self._hist_item = len(self.history)
self.mark_set('insert', 'input lineend')
if not event.char.isalnum():
return 'break'
def on_key_release(self, event):
"""Reset history scrolling"""
if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']:
self._hist_item = len(self.history)
return 'break'
def on_up(self, event):
"""Handle up arrow key press"""
if self.compare('insert', '<', 'input'):
self.mark_set('insert', 'end')
return 'break'
elif self.index('input linestart') == self.index('insert linestart'):
# navigate history
line = self.get('input', 'insert')
self._hist_match = line
hist_item = self._hist_item
self._hist_item -= 1
item = self.history[self._hist_item]
while self._hist_item >= 0 and not item.startswith(line):
self._hist_item -= 1
item = self.history[self._hist_item]
if self._hist_item >= 0:
index = self.index('insert')
self.insert_cmd(item)
self.mark_set('insert', index)
else:
self._hist_item = hist_item
return 'break'
def on_down(self, event):
"""Handle down arrow key press"""
if self.compare('insert', '<', 'input'):
self.mark_set('insert', 'end')
return 'break'
elif self.compare('insert lineend', '==', 'end-1c'):
# navigate history
line = self._hist_match
self._hist_item += 1
item = self.history[self._hist_item]
while item is not None and not item.startswith(line):
self._hist_item += 1
item = self.history[self._hist_item]
if item is not None:
self.insert_cmd(item)
self.mark_set('insert', 'input+%ic' % len(self._hist_match))
else:
self._hist_item = len(self.history)
self.delete('input', 'end')
self.insert('insert', line)
return 'break'
def on_tab(self, event):
"""Handle tab key press"""
if self.compare('insert', '<', 'input'):
self.mark_set('insert', 'input lineend')
return "break"
# indent code
sel = self.tag_ranges('sel')
if sel:
start = str(self.index('sel.first'))
end = str(self.index('sel.last'))
start_line = int(start.split('.')[0])
end_line = int(end.split('.')[0]) + 1
for line in range(start_line, end_line):
self.insert('%i.0' % line, ' ')
else:
txt = self.get('insert-1c')
if not txt.isalnum() and txt != '.':
self.insert('insert', ' ')
return "break"
def on_shift_return(self, event):
"""Handle Shift+Return key press"""
if self.compare('insert', '<', 'input'):
self.mark_set('insert', 'input lineend')
return 'break'
else: # execute commands
self.mark_set('insert', 'end')
self.insert('insert', '\n')
self.insert('insert', self._prompt2, 'prompt')
self.eval_current(True)
def on_return(self, event=None):
"""Handle Return key press"""
if self.compare('insert', '<', 'input'):
self.mark_set('insert', 'input lineend')
return 'break'
else:
self.eval_current(True)
self.see('end')
return 'break'
def on_ctrl_return(self, event=None):
"""Handle Ctrl+Return key press"""
self.insert('insert', '\n' + self._prompt2, 'prompt')
return 'break'
def on_backspace(self, event):
"""Handle delete key press"""
if self.compare('insert', '<=', 'input'):
self.mark_set('insert', 'input lineend')
return 'break'
sel = self.tag_ranges('sel')
if sel:
self.delete('sel.first', 'sel.last')
else:
linestart = self.get('insert linestart', 'insert')
if re.search(r' $', linestart):
self.delete('insert-4c', 'insert')
else:
self.delete('insert-1c')
return 'break'
def insert_cmd(self, cmd):
"""Insert lines of code, adding prompts"""
input_index = self.index('input')
self.delete('input', 'end')
lines = cmd.splitlines()
if lines:
indent = len(re.search(r'^( )*', lines[0]).group())
self.insert('insert', lines[0][indent:])
for line in lines[1:]:
line = line[indent:]
self.insert('insert', '\n')
self.prompt(True)
self.insert('insert', line)
self.mark_set('input', input_index)
self.see('end')
def eval_current(self, auto_indent=False):
"""Evaluate code"""
index = self.index('input')
lines = self.get('input', 'insert lineend').splitlines() # commands to execute
self.mark_set('insert', 'insert lineend')
if lines: # there is code to execute
# remove prompts
lines = [lines[0].rstrip()] + [line[len(self._prompt2):].rstrip() for line in lines[1:]]
for i, l in enumerate(lines):
if l.endswith('?'):
lines[i] = 'help(%s)' % l[:-1]
cmds = '\n'.join(lines)
self.insert('insert', '\n')
out = StringIO() # command output
err = StringIO() # command error traceback
with redirect_stderr(err): # redirect error traceback to err
with redirect_stdout(out): # redirect command output
# execute commands in interactive console
res = self._console.push(cmds)
# if res is True, this is a partial command, e.g. 'def test():' and we need to wait for the rest of the code
errors = err.getvalue()
if errors: # there were errors during the execution
self.insert('end', errors) # display the traceback
self.mark_set('input', 'end')
self.see('end')
self.prompt() # insert new prompt
else:
output = out.getvalue() # get output
if output:
self.insert('end', output, 'output')
self.mark_set('input', 'end')
self.see('end')
if not res and self.compare('insert linestart', '>', 'insert'):
self.insert('insert', '\n')
self.prompt(res)
if auto_indent and lines:
# insert indentation similar to previous lines
indent = re.search(r'^( )*', lines[-1]).group()
line = lines[-1].strip()
if line and line[-1] == ':':
indent = indent + ' '
self.insert('insert', indent)
self.see('end')
if res:
self.mark_set('input', index)
self._console.resetbuffer() # clear buffer since the whole command will be retrieved from the text widget
elif lines:
self.history.append(lines) # add commands to history
self._hist_item = len(self.history)
out.close()
err.close()
else:
self.insert('insert', '\n')
self.prompt()
if __name__ == '__main__':
root = tk.Tk()
console = TextConsole(root)
console.pack(fill='both', expand=True)
root.mainloop()