pqt5 - Cómo incrustar un intérprete de Python en un widget de PyQT
qt creator python (5)
Quiero poder abrir un terminal python interactivo desde mi aplicación python. Algunas de las variables de mi programa, 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 simple 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_())
¿No está seguro de lo que quiere 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 se define en document / commandinterface.py. Sin embargo, se hace principalmente manipulando un dict.
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 :
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_()