How to embed a Python interpreter in a PyQT widget
I want to be able to bring up an interactive python terminal from my python application. Some, but not all, variables in my program needs to be exposed to the interpreter.
Currently I use a sub-classed and modified QPlainTextEdit
and route all "commands" there to eval
or exec
, and keep track of a separate namespace in a dict. However there got to be a more elegant and robust way! How?
Here is an example doing just what I want, but it is with IPython and pyGTK... http://ipython.scipy.org/moin/Cookbook/EmbeddingInGTK
Below is what I currently have. But there are so many corner cases that I probably missed some. It is very slow, try a large print loop... It got to be a simpler and less bug prone way, ...I hope!!
It is the def runCommand(self)
function that is the key to understanding my problem. I ideally don't want to improve it, I rather want to replace its content with something simpler and smarter.
The functionality of the console.updateNamespace({'myVar1' : app, 'myVar2' : 1234})
statement in "main" is also important.
import sys, os
import traceback
from PyQt4 import QtCore
from PyQt4 import QtGui
class Console(QtGui.QPlainTextEdit):
def __init__(self, prompt='$> ', startup_message='', parent=None):
QtGui.QPlainTextEdit.__init__(self, parent)
self.prompt = prompt
self.history = []
self.namespace = {}
self.construct = []
self.setGeometry(50, 75, 600, 400)
self.setWordWrapMode(QtGui.QTextOption.WrapAnywhere)
self.setUndoRedoEnabled(False)
self.document().setDefaultFont(QtGui.QFont("monospace", 10, QtGui.QFont.Normal))
self.showMessage(startup_message)
def updateNamespace(self, namespace):
self.namespace.update(namespace)
def showMessage(self, message):
self.appendPlainText(message)
self.newPrompt()
def newPrompt(self):
if self.construct:
prompt = '.' * len(self.prompt)
else:
prompt = self.prompt
self.appendPlainText(prompt)
self.moveCursor(QtGui.QTextCursor.End)
def getCommand(self):
doc = self.document()
curr_line = unicode(doc.findBlockByLineNumber(doc.lineCount() - 1).text())
curr_line = curr_line.rstrip()
curr_line = curr_line[len(self.prompt):]
return curr_line
def setCommand(self, command):
if self.getCommand() == command:
return
self.moveCursor(QtGui.QTextCursor.End)
self.moveCursor(QtGui.QTextCursor.StartOfLine, QtGui.QTextCursor.KeepAnchor)
for i in range(len(self.prompt)):
self.moveCursor(QtGui.QTextCursor.Right, QtGui.QTextCursor.KeepAnchor)
self.textCursor().removeSelectedText()
self.textCursor().insertText(command)
self.moveCursor(QtGui.QTextCursor.End)
def getConstruct(self, command):
if self.construct:
prev_command = self.construct[-1]
self.construct.append(command)
if not prev_command and not command:
ret_val = '\n'.join(self.construct)
self.construct = []
return ret_val
else:
return ''
else:
if command and command[-1] == (':'):
self.construct.append(command)
return ''
else:
return command
def getHistory(self):
return self.history
def setHisory(self, history):
self.history = history
def addToHistory(self, command):
if command and (not self.history or self.history[-1] != command):
self.history.append(command)
self.history_index = len(self.history)
def getPrevHistoryEntry(self):
if self.history:
self.history_index = max(0, self.history_index - 1)
return self.history[self.history_index]
return ''
def getNextHistoryEntry(self):
if self.history:
hist_len = len(self.history)
self.history_index = min(hist_len, self.history_index + 1)
if self.history_index < hist_len:
return self.history[self.history_index]
return ''
def getCursorPosition(self):
return self.textCursor().columnNumber() - len(self.prompt)
def setCursorPosition(self, position):
self.moveCursor(QtGui.QTextCursor.StartOfLine)
for i in range(len(self.prompt) + position):
self.moveCursor(QtGui.QTextCursor.Right)
def runCommand(self):
command = self.getCommand()
self.addToHistory(command)
command = self.getConstruct(command)
if command:
tmp_stdout = sys.stdout
class stdoutProxy():
def __init__(self, write_func):
self.write_func = write_func
self.skip = False
def write(self, text):
if not self.skip:
stripped_text = text.rstrip('\n')
self.write_func(stripped_text)
QtCore.QCoreApplication.processEvents()
self.skip = not self.skip
sys.stdout = stdoutProxy(self.appendPlainText)
try:
try:
result = eval(command, self.namespace, self.namespace)
if result != None:
self.appendPlainText(repr(result))
except SyntaxError:
exec command in self.namespace
except SystemExit:
self.close()
except:
traceback_lines = traceback.format_exc().split('\n')
# Remove traceback mentioning this file, and a linebreak
for i in (3,2,1,-1):
traceback_lines.pop(i)
self.appendPlainText('\n'.join(traceback_lines))
sys.stdout = tmp_stdout
self.newPrompt()
def keyPressEvent(self, event):
if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.runCommand()
return
if event.key() == QtCore.Qt.Key_Home:
self.setCursorPosition(0)
return
if event.key() == QtCore.Qt.Key_PageUp:
return
elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Backspace):
if self.getCursorPosition() == 0:
return
elif event.key() == QtCore.Qt.Key_Up:
self.setCommand(self.getPrevHistoryEntry())
return
elif event.key() == QtCore.Qt.Key_Down:
self.setCommand(self.getNextHistoryEntry())
return
elif event.key() == QtCore.Qt.Key_D and event.modifiers() == QtCore.Qt.ControlModifier:
开发者_如何学运维 self.close()
super(Console, self).keyPressEvent(event)
welcome_message = '''
---------------------------------------------------------------
Welcome to a primitive Python interpreter.
---------------------------------------------------------------
'''
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
console = Console(startup_message=welcome_message)
console.updateNamespace({'myVar1' : app, 'myVar2' : 1234})
console.show();
sys.exit(app.exec_())
Bit late I know, but I recommend the code.InteractiveConsole class: http://docs.python.org/py3k/library/code.html#code.InteractiveConsole
You might look into using threads to keep the UI responsive while printing big loops. This would also help keep your tracebacks clean.
Keeping variables in a dict is the way to go – it's what Python itself does internally. As far as exposing “some, but not all” of them, consider just exposing them all. Much easier. If you're concerned about security, beware that you can't reliably hide anything in Python.
As for the hideous cursor/text manipulation: take advantage of the fact that you have a GUI. With a terminal, you just have one “text box”, but in Qt, it might be more appropriate to have a log/result view and a separate command box.
The log view would display the entered commands and results in a read-only textbox.
The command textbox would allow you to enter a command cleanly.
This approach is used in some web frameworks – e.g. via WebError:
First draft of updated version of my code to support IPython 0.13
'''
Created on 18-03-2012
@author: Paweł Jarosz
'''
import os, sys
import atexit
from PySide import QtCore, QtGui
from IPython.zmq.ipkernel import IPKernelApp
from IPython.lib.kernel import find_connection_file, connect_qtconsole
from IPython.frontend.qt.kernelmanager import QtKernelManager
from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
from IPython.config.application import catch_config_error
class IPythonLocalKernelApp(IPKernelApp):
"""IPython kernel application with nonblocking loop, running in dedicated thread.
example:
app = QtGui.QApplication([])
kernelapp = IPythonLocalKernelApp.instance()
kernelapp.start()
namespace = kernelapp.get_user_namespace()
namespace["QtGui"]=QtGui
namespace["QtCore"]=QtCore
app.exec_()"""
#DEFAULT_INSTANCE_ARGS starting commandline
DEFAULT_INSTANCE_ARGS = ['qtconsole','--pylab=inline', '--colors=linux']
@catch_config_error
def initialize(self, argv=None):
super(IPythonLocalKernelApp, self).initialize(argv)
self.kernel.eventloop = self.loop_qt4_nonblocking
def loop_qt4_nonblocking(self, kernel):
"""Non-blocking version of the ipython qt4 kernel loop"""
kernel.timer = QtCore.QTimer()
kernel.timer.timeout.connect(kernel.do_one_iteration)
kernel.timer.start(1000*kernel._poll_interval)
def start(self, argv=DEFAULT_INSTANCE_ARGS):
"""Starts IPython kernel app
argv: arguments passed to kernel
"""
self.initialize(argv)
#self.heartbeat.start()
#if self.poller is not None:
# self.poller.start()
self.kernel.start()
super(IPythonLocalKernelApp, self).start()
def get_connection_file(self):
"""Returne current kernel connection file."""
return self.connection_file
def get_user_namespace(self):
"""Returns current kernel userspace dict"""
return self.kernel.shell.user_ns
class IPythonConsoleQtWidget(RichIPythonWidget):
"""Ipython console Qt4+ widget
Usage example:
app = QtGui.QApplication([])
kernelapp = IPythonLocalKernelApp.instance()
kernelapp.start()
namespace = kernelapp.get_user_namespace()
widget = IPythonConsoleQtWidget()
widget.set_default_style(colors='linux')
widget.connect_kernel(connection_file=kernelapp.get_connection_file())
# if you won't to connect to remote kernel:
widget.connect_kernel(connection_file='kernel-16098.json')
widget.show()
namespace["widget"] = widget
namespace["QtGui"]=QtGui
namespace["QtCore"]=QtCore
app.exec_()"""
_connection_file = None
def __init__(self, *args, **kw):
RichIPythonWidget.__init__(self, *args, **kw)
self._existing = True
self._may_close = False
self._confirm_exit = False
def _init_kernel_manager(self):
km = QtKernelManager(connection_file=self._connection_file, config=self.config)
km.load_connection_file()
km.start_channels(hb=self._heartbeat)
self.kernel_manager = km
atexit.register(self.kernel_manager.cleanup_connection_file)
def connect_kernel(self, connection_file, heartbeat=False):
"""Connect's to ipython kernel.
connection_file - connection file to use
heartbeat - should start heartbeat server? Workaround for problems with inproc embedded kernels
(right click save image as/save as html kills kernel heartbeat/pool(??) serwer """
self._heartbeat = heartbeat
if os.path.exists(connection_file):
self._connection_file = connection_file
else:
self._connection_file = find_connection_file(connection_file)
self._init_kernel_manager()
app = QtGui.QApplication([])
kernelapp = IPythonLocalKernelApp.instance()
kernelapp.start()
namespace = kernelapp.get_user_namespace()
widget = IPythonConsoleQtWidget()
widget.set_default_style(colors='linux')
widget.connect_kernel(connection_file=kernelapp.get_connection_file())
# if you won't to connect to remote kernel:
# widget.connect_kernel(connection_file='kernel-16098.json')
widget.show()
namespace["widget"] = widget
namespace["QtGui"]=QtGui
namespace["QtCore"]=QtCore
app.exec_()
Not sure of what you want exactly but have tried to save the widget content into a a temporary file and pass it to a standard python interpreter with Popen ?
Doc is here : http://docs.python.org/release/2.6.5/library/subprocess.html#subprocess.Popen
Example :
import tempfile, os, sys, subprocess
# get the code
code = get_widget_content()
# save the code to a temporary file
file_handle, file_path = tempfile.mkstemp()
tmp_file = os.fdopen(file_handle, 'w')
tmp_file.write(code)
tmp_file.close()
#execute it
p = subprocess.Popen([sys.executable, file_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# wait for the command to complete
p.wait()
# retrieve the output:
pyerr = p.stderr.readlines()
pyout = p.stdout.readlines()
# do what ever you want with it
print(pyerr)
print(pyout)
It sounds like you did something similar to my Veusz application, https://veusz.github.io/. I thought you might find it useful to see a more complete implementation. I can't post hyperlinks but have a look at windows/consolewindow.py for the widget class. Commands are executed by the document/commandinterpreter.py class. The interface is defined in document/commandinterface.py. It's mostly done manipulating a dict however.
精彩评论