Python Forum
Embed Python console in GUI application
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Embed Python console in GUI application
#1
I am trying to figure out how best to provide access to a python interpreter from inside a GUI based program. The GUI is the interface for a process controller. Starting the GUI can take a long time because it involves booting multiple subsystems and turning on machinery in the proper sequence with lots of waiting for things to complete. If something goes wrong I would like a way to peek around inside the controller and maybe even try a patch before shutting things down and having to restart.

Looking for "embedded console", "gui console", "interactive interpreter", "python terminal" has pointed me to code.InteractiveConsole which is exactly what I want, except I don't see how I can hook this to a text editor widget inside the GUI. I am using Qt Widgets (PySide2), so QtConsole popped up, but that is about 100 times more powerful than I need and can I even use it outside of Jupyter?

I'd appreciate any suggestions or paths to pursue in my investigations. If you think my approach to solving this problem is off track I would appreciate hearing that too. I'm a C, C++, C# programmer and a lot of Python catches me off guard. Maybe I am completely backward (again) and I should be thinking of using a tool outside my GUI application instead of trying to shoehorn one into it.
Reply
#2
I made quite a lot of headway. I have a way of entering python code in a GUI control that works much line code.InteractiveConsole and I redirect stdout and stderr so they appear in my GUI:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import sys
import code
import PySide2.QtWidgets as QtWidgets
import PySide2.QtCore as QtCore
import PySide2.QtGui as QtGui
from contextlib import redirect_stdout, redirect_stderr
 
class Console(QtWidgets.QWidget):
<snip>
    def write(self, line: str):
        self.writeoutput(line, self.outfmt)
 
    def writeoutput(self, line: str, fmt: QtGui.QTextCharFormat=None) -> None:
        if fmt is not None:
            self.outdisplay.setCurrentCharFormat(fmt)
        self.outdisplay.appendPlainText(line.rstrip())
 
if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    console = Console()
    <snip>
    with redirect_stdout(console), redirect_stderr(console):
        console.show()
        sys.exit(app.exec_())
The program creates a format for characters typed as input (black) and another for captured output (blue). I would like a third format (red) for captured stderr. How do I differentiate stdout from stderr?

One idea I have is creating a redirect class that calls another function when it's write method gets called. I added an errorwrite method to my console class and create a redirect object to call that method. Then I redirect stderr to my redirect object instead of the console

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Redirect():
    def __init__(self, func):
        self.func = func
 
    def write(self, line:str):
        self.func(line)
 
class Console(QtWidgets.QWidget):
<snip>
    def errorwrite(self, line: str):
        self.writeoutput(line, self.errfmt)
 
if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    console = Console()
    redirect = Redirect(console.errorwrite)
    with redirect_stdout(console), redirect_stderr(redirect):
        console.show()
        sys.exit(app.exec_())
This works, but is there a better way?

Also, at what point is it polite to provide code snippets instead of enough code for a working example. My console.py file is 160 lines long and there isn't much fat to remove and still have it work.
Reply
#3
If anyone is interested here's a working solution. Happy to hear suggestions for improvement.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
""" Console
Interactive console widget.  Use to add an interactive python interpreter
in a GUI application.
"""
 
import sys
import code
import re
from typing import Dict, Callable
import PySide2.QtWidgets as QtWidgets
import PySide2.QtCore as QtCore
import PySide2.QtGui as QtGui
from contextlib import redirect_stdout, redirect_stderr
 
class LineEdit(QtWidgets.QLineEdit):
    """QLIneEdit with a history buffer for recalling previous lines.
    I also accept tab as input (4 spaces).
    """
    newline = QtCore.Signal(str)    # Signal when return key pressed
 
    def __init__(self, history: int=100) -> 'LineEdit':
        super().__init__()
        self.historymax = history
        self.clearhistory()
        self.promptpattern = re.compile('^[>\.]')
     
    def clearhistory(self) -> None:
        """Clear history buffer"""
        self.historyindex = 0
        self.historylist = []
 
    def event(self, ev: QtCore.QEvent) -> bool:
        """Intercept tab and arrow key presses.  Insert 4 spaces
        when tab pressed instead of moving to next contorl.  WHen
        arrow up or down are pressed select a line from the history
        buffer.  Emit newline signal when return key is pressed.
        """
        if ev.type() == QtCore.QEvent.KeyPress:
            if ev.key() == int(QtCore.Qt.Key_Tab):
                self.insert('    ')
                return True
            elif ev.key() == int(QtCore.Qt.Key_Up):
                self.recall(self.historyindex-1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Down):
                self.recall(self.historyindex+1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Home):
                self.recall(0)
                return True
            elif ev.key() == int(QtCore.Qt.Key_End):
                self.recall(len(self.historylist)-1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Return):
                self.returnkey()
                return True
        return super().event(ev)
 
    def returnkey(self) -> None:
        """Return key was pressed.  Add line to history and emit
        the newline signal.
        """
        text = self.text().rstrip()
        self.record(text)
        self.newline.emit(text)
        self.setText('')
 
    def recall(self, index: int) -> None:
        """Select a line from the history list"""
        length = len(self.historylist)
        if (length > 0):
            index = max(0, min(index, length-1))
            self.setText(self.historylist[index])
            self.historyindex = index
 
    def record(self, line:str) -> None:
        """Add line to history buffer"""
        self.historyindex += 1
        while len(self.historylist) >= self.historymax-1:
            self.historylist.pop()
        self.historylist.append(line)
        self.historyindex = min(self.historyindex, len(self.historylist))
 
 
class Redirect():
    """Map self.write to a function"""
    def __init__(self, func: Callable) -> 'Redirect':
        self.func = func
 
    def write(self, line:str) -> None:
        self.func(line)
 
 
class Console(QtWidgets.QWidget):
    """A GUI version of code.InteractiveConsole."""
     
    def __init__(
            self,
            context = locals(), # context for interpreter
            history: int=20,    # max lines in history buffer
            blockcount: int=500 # max lines in output buffer
            ) -> 'Console':
        super().__init__()
        self.setcontext(context)
        self.buffer = []
         
        self.content = QtWidgets.QGridLayout(self)
        self.content.setContentsMargins(0,0,0,0)
        self.content.setSpacing(0)
 
        # Display for output and stderr
        self.outdisplay = QtWidgets.QPlainTextEdit(self)
        self.outdisplay.setMaximumBlockCount(blockcount)
        self.outdisplay.setReadOnly(True)
        self.content.addWidget(self.outdisplay, 0, 0, 1, 2)
 
        # Use color to differentiate input, output and stderr
        self.inpfmt = self.outdisplay.currentCharFormat()
        self.outfmt = QtGui.QTextCharFormat(self.inpfmt)
        self.outfmt.setForeground(QtGui.QBrush(QtGui.QColor(0, 0, 255)))
        self.errfmt = QtGui.QTextCharFormat(self.inpfmt)
        self.errfmt.setForeground(QtGui.QBrush(QtGui.QColor(255, 0, 0)))
 
        # Display input prompt left of input edit
        self.promptdisp = QtWidgets.QLineEdit(self)
        self.promptdisp.setReadOnly(True)
        self.promptdisp.setFixedWidth(15)
        self.promptdisp.setFrame(False)
        self.content.addWidget(self.promptdisp, 1, 0)
        self.setprompt('> ')
 
        # Enter commands here
        self.inpedit = LineEdit(history = history)
        self.inpedit.newline.connect(self.push)
        self.inpedit.setFrame(False)
        self.content.addWidget(self.inpedit, 1, 1)
 
    def setcontext(self, context):
        """Set context for interpreter"""
        self.interp = code.InteractiveInterpreter(context)
 
    def resetbuffer(self) -> None:
        """Reset the input buffer."""
        self.buffer = []
 
    def setprompt(self, text: str):
        self.prompt = text
        self.promptdisp.setText(text)
 
    def push(self, line: str) -> None:
        """Execute entered command.  Command may span multiple lines"""
        if line == 'clear':
            self.inpedit.clearhistory()
            self.outdisplay.clear()
        else:
            lines = line.split('\n')
            for line in lines:
                if re.match('^[\>\.] ', line):
                    line = line[2:]
                self.writeoutput(self.prompt+line, self.inpfmt)
                self.setprompt('. ')
                self.buffer.append(line)
            # Built a command string from lines in the buffer
            source = "\n".join(self.buffer)
            more = self.interp.runsource(source, '<console>')
            if not more:
                self.setprompt('> ')
                self.resetbuffer()
 
    def setfont(self, font: QtGui.QFont) -> None:
        """Set font for input and display widgets.  Should be monospaced"""
        self.outdisplay.setFont(font)
        self.inpedit.setFont(font)
 
    def write(self, line: str) -> None:
        """Capture stdout and display in outdisplay"""
        if (len(line) != 1 or ord(line[0]) != 10):
            self.writeoutput(line.rstrip(), self.outfmt)
 
    def errorwrite(self, line: str) -> None:
        """Capture stderr and display in outdisplay"""
        self.writeoutput(line, self.errfmt)
 
    def writeoutput(self, line: str, fmt: QtGui.QTextCharFormat=None) -> None:
        """Set text formatting and display line in outdisplay"""
        if fmt is not None:
            self.outdisplay.setCurrentCharFormat(fmt)
        self.outdisplay.appendPlainText(line.rstrip())
 
       
if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    console = Console()
    console.setWindowTitle('Console')
    console.setfont(QtGui.QFont('Lucida Sans Typewriter', 10))
 
    # Redirect stdout to console.write and stderr to console.errorwrite
    redirect = Redirect(console.errorwrite)
    with redirect_stdout(console), redirect_stderr(redirect):
        console.show()
        sys.exit(app.exec_())
Gribouillis likes this post
Reply
#4
hey, I want to thank you for this awesome code, I have been banging my head on how to embed a console in pyqt for a while now. I modified it to use pyqt5, changed 1 line of code other than the imports and want to post it here for anyone else who wants to use pyqt5.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
""" Console
Interactive console widget.  Use to add an interactive python interpreter
in a GUI application.
 
Original by deanhystad available here: https://python-forum.io/thread-25117.html
changes: PySide2 -> pyqt5
            + line 22: QtCore.Signal(str) -> QtCore.pyqtSignal(str)
"""
 
import sys
import code
import re
from typing import Callable
from PyQt5 import QtCore, QtGui, QtWidgets
from contextlib import redirect_stdout, redirect_stderr
 
 
class LineEdit(QtWidgets.QLineEdit):
    """QLIneEdit with a history buffer for recalling previous lines.
    I also accept tab as input (4 spaces).
    """
    newline = QtCore.pyqtSignal(str# Signal when return key pressed
 
    def __init__(self, history: int = 100) -> 'LineEdit':
        super().__init__()
        self.historymax = history
        self.clearhistory()
        self.promptpattern = re.compile('^[>\.]')
 
    def clearhistory(self) -> None:
        """Clear history buffer"""
        self.historyindex = 0
        self.historylist = []
 
    def event(self, ev: QtCore.QEvent) -> bool:
        """Intercept tab and arrow key presses.  Insert 4 spaces
        when tab pressed instead of moving to next contorl.  WHen
        arrow up or down are pressed select a line from the history
        buffer.  Emit newline signal when return key is pressed.
        """
        if ev.type() == QtCore.QEvent.KeyPress:
            if ev.key() == int(QtCore.Qt.Key_Tab):
                self.insert('    ')
                return True
            elif ev.key() == int(QtCore.Qt.Key_Up):
                self.recall(self.historyindex - 1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Down):
                self.recall(self.historyindex + 1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Home):
                self.recall(0)
                return True
            elif ev.key() == int(QtCore.Qt.Key_End):
                self.recall(len(self.historylist) - 1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Return):
                self.returnkey()
                return True
        return super().event(ev)
 
    def returnkey(self) -> None:
        """Return key was pressed.  Add line to history and emit
        the newline signal.
        """
        text = self.text().rstrip()
        self.record(text)
        self.newline.emit(text)
        self.setText('')
 
    def recall(self, index: int) -> None:
        """Select a line from the history list"""
        length = len(self.historylist)
        if length > 0:
            index = max(0, min(index, length - 1))
            self.setText(self.historylist[index])
            self.historyindex = index
 
    def record(self, line: str) -> None:
        """Add line to history buffer"""
        self.historyindex += 1
        while len(self.historylist) >= self.historymax - 1:
            self.historylist.pop()
        self.historylist.append(line)
        self.historyindex = min(self.historyindex, len(self.historylist))
 
 
class Redirect:
    """Map self.write to a function"""
 
    def __init__(self, func: Callable) -> 'Redirect':
        self.func = func
 
    def write(self, line: str) -> None:
        self.func(line)
 
 
class Console(QtWidgets.QWidget):
    """A GUI version of code.InteractiveConsole."""
 
    def __init__(
            self,
            context=locals(),  # context for interpreter
            history: int = 20# max lines in history buffer
            blockcount: int = 500  # max lines in output buffer
    ) -> 'Console':
        super().__init__()
        self.setcontext(context)
        self.buffer = []
 
        self.content = QtWidgets.QGridLayout(self)
        self.content.setContentsMargins(0, 0, 0, 0)
        self.content.setSpacing(0)
 
        # Display for output and stderr
        self.outdisplay = QtWidgets.QPlainTextEdit(self)
        self.outdisplay.setMaximumBlockCount(blockcount)
        self.outdisplay.setReadOnly(True)
        self.content.addWidget(self.outdisplay, 0, 0, 1, 2)
 
        # Use color to differentiate input, output and stderr
        self.inpfmt = self.outdisplay.currentCharFormat()
        self.outfmt = QtGui.QTextCharFormat(self.inpfmt)
        self.outfmt.setForeground(QtGui.QBrush(QtGui.QColor(0, 0, 255)))
        self.errfmt = QtGui.QTextCharFormat(self.inpfmt)
        self.errfmt.setForeground(QtGui.QBrush(QtGui.QColor(255, 0, 0)))
 
        # Display input prompt left of input edit
        self.promptdisp = QtWidgets.QLineEdit(self)
        self.promptdisp.setReadOnly(True)
        self.promptdisp.setFixedWidth(15)
        self.promptdisp.setFrame(False)
        self.content.addWidget(self.promptdisp, 1, 0)
        self.setprompt('> ')
 
        # Enter commands here
        self.inpedit = LineEdit(history=history)
        self.inpedit.newline.connect(self.push)
        self.inpedit.setFrame(False)
        self.content.addWidget(self.inpedit, 1, 1)
 
    def setcontext(self, context):
        """Set context for interpreter"""
        self.interp = code.InteractiveInterpreter(context)
 
    def resetbuffer(self) -> None:
        """Reset the input buffer."""
        self.buffer = []
 
    def setprompt(self, text: str):
        self.prompt = text
        self.promptdisp.setText(text)
 
    def push(self, line: str) -> None:
        """Execute entered command.  Command may span multiple lines"""
        if line == 'clear':
            self.inpedit.clearhistory()
            self.outdisplay.clear()
        else:
            lines = line.split('\n')
            for line in lines:
                if re.match('^[\>\.] ', line):
                    line = line[2:]
                self.writeoutput(self.prompt + line, self.inpfmt)
                self.setprompt('. ')
                self.buffer.append(line)
            # Built a command string from lines in the buffer
            source = "\n".join(self.buffer)
            more = self.interp.runsource(source, '<console>')
            if not more:
                self.setprompt('> ')
                self.resetbuffer()
 
    def setfont(self, font: QtGui.QFont) -> None:
        """Set font for input and display widgets.  Should be monospaced"""
        self.outdisplay.setFont(font)
        self.inpedit.setFont(font)
 
    def write(self, line: str) -> None:
        """Capture stdout and display in outdisplay"""
        if len(line) != 1 or ord(line[0]) != 10:
            self.writeoutput(line.rstrip(), self.outfmt)
 
    def errorwrite(self, line: str) -> None:
        """Capture stderr and display in outdisplay"""
        self.writeoutput(line, self.errfmt)
 
    def writeoutput(self, line: str, fmt: QtGui.QTextCharFormat = None) -> None:
        """Set text formatting and display line in outdisplay"""
        if fmt is not None:
            self.outdisplay.setCurrentCharFormat(fmt)
        self.outdisplay.appendPlainText(line.rstrip())
 
 
if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    console = Console()
    console.setWindowTitle('Console')
    console.setfont(QtGui.QFont('Lucida Sans Typewriter', 10))
 
    # Redirect stdout to console.write and stderr to console.errorwrite
    redirect = Redirect(console.errorwrite)
    with redirect_stdout(console), redirect_stderr(redirect):
        console.show()
        sys.exit(app.exec_())
Gribouillis likes this post
Reply
#5
If you can use Gtk

[Image: pyterminal.png?raw=1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
gi.require_version("Vte", "2.91")
from gi.repository import Gtk, Vte, Gdk
from gi.repository import GLib
 
 
class Terminal(Vte.Terminal):
 
    def __init__(self):
        super(Vte.Terminal, self).__init__()
 
        self.spawn_async(Vte.PtyFlags.DEFAULT,
            "/tmp",
            ["/bin/bash"],
            None,
            GLib.SpawnFlags.DO_NOT_REAP_CHILD,
            None,
            None,
            -1,
            None,
            None
            )
             
 
        self.set_font_scale(0.9)
        self.set_scroll_on_output(True)
        self.set_scroll_on_keystroke(True)
        palette = [Gdk.RGBA(0.4, 0.8, 1.0, 1.0)] * 16
        self.set_colors(Gdk.RGBA(1.0, 1.0, 1.0, 1.0), Gdk.RGBA(0.3, 0.3, 0.3, 1.0), palette)
        self.connect("key_press_event", self.copy_or_paste)
        self.connect("current-directory-uri-changed", self.wd_changed)
 
        self.set_scrollback_lines(-1)
        self.set_audible_bell(0)
 
    def copy_or_paste(self, widget, event):
        control_key = Gdk.ModifierType.CONTROL_MASK
        shift_key = Gdk.ModifierType.SHIFT_MASK
        if event.type == Gdk.EventType.KEY_PRESS:
            if event.state == shift_key | control_key:
                if event.keyval == 67:
                    self.copy_clipboard()
                elif event.keyval == 86:
                    self.paste_clipboard()
                return True
                 
    def wd_changed(self, *args):
        workingDir = self.get_current_directory_uri()
        print("workingDir changed to:", workingDir)
 
class MyWindow(Gtk.Window):
    def __init__(self, parent=None):
        super(MyWindow, self).__init__()
 
    def main(self):
        self.terminal = Terminal()
        self.cb = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY)  
        self.cb.wait_for_text()
        self.cb.set_text("python3", -1)       
        self.connect('delete-event', Gtk.main_quit)
        self.scrolled_win = Gtk.ScrolledWindow()
        self.scrolled_win.add(self.terminal)
        self.add(self.scrolled_win)
        self.set_title("Terminal")
        self.resize(800, 300)
        self.move(0, 0)
        self.show_all()
        self.terminal.paste_primary()
        self.terminal.grab_focus()
        self.terminal.feed_child([13])
 
         
if __name__ == "__main__":
    win = MyWindow()
    win.main()
    Gtk.main()
Gribouillis likes this post
Reply
#6
You should also look at qtconsole. It gives you a very pretty, feature rich console.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Application development in Python muralikreddy 4 4,654 Mar-27-2024, 12:05 AM
Last Post: anastasiastefanyuk
  How to make cross platform gui python application ? Jayam 2 4,619 Dec-24-2021, 03:24 PM
Last Post: Linenloid
  [PyQt] Embed Google Maps in PyQt6 Widget Raures 2 4,189 Sep-11-2021, 04:32 PM
Last Post: Raures
  [PyGUI] Python Application Not Finding Excel File dseals26 2 3,950 Feb-17-2021, 01:45 AM
Last Post: thewolf
  Python Application in Taskbar constantin01 3 7,450 Jan-24-2020, 10:57 AM
Last Post: buran
  Interacting with python console while program is running Murmele 2 4,094 Feb-10-2018, 05:43 PM
Last Post: kmcollins
  Python Wrapper Application panick1992 8 8,359 Mar-15-2017, 11:54 PM
Last Post: micseydel

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020