Coverage for src/robotide/application/updatenotifier.py: 70%
190 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
1# Copyright 2008-2015 Nokia Networks
2# Copyright 2016- Robot Framework Foundation
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15import builtins 1ab
16import os 1ab
17import re 1ab
18import subprocess 1ab
19import sys 1ab
20import tempfile 1ab
21# Configure wx uversion to allow running test app in __main__
24import time 1ab
25import urllib.request as urllib2 1ab
26from dataclasses import dataclass 1ab
28import psutil 1ab
29import requests 1ab
30import wx 1ab
31from wx import Colour 1ab
32from os import path 1ab
34from .. import version 1ab
35from ..utils.versioncomparator import cmp_versions, parse_version 1ab
36from ..widgets import ButtonWithHandler, HtmlWindow, RIDEDialog 1ab
37from ..postinstall import MessageDialog 1ab
38from ..publish import PUBLISHER, RideRunnerStopped 1ab
40_ = wx.GetTranslation # To keep linter/code analyser happy 1ab
41builtins.__dict__['_'] = wx.GetTranslation 1ab
43_CHECK_FOR_UPDATES_SETTING = "check for updates" 1ab
44_LAST_UPDATE_CHECK_SETTING = "last update check" 1ab
45SPC = " " 1ab
48class UpdateNotifierController(object): 1ab
50 VERSION = version.VERSION 1ab
51 SECONDS_IN_WEEK = 60*60*24*7 1ab
53 def __init__(self, settings, notebook): 1ab
54 self._settings = settings 1arhikjedfsnoglpqm
55 self._notebook = notebook 1arhikjedfsnoglpqm
57 def notify_update_if_needed(self, update_notification_callback, ignore_check_condition=False, show_no_update=False): 1ab
58 if ignore_check_condition: 1rhikjedfsnoglpqm
59 dev_version = checking_version = True 1efg
60 else:
61 checking_version = self._should_check() 1rhikjdsnolpqm
62 dev_version = parse_version(self.VERSION).is_devrelease 1rhikjdsnolpqm
63 if checking_version and self._is_new_version_available(): 1rhikjedfsnoglpqm
64 update_notification_callback(self._newest_version, self._download_url, self._settings, self._notebook) 1jeflm
65 if checking_version and dev_version: 1rhikjedfsnoglpqm
66 upgrade_from_dev_dialog(version_installed=self.VERSION, notebook=self._notebook, 1hedfg
67 show_no_update=show_no_update)
69 def _should_check(self): 1ab
70 if self._settings.get(_CHECK_FOR_UPDATES_SETTING, None) is None: 1rhikjdsnolpqm
71 self._settings[_CHECK_FOR_UPDATES_SETTING] = True 1kj
72 return True 1kj
73 return (self._settings[_CHECK_FOR_UPDATES_SETTING] and 1rhidsnolpqm
74 time.time() - self._settings.get(_LAST_UPDATE_CHECK_SETTING, 0) > self.SECONDS_IN_WEEK)
76 def _is_new_version_available(self): 1ab
77 self._settings[_LAST_UPDATE_CHECK_SETTING] = time.time() 1hikjedfnoglpqm
78 try: 1hikjedfnoglpqm
79 self._get_rf_pypi_data() 1hikjedfnoglpqm
80 self._newest_version = self._get_newest_version() 1hikjedfnoglpqm
81 self._download_url = self._get_download_url() 1ikjedfnoglpqm
82 except Exception as e: 1hi
83 print(e) 1hi
84 # There are many possible errors:
85 # - Timeout
86 # - Corrupted data
87 # - Server fault message
88 # - Unexpected change in dataformat
89 return False 1hi
90 return cmp_versions(self.VERSION, self._newest_version) == -1 1kjedfnoglpqm
92 def _get_newest_version(self): 1ab
93 return self.pyver
95 def _get_download_url(self): 1ab
96 return self.pyurl
98 def _get_rf_pypi_data(self): 1ab
99 resp = requests.get("https://pypi.org/simple/robotframework-ride/", 1hi
100 headers={"Accept": "application/vnd.pypi.simple.v1+json"})
101 pydata = resp.json() 1hi
102 self.pyver = pydata['versions'][-1] 1hi
103 self.pyurl = pydata['files'][-1]['url'] 1hi
106def upgrade_from_dev_dialog(version_installed, notebook, show_no_update=False): 1ab
107 dev_version = urllib2.urlopen('https://raw.githubusercontent.com/robotframework/' 1hedfg
108 'RIDE/develop/src/robotide/version.py', timeout=1).read().decode('utf-8')
109 matches = re.findall(r"VERSION\s*=\s*'([\w.]*)'", dev_version) 1hedfg
110 version_latest = matches[0] if matches else None 1hedfg
111 if cmp_versions(version_installed, version_latest) == -1: 1hedfg
112 # Here is the Menu Help->Upgrade insertion part, try to highlight menu # wx.CANCEL_DEFAULT
113 command = sys.executable + " -m pip install -U https://github.com/robotframework/RIDE/archive/develop.zip" 1edfg
114 _add_content_to_clipboard(command) 1edfg
115 if not _askyesno(_("Upgrade?"), f"{SPC}{_('New development version is available.')}{SPC}\n{SPC}" 115 ↛ 122line 115 didn't jump to line 122 because the condition on line 115 was always true1edfg
116 f"{_('You may install version %s with:') % version_latest}\n"
117 f"{SPC}{command}{SPC}\n\n{SPC}{_('Click OK to Upgrade now!')}\n{SPC}"
118 f"{_('After upgrade you will see another dialog informing to close this RIDE instance.')}"
119 f"{SPC}\n", wx.GetActiveWindow(), no_default=True):
120 return False 1edfg
121 else:
122 do_upgrade(command, notebook)
123 return True
124 else:
125 if show_no_update: 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true1h
126 _askyesno(_("No Upgrade Available"), f"{SPC}{_('You have the latest version of RIDE.')}{SPC}"
127 f"\n\n{SPC}{_(' Have a nice day :)')}\n",
128 wx.GetActiveWindow())
129 return False 1h
132def _askyesno(title, message, frame=None, no_default=False): 1ab
133 if frame is None: 133 ↛ 137line 133 didn't jump to line 137 because the condition on line 133 was always true1edfg
134 _ = wx.GetApp() or wx.App() 1edfg
135 parent = wx.Frame(None, size=(0, 0)) 1edfg
136 else:
137 parent = wx.Frame(frame, size=(0, 0))
138 parent.CenterOnScreen() 1edfg
139 dlg = MessageDialog(parent, message, title, ttl=8, no_default=no_default) 1edfg
140 dlg.Fit() 1edfg
141 result = dlg.ShowModal() in [wx.ID_YES, wx.ID_OK] 1edfg
142 # print("DEBUG: updatenotifier _askyesno Result %s" % result)
143 if dlg: 143 ↛ 146line 143 didn't jump to line 146 because the condition on line 143 was always true1edfg
144 dlg.Destroy() 1edfg
145 # parent.Destroy()
146 return result 1edfg
149def _add_content_to_clipboard(content): 1ab
150 wx.TheClipboard.Open() 1edfgc
151 wx.TheClipboard.SetData(wx.TextDataObject(content)) 1edfgc
152 wx.TheClipboard.Close() 1edfgc
155@dataclass 1ab
156class RunnerCommand: 1ab
157 def __init__(self, name, command, documentation): 1ab
158 self.name = name
159 self.command = command
160 self.documentation = documentation
163def do_upgrade(command, notebook): 1ab
164 _add_content_to_clipboard(command)
165 # print("DEBUG: Here will be the installation step.") # DEBUG 'pip list'
166 from ..run import ui
167 config = RunnerCommand('Upgrade RIDE', command, 'Uses pip to upgrade RIDE.')
168 PUBLISHER.subscribe(start_upgraded, RideRunnerStopped)
169 result = ui.Runner(config, notebook).run()
170 time.sleep(10)
171 if result == -1:
172 _askyesno(_("Failed to Upgrade"), f"{SPC}{_('An error occurred when installing new version')}",
173 wx.GetActiveWindow())
174 return False
177def start_upgraded(message): 1ab
178 __ = message
179 import zipfile
180 import requests
182 def download_url(url, save_path, chunk_size=128):
183 r = requests.get(url, stream=True)
184 with open(save_path, 'wb') as fd:
185 for chunk in r.iter_content(chunk_size=chunk_size):
186 fd.write(chunk)
188 backup_configobj = tempfile.NamedTemporaryFile(delete=False)
189 config_obj_dir = path.join(path.dirname(__file__), '../preferences')
190 # print(f"DEBUG: updatenotifier, Starting do_upgrade {config_obj_dir=} zip is {backup_configobj.name=}")
191 download_url('https://robotframework.transformidea.com/RIDE/packages/configobj.zip', backup_configobj.name)
192 with zipfile.ZipFile(backup_configobj, 'r') as zzip:
193 zzip.extractall(config_obj_dir)
194 # print(f"DEBUG: updatenotifier, {config_obj_dir=} extracted {backup_configobj.name}")
195 try:
196 os.remove(backup_configobj.name)
197 except PermissionError:
198 pass
199 command = sys.executable + " -m robotide.__init__ --noupdatecheck"
200 wx.CallLater(1000, subprocess.Popen, command.split(' '), start_new_session=True)
201 p = psutil.Process()
202 result = _askyesno(_("Completed Upgrade"), f"\n{SPC}{_('You should close this RIDE (Process ID = ')}{p.pid}){SPC}"
203 f"\n{SPC}{_('Do you want to CLOSE RIDE now?')}\n{SPC}",
204 wx.GetActiveWindow())
205 PUBLISHER.unsubscribe(start_upgraded, RideRunnerStopped)
206 if result:
207 wx.CallAfter(wx.App.Get().GetTopWindow().Close)
208 # wx.CallAfter(p.terminate)
211class LocalHtmlWindow(HtmlWindow): 1ab
212 def __init__(self, parent, size=(600, 400)): 1ab
213 HtmlWindow.__init__(self, parent, size) 1c
214 if "gtk2" in wx.PlatformInfo or "gtk3" in wx.PlatformInfo: 214 ↛ 215line 214 didn't jump to line 215 because the condition on line 214 was never true1c
215 self.SetStandardFonts()
217 def OnLinkClicked(self, link): # Overrides wx method 1ab
218 wx.LaunchDefaultBrowser(link.GetHref())
221class UpdateDialog(RIDEDialog): 1ab
223 def __init__(self, uversion, url, settings, notebook, modal=True): 1ab
224 self._settings = settings 1c
225 self._notebook = notebook 1c
226 self._command = sys.executable + f" -m pip install -U robotframework-ride=={uversion}" 1c
227 _add_content_to_clipboard(self._command) 1c
228 RIDEDialog.__init__(self, title=_("Update available"), size=(600, 400), 1c
229 style=wx.DEFAULT_FRAME_STYLE | wx.FRAME_FLOAT_ON_PARENT)
230 # set Left to Right direction (while we don't have localization)
231 self.SetLayoutDirection(wx.Layout_LeftToRight) 1c
232 self.SetBackgroundColour(Colour(self.color_background)) 1c
233 self.SetForegroundColour(Colour(self.color_foreground)) 1c
234 sizer = wx.BoxSizer(orient=wx.VERTICAL) 1c
235 hwin = LocalHtmlWindow(self, size=(600, 200)) 1c
236 hwin.set_content(f"{SPC}{_('New version ')}{uversion}{_(' available from ')}<a href=\"{url}\">{url}</a><br/>" 1c
237 f"{SPC}{_('See this version ')}<a href=\"https://github.com/robotframework/RIDE/blob/master/doc"
238 f"/releasenotes/ride-{uversion}.rst\">Release Notes</a><br/><br/>"
239 f"{SPC}{_('You can update with the command:')}<br/><b>{self._command}</b>"
240 f"<br/><br/>{SPC}{_('Or, click <b>Upgrade Now</b>')}.<br/>"
241 f"{SPC}{_('After upgrade you will see another dialog informing to close this RIDE instance.')}</b>"
242 f"<br/><br/>{SPC}{_('See the latest development ')}<a href=\"https://github.com/robotframework/RIDE"
243 f"/blob/master/CHANGELOG.adoc\">CHANGELOG</a>")
244 irep = hwin.GetInternalRepresentation() 1c
245 hwin.SetSize((irep.GetWidth()+25, irep.GetHeight()+20)) 1c
246 sizer.Add(hwin) 1c
247 checkbox = wx.CheckBox(self, -1, label=_("I\'m using another method for RIDE updates\n and " 1c
248 "do not need automatic update checks"))
249 checkbox.Bind(wx.EVT_CHECKBOX, handler=self.on_checkbox_change) 1c
250 sizer.Add(checkbox) 1c
251 hsizer = wx.BoxSizer(orient=wx.HORIZONTAL) 1c
252 button = ButtonWithHandler(self, label=_("remind me later"), mk_handler="remind me later", 1c
253 handler=self.on_remind_me_later)
254 button.SetBackgroundColour(Colour(self.color_secondary_background)) 1c
255 button.SetForegroundColour(Colour(self.color_secondary_foreground)) 1c
256 hsizer.Add(button) 1c
257 hsizer.AddSpacer(50) 1c
258 up_button = ButtonWithHandler(self, label=_("Upgrade Now"), mk_handler="Upgrade Now", 1c
259 handler=self.on_upgrade_now)
260 up_button.SetBackgroundColour(Colour(self.color_secondary_background)) 1c
261 up_button.SetForegroundColour(Colour(self.color_secondary_foreground)) 1c
262 hsizer.Add(up_button) 1c
263 sizer.Add(hsizer) 1c
264 self.SetSizer(sizer) 1c
265 self.CentreOnParent(wx.BOTH) 1c
266 self.Fit() 1c
267 self.SetFocus() 1c
268 if modal: 268 ↛ 269line 268 didn't jump to line 269 because the condition on line 268 was never true1c
269 self.ShowModal()
270 self.Destroy()
271 else:
272 self.Show() 1c
274 def on_remind_me_later(self, event): 1ab
275 __ = event
276 self.Close(True)
278 def on_checkbox_change(self, event): 1ab
279 self._settings[_CHECK_FOR_UPDATES_SETTING] = not event.IsChecked()
280 event.Skip()
282 def on_upgrade_now(self, event): 1ab
283 __ = event
284 _add_content_to_clipboard(self._command)
285 self.Close()
286 do_upgrade(self._command, self._notebook)