diff --git a/qutebrowser/utils/misc.py b/qutebrowser/utils/misc.py index 887b25c7b..b15049e0f 100644 --- a/qutebrowser/utils/misc.py +++ b/qutebrowser/utils/misc.py @@ -20,6 +20,7 @@ """Other utilities which don't fit anywhere else. """ import os +import io import sys import shlex import os.path @@ -27,6 +28,7 @@ import urllib.request from urllib.parse import urljoin, urlencode from collections import OrderedDict from functools import reduce +from contextlib import contextmanager from PyQt5.QtCore import QCoreApplication, QStandardPaths, Qt from PyQt5.QtGui import QKeySequence, QColor @@ -439,3 +441,53 @@ def normalize_keystr(keystr): for mod in ('Ctrl', 'Meta', 'Alt', 'Shift'): keystr = keystr.replace(mod + '-', mod + '+') return keystr.lower() + + +class FakeIOStream(io.TextIOBase): + + """A fake file-like stream which calls a function for write-calls.""" + + def __init__(self, write_func): + self.write = write_func + + def flush(self): + """This is only here to satisfy pylint.""" + return super().flush() + + def isatty(self): + """This is only here to satisfy pylint.""" + return super().isatty() + + +@contextmanager +def fake_io(write_func): + """Run code with stdout and stderr replaced by FakeIOStreams. + + Args: + write_func: The function to call when write is called. + """ + old_stdout = sys.stdout + old_stderr = sys.stderr + fake_stderr = FakeIOStream(write_func) + fake_stdout = FakeIOStream(write_func) + sys.stderr = fake_stderr + sys.stdout = fake_stdout + yield + # If the code we did run did change sys.stdout/sys.stderr, we leave it + # unchanged. Otherwise, we reset it. + if sys.stdout is fake_stdout: + sys.stdout = old_stdout + if sys.stderr is fake_stderr: + sys.stderr = old_stderr + + +@contextmanager +def disabled_excepthook(): + """Run code with the exception hook temporarely disabled.""" + old_excepthook = sys.excepthook + sys.excepthook = sys.__excepthook__ + yield + # If the code we did run did change sys.excepthook, we leave it + # unchanged. Otherwise, we reset it. + if sys.excepthook is sys.__excepthook__: + sys.excepthook = old_excepthook diff --git a/qutebrowser/widgets/console.py b/qutebrowser/widgets/console.py index bd04e3caa..798cf954d 100644 --- a/qutebrowser/widgets/console.py +++ b/qutebrowser/widgets/console.py @@ -21,27 +21,12 @@ from code import InteractiveInterpreter -from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt from PyQt5.QtWidgets import QLineEdit, QTextEdit, QWidget, QVBoxLayout + from qutebrowser.models.cmdhistory import (History, HistoryEmptyError, HistoryEndReachedError) - - -class ConsoleInteractiveInterpreter(InteractiveInterpreter, QObject): - - """Subclass of InteractiveInterpreter to use a signal instead of stderr. - - FIXME: This approach doesn't actually work. - """ - - write_output = pyqtSignal(str) - - def __init__(self, parent=None): - QObject.__init__(self, parent) - InteractiveInterpreter.__init__(self) - - def write(self, data): - self.write_output.emit(data) +from qutebrowser.utils.misc import fake_io, disabled_excepthook class ConsoleLineEdit(QLineEdit): @@ -54,8 +39,7 @@ class ConsoleLineEdit(QLineEdit): super().__init__(parent) self._more = False self._buffer = [] - self._interpreter = ConsoleInteractiveInterpreter() - self._interpreter.write_output.connect(self.write) + self._interpreter = InteractiveInterpreter() self.history = History() self.returnPressed.connect(self.execute) @@ -71,7 +55,15 @@ class ConsoleLineEdit(QLineEdit): """Push a line to the interpreter.""" self._buffer.append(line) source = '\n'.join(self._buffer) - self._more = self._interpreter.runsource(source, '') + # We do two special things with the contextmanagers here: + # - We replace stdout/stderr to capture output. Even if we could + # override InteractiveInterpreter's write method, most things are + # printed elsewhere (e.g. by exec). Other Python GUI shells do the + # same. + # - We disable our exception hook, so exceptions from the console get + # printed and don't ooen a crashdialog. + with fake_io(self.write.emit), disabled_excepthook(): + self._more = self._interpreter.runsource(source, '') if not self._more: self._buffer = []