Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions qtapputils/managers/capture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright © SARDES Project Contributors
# https://github.com/cgq-qgc/sardes
#
# This file is part of SARDES.
# Licensed under the terms of the GNU General Public License.
# -----------------------------------------------------------------------------

# ---- Standard library imports
import sys
import traceback

# ---- Third party imports
from qtpy.QtCore import QObject, Signal
from qtpy.QtWidgets import QApplication


class ExceptHook(QObject):
"""
A Qt object to catch exceptions and emit a formatted string of the error.
"""
sig_except_caught = Signal(str)

def excepthook(self, exc_type, exc_value, exc_traceback):
"""Handle uncaught exceptions."""
sys.__excepthook__(exc_type, exc_value, exc_traceback)
if not issubclass(exc_type, SystemExit):
log_msg = ''.join(traceback.format_exception(
exc_type, exc_value, exc_traceback))
self.sig_except_caught.emit(log_msg)


class StandardStreamEmitter(QObject):
"""
A Qt object to intercept and emit the input and output of the
Python interpreter.

https://docs.python.org/3/library/sys.html#sys.stdout
https://docs.python.org/3/library/sys.html#sys.stderr
"""
sig_new_text = Signal(str)

def write(self, text):
try:
sys.__stdout__.write(text)
except Exception:
pass
self.sig_new_text.emit(str(text))


class SysCaptureManager(QObject):
"""
A manager to capture and manage Python's standard input and output
streams, logging and internal errors reporting.

Important Note:
the system capture manager should NOT be started when testing
under pytest because this will cause problems with the way
pytest is already capturing standard system messages and
raised exceptions.

See pytest/src/_pytest/capture.py
"""

def __init__(self, start_capture=False):
super().__init__()
self._stdstream_stack = ''
self._stdstream_consoles = []
self._is_capturing = False

self.except_dialog = None

# Setup the Except hook.
self.except_hook = ExceptHook()
self.except_hook.sig_except_caught.connect(self._handle_except)

# Setup the standard stream emitter.
self.stdout_emitter = StandardStreamEmitter()
self.stdout_emitter.sig_new_text.connect(self.__handle_stdout)

self.stderr_emitter = StandardStreamEmitter()
self.stderr_emitter.sig_new_text.connect(self.handle_stderr)

if start_capture:
self.start_capture()

def start_capture(self):
"""
Start capturing Python interpreter standard messages and unhandled
raised exceptions.

Important Note:
the system capture manager should NOT be started when testing
under pytest because this will cause problems with the way
pytest is already capturing standard system messages and
raised exceptions.

See pytest/src/_pytest/capture.py
"""
self._is_capturing = True
self.__orig_except_hook = sys.excepthook
self.__orig_stdout = sys.stdout
self.__orig_stderr = sys.stderr

sys.excepthook = self.except_hook.excepthook
sys.stdout = self.stdout_emitter
sys.stderr = self.stderr_emitter

def stop_capture(self):
"""
Stop capturing Python interpreter standard messages and unhandled
raised exceptions.
"""
if self._is_capturing:
self._is_capturing = False
sys.excepthook = self.__orig_except_hook
sys.stdout = self.__orig_stdout
sys.stderr = self.__orig_stderr

def register_stdstream_console(self, console):
"""
Register the specified console to this system capture manager.
"""
self._stdstream_consoles.append(console)
console.write(self._stdstream_stack)

def register_except_dialog(self, except_dialog):
"""
Register the system except dialog.
"""
self.except_dialog = except_dialog

def handle_stderr(self, text):
"""
Handle Python interpreter standard errors.
"""
self._stdstream_stack += text
for console in self._stdstream_consoles:
console.write(text)

def __handle_stdout(self, text):
"""
Handle Python interpreter standard output.
"""
self._stdstream_stack += text
for console in self._stdstream_consoles:
console.write(text)

def _handle_except(self, log_msg):
"""
Handle raised exceptions that have not been handled properly
internally and need to be reported for bug fixing.
"""
QApplication.restoreOverrideCursor()
if self.except_dialog is not None:
self.except_dialog.show_error(
log_msg, self._stdstream_stack
)
138 changes: 138 additions & 0 deletions qtapputils/widgets/console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright © SARDES Project Contributors
# https://github.com/cgq-qgc/sardes
#
# This file is part of SARDES.
# Licensed under the terms of the GNU General Public License.
# -----------------------------------------------------------------------------

# ---- Standard library imports
import sys
import os.path as osp
import datetime

# ---- Third party imports
from qtpy.QtCore import Qt
from qtpy.QtGui import QTextCursor, QIcon
from qtpy.QtWidgets import (
QApplication, QDialog, QDialogButtonBox, QGridLayout, QPushButton,
QTextEdit, QWidget)

# ---- Local imports
from qtapputils.managers import SaveFileManager


class StandardStreamConsole(QTextEdit):
"""
A Qt text edit to hold and show the standard input and output of the
Python interpreter.
"""

def __init__(self):
super().__init__()
self.setReadOnly(True)

def write(self, text):
self.moveCursor(QTextCursor.End)
self.insertPlainText(text)


class SystemMessageDialog(QDialog):
"""
A dialog to show and manage the standard input and ouput
of the Python interpreter.
"""

def __init__(self, title: str, icon: QIcon = None, parent: QWidget = None):
super().__init__(parent)
self.setWindowFlags(
self.windowFlags() &
~Qt.WindowContextHelpButtonHint |
Qt.WindowMinMaxButtonsHint)
if icon is not None:
self.setWindowIcon(icon)
self.setWindowTitle(title)
self.setMinimumSize(700, 500)

def _save_file(filename, content):
with open(filename, 'w') as txtfile:
txtfile.write(content)

self.savefile_manager = SaveFileManager(
namefilters={'.txt': "Text File (*.txt)"},
onsave=_save_file,
parent=self
)
self.std_console = StandardStreamConsole()

# Setup the dialog button box.
self.saveas_btn = QPushButton('Save As')
self.saveas_btn.setDefault(False)
self.saveas_btn.clicked.connect(lambda checked: self.save_as())

self.close_btn = QPushButton('Close')
self.close_btn.setDefault(True)
self.close_btn.clicked.connect(self.close)

self.copy_btn = QPushButton('Copy')
self.copy_btn.setDefault(False)
self.copy_btn.clicked.connect(self.copy_to_clipboard)

button_box = QDialogButtonBox()
button_box.addButton(self.copy_btn, button_box.ActionRole)
button_box.addButton(self.saveas_btn, button_box.ActionRole)
button_box.addButton(self.close_btn, button_box.AcceptRole)

# self.setCentralWidget(self.std_console)
layout = QGridLayout(self)
layout.addWidget(self.std_console, 0, 0)
layout.addWidget(button_box, 1, 0)

def write(self, text):
self.std_console.write(text)

def plain_text(self):
"""
Return the content of the console as plain text.
"""
return self.std_console.toPlainText()

def save_as(self):
"""
Save the content of the console to a text file.
"""
now = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
filename = osp.join(
osp.expanduser('~'),
'HydrogeolabLog_{}.txt'.format(now)
)
filename = self.savefile_manager.save_file_as(
filename, self.plain_text())

def copy_to_clipboard(self):
"""
Copy the content of the console on the clipboard.
"""
QApplication.clipboard().clear()
QApplication.clipboard().setText(self.plain_text())

def show(self):
"""
Override Qt method.
"""
if self.windowState() == Qt.WindowMinimized:
self.setWindowState(Qt.WindowNoState)
super().show()
self.activateWindow()
self.raise_()


if __name__ == '__main__':
from qtapputils.qthelpers import create_application
app = create_application()

console = SystemMessageDialog()
console.show()

sys.exit(app.exec_())
Loading