-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a check for updates method #2321
Changes from 2 commits
ea1a37d
092502c
cb14066
9a10da9
ecc948d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,7 +79,7 @@ | |
QKeySequence, QDockWidget, QAction, | ||
QDesktopServices) | ||
from spyderlib.qt.QtCore import (Signal, QPoint, Qt, QSize, QByteArray, QUrl, | ||
Slot, QTimer, QCoreApplication) | ||
Slot, QTimer, QCoreApplication, QThread) | ||
from spyderlib.qt.compat import (from_qvariant, getopenfilename, | ||
getsavefilename) | ||
# Avoid a "Cannot mix incompatible Qt library" error on Windows platforms | ||
|
@@ -358,7 +358,12 @@ def __init__(self, options=None): | |
# Tour # TODO: Should I consider it a plugin?? or? | ||
self.tour = None | ||
self.tours_available = None | ||
|
||
|
||
# Check for updates Thread and Worker | ||
self.check_updates_action = None | ||
self.thread_updates = None | ||
self.worker_updates = None | ||
|
||
# Preferences | ||
from spyderlib.plugins.configdialog import (MainConfigPage, | ||
ColorSchemeConfigPage) | ||
|
@@ -942,6 +947,10 @@ def create_edit_action(text, tr_text, icon_name): | |
support_action = create_action(self, | ||
_("Spyder support..."), | ||
triggered=self.google_group) | ||
self.check_updates_action = create_action(self, | ||
_("Check for updates"), | ||
triggered=self.check_updates) | ||
|
||
# Spyder documentation | ||
doc_path = get_module_data_path('spyderlib', relpath="doc", | ||
attr_name='DOCPATH') | ||
|
@@ -993,7 +1002,8 @@ def trigger(i=i, self=self): # closure needed! | |
|
||
self.help_menu_actions = [doc_action, tut_action, self.tours_menu, | ||
None, | ||
report_action, dep_action, support_action, | ||
report_action, dep_action, | ||
support_action, self.check_updates_action, | ||
None] | ||
# Python documentation | ||
if get_python_doc_path() is not None: | ||
|
@@ -1273,8 +1283,12 @@ def post_visible_setup(self): | |
except AttributeError: | ||
pass | ||
|
||
# Check for spyder updates | ||
if DEV is None and CONF.get('main', 'check_updates_on_startup'): | ||
self.check_updates() | ||
|
||
self.is_setting_up = False | ||
|
||
def load_window_settings(self, prefix, default=False, section='main'): | ||
"""Load window layout settings from userconfig-based configuration | ||
with *prefix*, under *section* | ||
|
@@ -2660,6 +2674,93 @@ def show_tour(self, index): | |
self.tour.set_tour(index, frames, self) | ||
self.tour.start_tour() | ||
|
||
# ---- Check for Spyder Updates | ||
def _check_updates_ready(self): | ||
"""Called by WorkerUpdates when ready""" | ||
from spyderlib.workers.updates import MessageCheckBox | ||
|
||
update_available = self.worker_updates.update_available | ||
latest_release = self.worker_updates.latest_release | ||
feedback = self.worker_updates.feedback | ||
error_msg = self.worker_updates.error | ||
|
||
url_r = 'https://github.com/spyder-ide/spyder/releases' | ||
url_i = 'http://pythonhosted.org/spyder/installation.html' | ||
|
||
# Define the custom QMessageBox | ||
box = MessageCheckBox() | ||
box.setWindowTitle(_("Spyder updates")) | ||
box.set_checkbox_text(_("Check for updates on startup")) | ||
box.setStandardButtons(QMessageBox.Ok) | ||
box.setDefaultButton(QMessageBox.Ok) | ||
box.setIcon(QMessageBox.Information) | ||
|
||
# Adjust the checkbox depending on the stored configuration | ||
section, option = 'main', 'check_updates_on_startup' | ||
check_updates = CONF.get(section, option) | ||
box.set_checked(check_updates) | ||
|
||
if error_msg is not None: | ||
msg = error_msg | ||
box.setText(msg) | ||
box.set_check_visible(False) | ||
box.exec_() | ||
check_updates = box.is_checked() | ||
else: | ||
if update_available: | ||
msg = _("<b>Spyder {0} is available!</b> <br><br>Please use " | ||
"your package manager to update Spyder or go to our " | ||
"<a href=\"{1}\">Releases</a> page to download this " | ||
"new version. <br><br>If you are not sure how to " | ||
"proceed to update Spyder please refer to our " | ||
" <a href=\"{2}\">Installation</a> instructions." | ||
"" .format(latest_release, url_r, url_i)) | ||
box.setText(msg) | ||
box.set_check_visible(True) | ||
box.exec_() | ||
check_updates = box.is_checked() | ||
elif feedback: | ||
msg = _("Spyder is up to date.") | ||
box.setText(msg) | ||
box.set_check_visible(False) | ||
box.exec_() | ||
check_updates = box.is_checked() | ||
|
||
# Update checkbox based on user interaction | ||
CONF.set(section, option, check_updates) | ||
|
||
# Enable check_updates_action after the thread has finished | ||
self.check_updates_action.setDisabled(False) | ||
|
||
def check_updates(self): | ||
""" | ||
Check for spyder updates on github releases using a QThread. | ||
""" | ||
from spyderlib.workers.updates import WorkerUpdates | ||
|
||
# Disable check_updates_action while the thread is working | ||
self.check_updates_action.setDisabled(True) | ||
|
||
# feedback` = False is used on startup, so only positive feedback is | ||
# given. `feedback` = True is used when using the menu action, and | ||
# gives feeback if updates are, or are not found. | ||
feedback = False | ||
|
||
if DEV: | ||
feedback = True | ||
elif self.thread_updates is not None: | ||
self.thread_updates.terminate() | ||
feedback = True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't feel at ease with this But wouldn't it be better to move this block to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, you are right, these are the leftovers of the original way I did it, when the method accepted parameters. Will move it |
||
|
||
self.thread_updates = QThread(self) | ||
self.worker_updates = WorkerUpdates(self, feedback) | ||
self.worker_updates.sig_ready.connect(self._check_updates_ready) | ||
self.worker_updates.sig_ready.connect(self.thread_updates.quit) | ||
self.worker_updates.moveToThread(self.thread_updates) | ||
self.thread_updates.started.connect(self.worker_updates.start) | ||
self.thread_updates.start() | ||
|
||
|
||
#============================================================================== | ||
# Utilities to create the 'main' function | ||
#============================================================================== | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright © 2009-2013 Pierre Raybaut | ||
# Copyright © 2013-2015 The Spyder Development Team | ||
# Licensed under the terms of the MIT License | ||
# (see spyderlib/__init__.py for details) | ||
|
||
import json | ||
|
||
|
||
from spyderlib import __version__ | ||
from spyderlib.baseconfig import _ | ||
from spyderlib.py3compat import PY3 | ||
from spyderlib.qt.QtGui import QMessageBox, QCheckBox, QSpacerItem, QVBoxLayout | ||
from spyderlib.qt.QtCore import Signal, Qt, QObject | ||
from spyderlib.utils.programs import check_version | ||
|
||
|
||
if PY3: | ||
from urllib.request import urlopen | ||
from urllib.error import URLError, HTTPError | ||
else: | ||
from urllib2 import urlopen, URLError, HTTPError | ||
|
||
|
||
class MessageCheckBox(QMessageBox): | ||
""" | ||
A QMessageBox derived widget that includes a QCheckBox aligned to the right | ||
under the message and on top of the buttons. | ||
""" | ||
def __init__(self, *args, **kwargs): | ||
super(MessageCheckBox, self).__init__(*args, **kwargs) | ||
|
||
self._checkbox = QCheckBox() | ||
|
||
# Set layout to include checkbox | ||
size = 9 | ||
check_layout = QVBoxLayout() | ||
check_layout.addItem(QSpacerItem(size, size)) | ||
check_layout.addWidget(self._checkbox, 0, Qt.AlignRight) | ||
check_layout.addItem(QSpacerItem(size, size)) | ||
|
||
# Access the Layout of the MessageBox to add the Checkbox | ||
layout = self.layout() | ||
layout.addLayout(check_layout, 1, 1) | ||
|
||
# --- Public API | ||
# Methods to access the checkbox | ||
def is_checked(self): | ||
return self._checkbox.isChecked() | ||
|
||
def set_checked(self, value): | ||
return self._checkbox.setChecked(value) | ||
|
||
def set_check_visible(self, value): | ||
self._checkbox.setVisible(value) | ||
|
||
def is_check_visible(self): | ||
self._checkbox.isVisible() | ||
|
||
def checkbox_text(self): | ||
self._checkbox.text() | ||
|
||
def set_checkbox_text(self, text): | ||
self._checkbox.setText(text) | ||
|
||
|
||
class WorkerUpdates(QObject): | ||
""" | ||
Worker that checks for releases using the Github API without blocking the | ||
Spyder user interface, in case of connections issues. | ||
""" | ||
sig_ready = Signal() | ||
|
||
def __init__(self, parent, feedback): | ||
QObject.__init__(self) | ||
self._parent = parent | ||
self.feedback = feedback | ||
self.error = feedback | ||
self.latest_release = None | ||
|
||
def is_stable_version(self, version): | ||
""" | ||
A stable version has no letters in the final part, it has only numbers. | ||
|
||
Stable version example: 1.2, 1.3.4, 1.0.5 | ||
Not stable version: 1.2alpha, 1.3.4beta, 0.1.0rc1, 3.0.0dev | ||
""" | ||
if not isinstance(version, tuple): | ||
version = version.split('.') | ||
last_part = version[-1] | ||
|
||
try: | ||
int(last_part) | ||
return True | ||
except ValueError: | ||
return False | ||
|
||
def check_update_available(self, version, releases): | ||
"""Checks if there is an update available. | ||
|
||
It takes as parameters the current version of Spyder and a list of | ||
valid cleaned releases in chronological order (what github api returns | ||
by default). Example: ['2.3.4', '2.3.3' ...]""" | ||
if self.is_stable_version(version): | ||
# Remove non stable versions from the list | ||
releases = [r for r in releases if self.is_stable_version(r)] | ||
|
||
latest_release = releases[0] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you need to define two different variables that hold the same thing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the little hack that adds a |
||
|
||
if version.endswith('dev'): | ||
return (False, latest_release) | ||
|
||
# check_version is based on LooseVersion, so a small hack is needed so | ||
# that LooseVersion understands that '3.0.0' is in fact bigger than | ||
# '3.0.0rc1' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One last thing: please move this hack to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bear in mind, that also, |
||
if self.is_stable_version(latest_release) and \ | ||
version.startswith(latest_release) and latest_release != version: | ||
parts = latest_release.split('.') | ||
parts = parts[:-1] + [parts[-1] + 'z'] | ||
latest_mod = '.'.join(parts) | ||
else: | ||
latest_mod = releases[0] | ||
|
||
return (check_version(version, latest_mod, '<'), latest_release) | ||
|
||
def start(self): | ||
"""Main method of the WorkerUpdates worker""" | ||
self.url = 'https://api.github.com/repos/spyder-ide/spyder/releases' | ||
self.update_available = False | ||
self.latest_release = __version__ | ||
|
||
error_msg = None | ||
try: | ||
page = urlopen(self.url) | ||
try: | ||
data = page.read() | ||
if not isinstance(data, str): | ||
data = data.decode() | ||
data = json.loads(data) | ||
releases = [item['tag_name'].replace('v', '') for item in data] | ||
version = __version__ | ||
result = self.check_update_available(version, releases) | ||
self.update_available, self.latest_release = result | ||
except Exception: | ||
error_msg = _('Unable to retrieve information.') | ||
except HTTPError: | ||
error_msg = _('Unable to retrieve information.') | ||
except URLError: | ||
error_msg = _('Unable to connect to the internet. <br><br>Make ' | ||
'sure the connection is working properly.') | ||
except Exception: | ||
error_msg = _('Unable to check for updates.') | ||
|
||
self.error = error_msg | ||
self.sig_ready.emit() | ||
|
||
|
||
def test_msgcheckbox(): | ||
from spyderlib.utils.qthelpers import qapplication | ||
app = qapplication() | ||
app.setStyle('Plastique') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this style line? Just curious :-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ehh I copied from another file, and did not bother to change that :-p |
||
box = MessageCheckBox() | ||
box.setWindowTitle(_("Spyder updates")) | ||
box.setText("Testing checkbox") | ||
box.set_checkbox_text("Check for updates on startup?") | ||
box.setStandardButtons(QMessageBox.Ok) | ||
box.setDefaultButton(QMessageBox.Ok) | ||
box.setIcon(QMessageBox.Information) | ||
box.exec_() | ||
|
||
if __name__ == '__main__': | ||
test_msgcheckbox() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need this checkbox here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on @Nodd suggestions, having the checkbox there would make it easy for the user to disable the message in case he/she cannot update (due to admin rights.... or any other reason) spyder. I guess s not strictly necessary but it would improve user experience.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, a "Don't check updates" pushbutton would be better than a checkbox. If it can be at the opposite side thant the OK button it's even better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really dislike that idea...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is that ? Its like a "Leave me alone with your annoying popup" button.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the user clicks the Check for Updates on the menu, you would get a popup with a button saying do not check for updates.... instead of displaying the actual setting and allowing for the user to adapt it right there.
From the user experience point of view I think is much better to have the checkbox.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right, didn't think about manual checking. The checkbox is better then, I agree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A button will save you from one click, but would be detrimental for the user experience... I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, let's leave the checkbox then, given that @goanpeca and @Nodd think it's better ;-)