From 827905e862069482b82aecd4d14686676c88ec8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lio=20Guilherme?= Date: Tue, 10 Oct 2023 02:18:00 +0100 Subject: [PATCH] Improve Update and allows to upgrade and install from development (#2653) * Adds check for development version and dialog to install new, WIP * Add upgrade instalation and upgrade from development branch actions * Fix unit tests --- CHANGELOG.adoc | 3 + src/robotide/application/CHANGELOG.html | 5 +- src/robotide/application/releasenotes.py | 5 +- src/robotide/application/updatenotifier.py | 118 ++++++++++++++++++++- src/robotide/postinstall/__main__.py | 8 +- src/robotide/ui/mainframe.py | 26 +++-- src/robotide/utils/versioncomparator.py | 10 +- src/robotide/version.py | 2 +- utest/application/test_updatenotifier.py | 36 ++++++- 9 files changed, 190 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 7d82f92d1..64d8ada88 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -10,6 +10,9 @@ and this project adheres to http://semver.org/spec/v2.0.0.html[Semantic Versioni === Added +- Added auto update check when development version is installed +- Added menu option ``Help->Check for Upgrade`` which allows to force update check and install development version +- Added ``Upgrade Now`` action to update dialog. - Added Test Tags field (new, since Robot Framework 6.0) to Test Suites settings. This field will replace Default and Force Tags settings, after Robot Framework 7.0 - Added content help pop-up on Text Editor by pressing ``Ctrl`` for text at cursor position or selected autocomplete list item diff --git a/src/robotide/application/CHANGELOG.html b/src/robotide/application/CHANGELOG.html index 15661cb78..9f97ae9b4 100644 --- a/src/robotide/application/CHANGELOG.html +++ b/src/robotide/application/CHANGELOG.html @@ -1,7 +1,10 @@ Changelog

Changelog


All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

1.1. Added

    -
  • Added Test Tags field (new, since Robot Framework 6.0) to Test Suites settings. This field will replace Default and Force Tags settings, after Robot Framework 7.0
  • +
  • Added auto update check when development version is installed +
  • Added menu option ``Help->Check for Upgrade`` which allows to force update check and install development version +
  • Added ``Upgrade Now`` action to update dialog. +
  • Added Test Tags field (new, since Robot Framework 6.0) to Test Suites settings. This field will replace Default and Force Tags settings, after Robot Framework 7.0
  • Added content help pop-up on Text Editor by pressing ``Ctrl`` for text at cursor position or selected autocomplete list item
  • diff --git a/src/robotide/application/releasenotes.py b/src/robotide/application/releasenotes.py index ece29db8d..b904ca498 100644 --- a/src/robotide/application/releasenotes.py +++ b/src/robotide/application/releasenotes.py @@ -168,6 +168,9 @@ def set_content(self, html_win, content):

New Features and Fixes Highlights

    +
  • Added auto update check when development version is installed
  • +
  • Added menu option Help->Check for Upgrade which allows to force update check and install development version
  • +
  • Added Upgrade Now action to update dialog.
  • Added Test Tags field (new, since Robot Framework 6.0) to Test Suites settings. This field will replace Default and Force Tags settings, after Robot Framework 7.0
  • Improved RIDE Log and Parser Log windows to allow Zoom In/Out with Ctrl-Mouse Wheel
  • Hide continuation markers in Project Tree
  • @@ -243,6 +246,6 @@ def set_content(self, html_win, content):
     python -m robotide.postinstall -install
     
    -

    RIDE {VERSION} was released on 7/Oct/2023.

    +

    RIDE {VERSION} was released on 10/Oct/2023.

""" diff --git a/src/robotide/application/updatenotifier.py b/src/robotide/application/updatenotifier.py index e5474cdb4..14f6312df 100644 --- a/src/robotide/application/updatenotifier.py +++ b/src/robotide/application/updatenotifier.py @@ -12,7 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import subprocess +import sys # Configure wx uversion to allow running test app in __main__ @@ -20,12 +21,14 @@ import urllib.request as urllib2 import xmlrpc.client as xmlrpclib +import psutil import wx from wx import Colour from .. import version -from ..utils.versioncomparator import cmp_versions +from ..utils.versioncomparator import cmp_versions, parse_version from ..widgets import ButtonWithHandler, HtmlWindow, RIDEDialog +from ..postinstall.__main__ import MessageDialog _CHECK_FOR_UPDATES_SETTING = "check for updates" _LAST_UPDATE_CHECK_SETTING = "last update check" @@ -39,9 +42,16 @@ class UpdateNotifierController(object): def __init__(self, settings): self._settings = settings - def notify_update_if_needed(self, update_notification_callback): - if self._should_check() and self._is_new_version_available(): + def notify_update_if_needed(self, update_notification_callback, ignore_check_condition=False): + if ignore_check_condition: + dev_version = checking_version = True + else: + checking_version = self._should_check() + dev_version = parse_version(self.VERSION).is_devrelease + if checking_version and self._is_new_version_available(): update_notification_callback(self._newest_version, self._download_url, self._settings) + if checking_version and dev_version: + upgrade_from_dev_dialog(version_installed=self.VERSION) def _should_check(self): if self._settings.get(_CHECK_FOR_UPDATES_SETTING, None) is None: @@ -82,6 +92,91 @@ def _get_response(params, method): return xml +def upgrade_from_dev_dialog(version_installed): + VERSION = None + dev_version = urllib2.urlopen('https://raw.githubusercontent.com/robotframework/' + 'RIDE/master/src/robotide/version.py', timeout=1).read().decode('utf-8') + master_code = compile(dev_version, 'version', 'exec') + main_dict = {'VERSION': VERSION} + exec(master_code, main_dict) # defines VERSION + if cmp_versions(version_installed, main_dict['VERSION']) == -1: + # Here is the Menu Help->Upgrade insertion part, try to highlight menu + if not _askyesno("Upgrade?", f"New development version is available.\nYou may install" + f" version {main_dict['VERSION']} with:\npip install -U https://github.com/" + f"robotframework/RIDE/archive/master.zip", wx.GetActiveWindow()): + return False + else: + command = sys.executable + " -m pip install -U https://github.com/robotframework/RIDE/archive/master.zip" + do_upgrade(command) + return True + else: + _askyesno("No Upgrade Available", "You have the latest version of RIDE.\nHave a nice day :)", + wx.GetActiveWindow()) + return False + + +def _askyesno(title, message, frame=None): + if frame is None: + _ = wx.App() + parent = wx.Frame(None, size=(0, 0)) + else: + parent = wx.Frame(frame, size=(0, 0)) + parent.CenterOnScreen() + dlg = MessageDialog(parent, message, title, ttl=8) + dlg.Fit() + result = dlg.ShowModal() in [wx.ID_YES, wx.ID_OK] + # print("DEBUG: updatenotifier _askyesno Result %s" % result) + if dlg: + dlg.Destroy() + # parent.Destroy() + return result + + +def _add_content_to_clipboard(content): + wx.TheClipboard.Open() + wx.TheClipboard.SetData(wx.TextDataObject(content)) + wx.TheClipboard.Close() + + +def do_upgrade(command): + _add_content_to_clipboard(command) + # print("DEBUG: Here will be the installation step.") + my_pid = psutil.Process() + my_pip = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result = None + count = 0 + while not result and count < 60: + count += 1 + outs, errs = my_pip.communicate() + # DEBUG: Add output to a notebook tab + print(f"{outs}\n") + result = my_pip.returncode + if result == 0: + break + """ DEBUG: need to get outs line by line + except subprocess.SubprocessError: + my_pip.kill() + outs, errs = my_pip.communicate() + result = False + # DEBUG: Add output to a notebook tab + print(f"{outs}\n") + print(f"{errs}\n") + """ + time.sleep(1) + if result != 0: + _askyesno("Failed to Upgrade", "An error occurred when installing new version", + wx.GetActiveWindow()) + return False + command = sys.executable + " -m robotide.__init__ --noupdatecheck" + wx.CallLater(500, subprocess.Popen, command.split(' '), start_new_session=True) + # Wait 10 seconds before trying to kill this process + """ Not working well: + wx.CallLater(10000, psutil.Process.kill, my_pid.pid) + """ + _askyesno("Completed Upgrade", f"You should close this RIDE (Process ID = {my_pid.pid})", + wx.GetActiveWindow()) + + class LocalHtmlWindow(HtmlWindow): def __init__(self, parent, size=(600, 400)): HtmlWindow.__init__(self, parent, size) @@ -96,6 +191,7 @@ class UpdateDialog(RIDEDialog): def __init__(self, uversion, url, settings): self._settings = settings + self._command = sys.executable + f" -m pip install -U robotframework-ride=={uversion}" RIDEDialog.__init__(self, title="Update available", size=(400, 400), style=wx.DEFAULT_FRAME_STYLE | wx.FRAME_FLOAT_ON_PARENT) # set Left to Right direction (while we don't have localization) @@ -117,10 +213,17 @@ def __init__(self, uversion, url, settings): "do not need automatic update checks") checkbox.Bind(wx.EVT_CHECKBOX, handler=self.on_checkbox_change) sizer.Add(checkbox) + hsizer = wx.BoxSizer(orient=wx.HORIZONTAL) button = ButtonWithHandler(self, label="remind me later", handler=self.on_remind_me_later) button.SetBackgroundColour(Colour(self.color_secondary_background)) button.SetForegroundColour(Colour(self.color_secondary_foreground)) - sizer.Add(button) + hsizer.Add(button) + hsizer.AddSpacer(50) + up_button = ButtonWithHandler(self, label="Upgrade Now", handler=self.on_upgrade_now) + up_button.SetBackgroundColour(Colour(self.color_secondary_background)) + up_button.SetForegroundColour(Colour(self.color_secondary_foreground)) + hsizer.Add(up_button) + sizer.Add(hsizer) self.SetSizer(sizer) self.CentreOnParent(wx.BOTH) self.Fit() @@ -135,3 +238,8 @@ def on_remind_me_later(self, event): def on_checkbox_change(self, event): self._settings[_CHECK_FOR_UPDATES_SETTING] = not event.IsChecked() event.Skip() + + def on_upgrade_now(self, event): + _ = event + _add_content_to_clipboard(self._command) + do_upgrade(self._command) diff --git a/src/robotide/postinstall/__main__.py b/src/robotide/postinstall/__main__.py index aa16cf8fd..013fdec92 100755 --- a/src/robotide/postinstall/__main__.py +++ b/src/robotide/postinstall/__main__.py @@ -72,11 +72,13 @@ def __init__(self, parent, message, title, ttl=10): self.CenterOnScreen(wx.BOTH) self.timeToLive = ttl - - st_msg = wx.StaticText(self, -1, message) + st_msg = [] + for msg in message.split('\n'): + st_msg.append(wx.StaticText(self, -1, msg)) self.settimetolivemsg = wx.StaticText(self, -1, 'Closing this dialog box in %ds...' % self.timeToLive) vbox = wx.BoxSizer(wx.VERTICAL) - vbox.Add(st_msg, 0, wx.ALIGN_CENTER | wx.TOP, 40) + for sp_msg in st_msg: + vbox.Add(sp_msg, 0, wx.ALIGN_LEFT | wx.TOP, 10) vbox.Add(self.settimetolivemsg, 0, wx.ALIGN_CENTER | wx.TOP, 10) self.SetSizer(vbox) self.SetAffirmativeId(wx.ID_OK) diff --git a/src/robotide/ui/mainframe.py b/src/robotide/ui/mainframe.py index 94069ca58..36b8df3ec 100644 --- a/src/robotide/ui/mainframe.py +++ b/src/robotide/ui/mainframe.py @@ -67,6 +67,7 @@ !Report a Problem | Open browser to SEARCH on the RIDE issue tracker !Release notes | Shows release notes !About | Information about RIDE +!Check for Upgrade | Looks at PyPi for new released version """ ID_CustomizeToolbar = wx.ID_HIGHEST + 1 @@ -159,15 +160,16 @@ def _show_modification_prevented_error(message): style=wx.ICON_ERROR) def _init_ui(self): - # self.aui_mgr.AddPane(wx.Panel(self), aui.AuiPaneInfo().CenterPane()) - # #### self.splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE) + """ DEBUG: + self.aui_mgr.AddPane(wx.Panel(self), aui.AuiPaneInfo().CenterPane()) + self.splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE) + """ self.aui_mgr.AddPane(wx.Panel(self), aui.AuiPaneInfo().Name("right_pane").Right()) # set up default notebook style self._notebook_style = (aui.AUI_NB_DEFAULT_STYLE | aui.AUI_NB_WINDOWLIST_BUTTON | aui.AUI_NB_TAB_EXTERNAL_MOVE | aui.AUI_NB_SUB_NOTEBOOK | aui.AUI_NB_SMART_TABS) - # DEBUG self._notebook_theme = 0 (allow to select themes for notebooks) - # self.notebook = NoteBook(self.splitter, self._application, - # self._notebook_style) + # DEBUG: self._notebook_theme = 0 (allow to select themes for notebooks) + # DEBUG:self.notebook = NoteBook(self.splitter, self._application, self._notebook_style) self.notebook = NoteBook(self, self._application, self._notebook_style) self.notebook.SetBackgroundColour(Colour(self.color_background)) @@ -504,13 +506,21 @@ def on_about(event): dlg.ShowModal() dlg.Destroy() + def on_check_for_upgrade(self, event): + _ = event + from ..application.updatenotifier import UpdateNotifierController, UpdateDialog + wx.CallAfter(UpdateNotifierController(self.general_settings).notify_update_if_needed, + UpdateDialog, ignore_check_condition=True) + # @staticmethod def on_shortcut_keys(self, event): _ = event dialog = ShortcutKeysDialog() - # self.aui_mgr.AddPane(dialog.GetContentWindow(),aui.AuiPaneInfo().Name("shortcuts").Caption("Shortcuts Keys"). - # CloseButton(True).RightDockable().Floatable().Float(), self.notebook) - # self.aui_mgr.Update() + """ DEBUG: + self.aui_mgr.AddPane(dialog.GetContentWindow(),aui.AuiPaneInfo().Name("shortcuts").Caption("Shortcuts Keys") + .CloseButton(True).RightDockable().Floatable().Float(), self.notebook) + self.aui_mgr.Update() + """ dialog.Show() @staticmethod diff --git a/src/robotide/utils/versioncomparator.py b/src/robotide/utils/versioncomparator.py index f704e89b0..d7844fa7e 100644 --- a/src/robotide/utils/versioncomparator.py +++ b/src/robotide/utils/versioncomparator.py @@ -13,7 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pkg_resources import parse_version +try: + from pkg_resources import parse_version +except ImportError: + try: + from packaging.version import parse as parse_version + except ImportError as e: + print("RIDE cannot verify versions upgrade because of missing packages." + "You can install missing package with:\npip install packaging\nor\npip install setuptools") + raise e def cmp_versions(version1, version2): diff --git a/src/robotide/version.py b/src/robotide/version.py index a92a18e61..db3167d90 100644 --- a/src/robotide/version.py +++ b/src/robotide/version.py @@ -14,4 +14,4 @@ # limitations under the License. # # Automatically generated by `tasks.py`. -VERSION = 'v2.0.8dev19' +VERSION = 'v2.0.8dev20' diff --git a/utest/application/test_updatenotifier.py b/utest/application/test_updatenotifier.py index 485be03e3..9ed9e1575 100644 --- a/utest/application/test_updatenotifier.py +++ b/utest/application/test_updatenotifier.py @@ -16,12 +16,14 @@ import unittest import time import urllib +import wx from robotide.application.updatenotifier import UpdateNotifierController CHECKFORUPDATES = 'check for updates' LASTUPDATECHECK = 'last update check' + class UpdateNotifierTestCase(unittest.TestCase): def setUp(self): @@ -113,15 +115,16 @@ def test_first_run_sets_settings_correctly_and_finds_an_update(self): self.assertTrue(self._callback_called) def test_checking_timeouts(self): + app = wx.App() settings = self.internal_settings() ctrl = UpdateNotifierController(settings) def throw_timeout_error(): - raise urllib.URLError('timeout') + raise urllib.error.URLError('timeout') ctrl._get_newest_version = throw_timeout_error ctrl.notify_update_if_needed(self._callback) - self.assertTrue(settings[LASTUPDATECHECK] > time.time() - 1) + self.assertTrue(settings[LASTUPDATECHECK] > time.time() - 10) # The dialog timeout in 10 seconds self.assertFalse(self._callback_called) def test_download_url_checking_timeouts(self): @@ -132,7 +135,7 @@ def test_download_url_checking_timeouts(self): def throw_timeout_error(*args): _ = args - raise urllib.URLError('timeout') + raise urllib.error.URLError('timeout') ctrl._get_download_url = throw_timeout_error ctrl.notify_update_if_needed(self._callback) @@ -155,6 +158,33 @@ def test_server_returns_older_version(self): self.assertTrue(settings[CHECKFORUPDATES]) self.assertFalse(self._callback_called) + def test_forced_check_released(self): + app = wx.App() + settings = self.internal_settings() + ctrl = self._update_notifier_controller(settings, '0.43.0', '0.43.1') + ctrl.notify_update_if_needed(self._callback, ignore_check_condition=True) + self.assertTrue(settings[LASTUPDATECHECK] > time.time() - 9) # The dialog timeout in 10 seconds + self.assertTrue(settings[CHECKFORUPDATES]) + self.assertTrue(self._callback_called) + + def test_forced_check_development(self): + app = wx.App() + settings = self.internal_settings() + ctrl = self._update_notifier_controller(settings, '0.44dev12', '0.44.dev14') + ctrl.notify_update_if_needed(self._callback, ignore_check_condition=True) + self.assertTrue(settings[LASTUPDATECHECK] > time.time() - 10) # The dialog timeout in 10 seconds + self.assertTrue(settings[CHECKFORUPDATES]) + self.assertTrue(self._callback_called) + + def test_forced_check_development_ok(self): + app = wx.App() + settings = self.internal_settings() + ctrl = self._update_notifier_controller(settings, '0.44dev12', '0.44.dev12') + ctrl.notify_update_if_needed(self._callback, ignore_check_condition=False) + self.assertTrue(settings[LASTUPDATECHECK] > time.time() - 10) # The dialog timeout in 10 seconds + self.assertTrue(settings[CHECKFORUPDATES]) + self.assertFalse(self._callback_called) + if __name__ == '__main__': unittest.main()