views:

469

answers:

3

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_())
+1  A: 

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)
thomas
It is to basic, I added some code above to show what I want.
Mathias
+1  A: 

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 e.g. in Pylons, and it's quite intuitive. (Pylons is a library for the Web, see for example a screenshot at http://jythonpodcast.hostjava.net/jythonbook/en/.99/_images/picture_1.png)

Petr Viktorin
A: 

It sounds like you did something similar to my Veusz application, http://home.gna.org/veusz. 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.

xioxox