Posts: 6,779
Threads: 20
Joined: Feb 2020
Mar-20-2020, 04:50 AM
(This post was last modified: Mar-20-2020, 06:37 AM by deanhystad.)
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.
Posts: 6,779
Threads: 20
Joined: Feb 2020
Mar-22-2020, 05:25 PM
(This post was last modified: Mar-22-2020, 05:25 PM by deanhystad.)
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:
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
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.
Posts: 6,779
Threads: 20
Joined: Feb 2020
Mar-23-2020, 11:04 PM
(This post was last modified: Mar-23-2020, 11:04 PM by deanhystad.)
If anyone is interested here's a working solution. Happy to hear suggestions for improvement.
""" 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
Posts: 48
Threads: 19
Joined: Oct 2016
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.
""" 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
Posts: 1,025
Threads: 16
Joined: Dec 2016
Jun-03-2021, 09:12 PM
(This post was last modified: Jun-03-2021, 09:12 PM by Axel_Erfurt.)
If you can use Gtk
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
Posts: 6,779
Threads: 20
Joined: Feb 2020
You should also look at qtconsole. It gives you a very pretty, feature rich console.
|