Files
pallectrum/electrum/gui/qt/console.py
SomberNight ff53925811 qt console: fix tracebacks in windows binaries
fixes https://github.com/spesmilo/electrum/issues/3315

The cause was that tracebacks look different whether stack items have source text available.
When using the pyinstaller windows binary, there is no source text available.

Example when running from source:
```
>>> a
Traceback (most recent call last):
  File "...\electrum\gui\qt\console.py", line 256, in exec_command
    result = eval(command, self.namespace, self.namespace)
  File "<string>", line 1, in <module>
NameError: name 'a' is not defined

```

Example for pyinstaller windows binary:
```
>>> a
Traceback (most recent call last):
  File "electrum\gui\qt\console.py", line 256, in exec_command
  File "<string>", line 1, in <module>
NameError: name 'a' is not defined

```
2023-11-24 22:36:37 +00:00

357 lines
12 KiB
Python

# source: http://stackoverflow.com/questions/2758159/how-to-embed-a-python-interpreter-in-a-pyqt-widget
import sys
import os
import re
import traceback
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from electrum import util
from electrum.i18n import _
from .util import MONOSPACE_FONT, font_height
# sys.ps1 and sys.ps2 are only declared if an interpreter is in interactive mode.
sys.ps1 = '>>> '
sys.ps2 = '... '
class OverlayLabel(QtWidgets.QLabel):
STYLESHEET = '''
QLabel, QLabel link {
color: rgb(0, 0, 0);
background-color: rgb(248, 240, 200);
border: 1px solid;
border-color: rgb(255, 114, 47);
padding: 2px;
}
'''
def __init__(self, text, parent):
super().__init__(text, parent)
self.setMinimumHeight(max(150, 10 * font_height()))
self.setGeometry(0, 0, self.width(), self.height())
self.setStyleSheet(self.STYLESHEET)
self.setMargin(0)
parent.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setWordWrap(True)
def mousePressEvent(self, e):
self.hide()
def on_resize(self, w):
padding = 2 # px, from the stylesheet above
self.setFixedWidth(w - padding)
class Console(QtWidgets.QPlainTextEdit):
def __init__(self, parent=None):
QtWidgets.QPlainTextEdit.__init__(self, parent)
self.history = []
self.namespace = {}
self.construct = []
self.setGeometry(50, 75, 600, 400)
self.setWordWrapMode(QtGui.QTextOption.WrapAnywhere)
self.setUndoRedoEnabled(False)
self.setFont(QtGui.QFont(MONOSPACE_FONT, 10, QtGui.QFont.Normal))
self.newPrompt("") # make sure there is always a prompt, even before first server.banner
self.updateNamespace({'run':self.run_script})
self.set_json(False)
warning_text = "<h1>{}</h1><br>{}<br><br>{}".format(
_("Warning!"),
_("Do not paste code here that you don't understand. Executing the wrong code could lead "
"to your coins being irreversibly lost."),
_("Click here to hide this message.")
)
self.messageOverlay = OverlayLabel(warning_text, self)
def resizeEvent(self, e):
super().resizeEvent(e)
vertical_scrollbar_width = self.verticalScrollBar().width() * self.verticalScrollBar().isVisible()
self.messageOverlay.on_resize(self.width() - vertical_scrollbar_width)
def set_json(self, b):
self.is_json = b
def run_script(self, filename):
with open(filename) as f:
script = f.read()
self.exec_command(script)
def updateNamespace(self, namespace):
self.namespace.update(namespace)
def showMessage(self, message):
curr_line = self.getCommand(strip=False)
self.appendPlainText(message)
self.newPrompt(curr_line)
def clear(self):
curr_line = self.getCommand()
self.setPlainText('')
self.newPrompt(curr_line)
def keyboard_interrupt(self):
self.construct = []
self.appendPlainText('KeyboardInterrupt')
self.newPrompt('')
def newPrompt(self, curr_line):
if self.construct:
prompt = sys.ps2 + curr_line
else:
prompt = sys.ps1 + curr_line
self.completions_pos = self.textCursor().position()
self.completions_visible = False
self.appendPlainText(prompt)
self.moveCursor(QtGui.QTextCursor.End)
def getCommand(self, *, strip=True):
doc = self.document()
curr_line = doc.findBlockByLineNumber(doc.lineCount() - 1).text()
if strip:
curr_line = curr_line.rstrip()
curr_line = curr_line[len(sys.ps1):]
return curr_line
def setCommand(self, command):
if self.getCommand() == command:
return
doc = self.document()
curr_line = doc.findBlockByLineNumber(doc.lineCount() - 1).text()
self.moveCursor(QtGui.QTextCursor.End)
for i in range(len(curr_line) - len(sys.ps1)):
self.moveCursor(QtGui.QTextCursor.Left, QtGui.QTextCursor.KeepAnchor)
self.textCursor().removeSelectedText()
self.textCursor().insertText(command)
self.moveCursor(QtGui.QTextCursor.End)
def show_completions(self, completions):
if self.completions_visible:
self.hide_completions()
c = self.textCursor()
c.setPosition(self.completions_pos)
completions = map(lambda x: x.split('.')[-1], completions)
t = '\n' + ' '.join(completions)
if len(t) > 500:
t = t[:500] + '...'
c.insertText(t)
self.completions_end = c.position()
self.moveCursor(QtGui.QTextCursor.End)
self.completions_visible = True
def hide_completions(self):
if not self.completions_visible:
return
c = self.textCursor()
c.setPosition(self.completions_pos)
l = self.completions_end - self.completions_pos
for x in range(l): c.deleteChar()
self.moveCursor(QtGui.QTextCursor.End)
self.completions_visible = False
def getConstruct(self, command):
if self.construct:
self.construct.append(command)
if 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 addToHistory(self, command):
if not self.construct and command[0:1] == ' ':
return
if command and (not self.history or self.history[-1] != command):
while len(self.history) >= 50:
self.history.remove(self.history[0])
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):
c = self.textCursor()
return c.position() - c.block().position() - len(sys.ps1)
def setCursorPosition(self, position):
self.moveCursor(QtGui.QTextCursor.StartOfLine)
for i in range(len(sys.ps1) + position):
self.moveCursor(QtGui.QTextCursor.Right)
def run_command(self):
command = self.getCommand()
self.addToHistory(command)
command = self.getConstruct(command)
if command:
self.exec_command(command)
self.newPrompt('')
self.set_json(False)
def exec_command(self, command):
tmp_stdout = sys.stdout
class stdoutProxy():
def __init__(self, write_func):
self.write_func = write_func
self.skip = False
def flush(self):
pass
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
if type(self.namespace.get(command)) == type(lambda:None):
self.appendPlainText("'{}' is a function. Type '{}()' to use it in the Python console."
.format(command, command))
return
sys.stdout = stdoutProxy(self.appendPlainText)
try:
try:
# eval is generally considered bad practice. use it wisely!
result = eval(command, self.namespace, self.namespace)
if result is not None:
if self.is_json:
util.print_msg(util.json_encode(result))
else:
self.appendPlainText(repr(result))
except SyntaxError:
# exec is generally considered bad practice. use it wisely!
exec(command, self.namespace, self.namespace)
except SystemExit:
self.close()
except BaseException as e:
te = traceback.TracebackException.from_exception(e)
# rm part of traceback mentioning this file.
# (note: we rm stack items before converting to str, instead of removing lines from the str,
# as this is more reliable. The latter would differ whether the traceback has source text lines,
# which is not always the case.)
te.stack = traceback.StackSummary.from_list(te.stack[1:])
tb_str = "".join(te.format())
# rm last linebreak:
if tb_str.endswith("\n"):
tb_str = tb_str[:-1]
self.appendPlainText(tb_str)
sys.stdout = tmp_stdout
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Tab:
self.completions()
return
self.hide_completions()
if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.run_command()
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_L and event.modifiers() == QtCore.Qt.ControlModifier:
self.clear()
elif event.key() == QtCore.Qt.Key_C and event.modifiers() == QtCore.Qt.ControlModifier:
if not self.textCursor().selectedText():
self.keyboard_interrupt()
super(Console, self).keyPressEvent(event)
def completions(self):
cmd = self.getCommand()
# note for regex: new words start after ' ' or '(' or ')'
lastword = re.split(r'[ ()]', cmd)[-1]
beginning = cmd[0:-len(lastword)]
path = lastword.split('.')
prefix = '.'.join(path[:-1])
prefix = (prefix + '.') if prefix else prefix
ns = self.namespace.keys()
if len(path) == 1:
ns = ns
else:
assert len(path) > 1
obj = self.namespace.get(path[0])
try:
for attr in path[1:-1]:
obj = getattr(obj, attr)
except AttributeError:
ns = []
else:
ns = dir(obj)
completions = []
for name in ns:
if name[0] == '_':continue
if name.startswith(path[-1]):
completions.append(prefix+name)
completions.sort()
if not completions:
self.hide_completions()
elif len(completions) == 1:
self.hide_completions()
self.setCommand(beginning + completions[0])
else:
# find common prefix
p = os.path.commonprefix(completions)
if len(p)>len(lastword):
self.hide_completions()
self.setCommand(beginning + p)
else:
self.show_completions(completions)