From f865b87a749e185088921dffda03aebad5886493 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 10 Apr 2015 07:52:06 +0200 Subject: [PATCH] Show a message and update notifier on reports. Fixes #340. Fixes #447. See #429. --- qutebrowser/misc/autoupdate.py | 101 ++++++++++++++++++++++++++++++++ qutebrowser/misc/crashdialog.py | 53 ++++++++++++++++- qutebrowser/misc/msgbox.py | 68 +++++++++++++++++++++ 3 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 qutebrowser/misc/autoupdate.py create mode 100644 qutebrowser/misc/msgbox.py diff --git a/qutebrowser/misc/autoupdate.py b/qutebrowser/misc/autoupdate.py new file mode 100644 index 000000000..370de0f06 --- /dev/null +++ b/qutebrowser/misc/autoupdate.py @@ -0,0 +1,101 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014-2015 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Classes related to auto-updating and getting the latest version.""" + +import json +import functools + +from PyQt5.QtCore import pyqtSignal, QObject, QUrl +from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest, + QNetworkReply) + + +class PyPIVersionClient(QObject): + + """A client for the PyPI API using QNetworkAccessManager. + + It gets the latest version of qutebrowser from PyPI. + + Attributes: + _nam: The QNetworkAccessManager used. + + Class attributes: + API_URL: The base API URL. + + Signals: + success: Emitted when getting the version info succeeded. + arg: The newest version. + error: Emitted when getting the version info failed. + arg: The error message, as string. + """ + + API_URL = 'https://pypi.python.org/pypi/{}/json' + success = pyqtSignal(str) + error = pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self._nam = QNetworkAccessManager(self) + + def get_version(self, package='qutebrowser'): + """Get the newest version of a given package. + + Emits success/error when done. + + Args: + package: The name of the package to check. + """ + url = QUrl(self.API_URL.format(package)) + request = QNetworkRequest(url) + reply = self._nam.get(request) + if reply.isFinished(): + self.on_reply_finished(reply) + else: + reply.finished.connect(functools.partial( + self.on_reply_finished, reply)) + + def on_reply_finished(self, reply): + """When the reply finished, load and parse the json data. + + Then emits error/success. + + Args: + reply: The QNetworkReply which finished. + """ + if reply.error() != QNetworkReply.NoError: + self.error.emit(reply.errorString()) + return + try: + data = bytes(reply.readAll()).decode('utf-8') + except UnicodeDecodeError as e: + self.error.emit("Invalid UTF-8 data received in reply: " + "{}!".format(e)) + return + try: + json_data = json.loads(data) + except ValueError as e: + self.error.emit("Invalid JSON received in reply: {}!".format(e)) + return + try: + self.success.emit(json_data['info']['version']) + except KeyError as e: + self.error.emit("Malformed data recieved in reply " + "({!r} not found)!".format(e)) + return diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 4a5a4f0ea..bc00bce06 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -24,6 +24,8 @@ import sys import html import getpass import traceback +import distutils.version # pylint: disable=no-name-in-module,import-error +# https://bitbucket.org/logilab/pylint/issue/73/ from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, @@ -32,7 +34,7 @@ from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, import qutebrowser from qutebrowser.utils import version, log, utils, objreg, qtutils -from qutebrowser.misc import miscwidgets +from qutebrowser.misc import miscwidgets, autoupdate, msgbox from qutebrowser.browser.network import pastebin from qutebrowser.config import config @@ -103,6 +105,7 @@ class _CrashDialog(QDialog): _url: Pastebin URL QLabel. _crash_info: A list of tuples with title and crash information. _paste_client: A PastebinClient instance to use. + _pypi_client: A PyPIVersionClient instance to use. _paste_text: The text to pastebin. """ @@ -125,6 +128,7 @@ class _CrashDialog(QDialog): self.resize(QSize(640, 600)) self._vbox = QVBoxLayout(self) self._paste_client = pastebin.PastebinClient(self) + self._pypi_client = autoupdate.PyPIVersionClient(self) self._init_text() contact = QLabel("I'd like to be able to follow up with you, to keep " @@ -293,10 +297,17 @@ class _CrashDialog(QDialog): self._btn_report.setEnabled(False) self._btn_cancel.setEnabled(False) self._btn_report.setText("Reporting...") - self._paste_client.success.connect(self.finish) + self._paste_client.success.connect(self.on_paste_success) self._paste_client.error.connect(self.show_error) self.report() + @pyqtSlot() + def on_paste_success(self): + """Get the newest version from PyPI when the paste is done.""" + self._pypi_client.success.connect(self.on_version_success) + self._pypi_client.error.connect(self.on_version_error) + self._pypi_client.get_version() + @pyqtSlot(str) def show_error(self, text): """Show a paste error dialog. @@ -308,6 +319,44 @@ class _CrashDialog(QDialog): error_dlg.finished.connect(self.finish) error_dlg.show() + @pyqtSlot(str) + def on_version_success(self, newest): + """Called when the version was obtained from self._pypi_client. + + Args: + newest: The newest version as a string. + """ + # pylint: disable=no-member + # https://bitbucket.org/logilab/pylint/issue/73/ + new_version = distutils.version.StrictVersion(newest) + cur_version = distutils.version.StrictVersion(qutebrowser.__version__) + lines = ['The report has been sent successfully. Thanks!'] + if new_version > cur_version: + lines.append("Note: The newest available version is v{}, " + "but you're currently running v{} - please " + "update!".format(newest, qutebrowser.__version__)) + text = '

'.join(lines) + self.hide() + msgbox.information(self, "Report successfully sent!", text, + on_finished=self.finish, plain_text=False) + + @pyqtSlot(str) + def on_version_error(self, msg): + """Called when the version was not obtained from self._pypi_client. + + Args: + msg: The error message to show. + """ + lines = ['The report has been sent successfully. Thanks!'] + lines.append("There was an error while getting the newest version: " + "{}. Please check for a new version on " + "qutebrowser.org " + "by yourself.".format(msg)) + text = '

'.join(lines) + self.hide() + msgbox.information(self, "Report successfully sent!", text, + on_finished=self.finish, plain_text=False) + @pyqtSlot() def finish(self): """Save contact info and close the dialog.""" diff --git a/qutebrowser/misc/msgbox.py b/qutebrowser/misc/msgbox.py new file mode 100644 index 000000000..406e4e0bf --- /dev/null +++ b/qutebrowser/misc/msgbox.py @@ -0,0 +1,68 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Convenience functions to show message boxes.""" + + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMessageBox + + +def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok, + on_finished=None, plain_text=None): + """Display an QMessageBox with the given icon. + + Args: + parent: The parent to set for the message box. + title: The title to set. + text: The text to set. + buttons: The buttons to set (QMessageBox::StandardButtons) + on_finished: A slot to connect to the 'finished' signal. + plain_text: Whether to force plain text (True) or rich text (False). + None (the default) uses Qt's auto detection. + + Return: + A new QMessageBox. + """ + box = QMessageBox(parent) + box.setIcon(icon) + box.setStandardButtons(buttons) + if on_finished is not None: + box.finished.connect(on_finished) + if plain_text: + box.setTextFormat(Qt.PlainText) + elif plain_text is not None: + box.setTextFormat(Qt.RichText) + box.setWindowTitle(title) + box.setText(text) + box.show() + return box + + +def information(*args, **kwargs): + """Display an information box. + + Args: + *args: Passed to msgbox. + **kwargs: Passed to msgbox. + + Return: + A new QMessageBox. + """ + return msgbox(*args, icon=QMessageBox.Information, **kwargs)