Cómo incrustar un intérprete de Python en un widget de PyQT

Quiero poder abrir un terminal python interactivo desde mi aplicación python. Algunas de las variables de mi progtwig, pero no todas, deben estar expuestas al intérprete.

Actualmente utilizo un QPlainTextEdit modificado y sub-clasificado y QPlainTextEdit todos los “comandos” allí a eval o exec , y mantengo un registro de un espacio de nombres separado en un dict. Sin embargo, tiene que haber una forma más elegante y robusta! ¿Cómo?

Aquí hay un ejemplo haciendo exactamente lo que quiero, pero es con IPython y pyGTK … http://ipython.scipy.org/moin/Cookbook/EmbeddingInGTK

A continuación se muestra lo que tengo actualmente. Pero hay tantos casos de esquina que probablemente me perdí algunos. Es muy lento, intente con un bucle de impresión grande … Tiene que ser una forma más sencilla y menos propensa a errores, … ¡espero!

Es la función def runCommand(self) que es la clave para entender mi problema. Idealmente, no quiero mejorarlo, prefiero reemplazar su contenido con algo más simple y más inteligente.

La funcionalidad de la console.updateNamespace({'myVar1' : app, 'myVar2' : 1234}) en “main” también es importante.

 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_()) 

Poco tarde, lo sé, pero recomiendo la clase code.InteractiveConsole : http://docs.python.org/py3k/library/code.html#code.InteractiveConsole

Podría considerar el uso de subprocesos para mantener la interfaz de usuario receptiva al imprimir grandes bucles. Esto también ayudaría a mantener tus trazas limpias.

Mantener las variables en un dict es el camino a seguir, es lo que Python hace internamente. En cuanto a exponer “algunos, pero no todos” de ellos, considere simplemente exponerlos a todos. Más fácil. Si te preocupa la seguridad, ten en cuenta que no puedes ocultar nada de forma confiable en Python.

En cuanto a la horrible manipulación del cursor / texto: aproveche el hecho de que tiene una GUI. Con un terminal, solo tiene un “cuadro de texto”, pero en Qt, podría ser más apropiado tener una vista de registro / resultado y un cuadro de comando separado .

La vista de registro mostraría los comandos ingresados ​​y los resultados en un cuadro de texto de solo lectura.

El cuadro de texto del comando le permitiría ingresar un comando limpiamente.

Este enfoque se utiliza en algunos marcos web, por ejemplo, a través de WebError :

introduzca la descripción de la imagen aquí

Primer borrador de la versión actualizada de mi código para soportar 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_() 

¿No está seguro de lo que desea exactamente pero ha intentado guardar el contenido del widget en un archivo temporal y pasarlo a un intérprete de python estándar con Popen?

El documento está aquí: http://docs.python.org/release/2.6.5/library/subprocess.html#subprocess.Popen

Ejemplo:

 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) 

Parece que hiciste algo similar a mi aplicación Veusz, https://veusz.github.io/ . Pensé que podría resultarle útil ver una implementación más completa. No puedo publicar hipervínculos pero puedo ver windows / consolewindow.py para la clase de widget. Los comandos son ejecutados por la clase document / commandinterpreter.py. La interfaz está definida en document / commandinterface.py. Sin embargo, se hace principalmente manipulando un dict.