Skip to content
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

Merged
merged 5 commits into from
Apr 22, 2015
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion spyderlib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ def is_ubuntu():
'cpu_usage/timeout': 2000,
'use_custom_margin': True,
'custom_margin': 0,
'show_internal_console_if_traceback': True
'show_internal_console_if_traceback': True,
'check_updates_on_startup': True
}),
('quick_layouts',
{
Expand Down
11 changes: 10 additions & 1 deletion spyderlib/plugins/configdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,14 +757,23 @@ def setup_page(self):
debug_layout = QVBoxLayout()
debug_layout.addWidget(popup_console_box)
debug_group.setLayout(debug_layout)

# --- Spyder updates
update_group = QGroupBox(_("Updates"))
check_updates = newcb(_("Check for updates on startup"),
'check_updates_on_startup')
update_layout = QVBoxLayout()
update_layout.addWidget(check_updates)
update_group.setLayout(update_layout)

vlayout = QVBoxLayout()
vlayout.addWidget(interface_group)
vlayout.addWidget(sbar_group)
vlayout.addWidget(debug_group)
vlayout.addWidget(update_group)
vlayout.addStretch(1)
self.setLayout(vlayout)

def apply_settings(self, options):
self.main.apply_settings()

Expand Down
109 changes: 105 additions & 4 deletions spyderlib/spyder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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*
Expand Down Expand Up @@ -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()
Copy link
Member

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

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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...

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Member

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 ;-)

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel at ease with this feedback variable. It's passed to the worker, but it doesn't do anything with it. It seems the worker just holds it, so that _check_updates_ready can later read it.

But wouldn't it be better to move this block to _check_updates_ready directly and get rid of this passing around process?

Copy link
Member Author

Choose a reason for hiding this comment

The 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
#==============================================================================
Expand Down
Empty file added spyderlib/workers/__init__.py
Empty file.
173 changes: 173 additions & 0 deletions spyderlib/workers/updates.py
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]
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the little hack that adds a z to the version number... so in the case we have 3.0.0 vs 3.0.0rc2 then the comparions is made 3.0.0z > 3.0.0rc2, but I still need a reference to the original number without the z


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'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last thing: please move this hack to check_version. It'll be more useful there :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bear in mind, that also, is_stable_version will be moved to utils.programs

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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this style line? Just curious :-)

Copy link
Member Author

Choose a reason for hiding this comment

The 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()