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

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__ 

22 

23 

24import time 1ab

25import urllib.request as urllib2 1ab

26from dataclasses import dataclass 1ab

27 

28import psutil 1ab

29import requests 1ab

30import wx 1ab

31from wx import Colour 1ab

32from os import path 1ab

33 

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

39 

40_ = wx.GetTranslation # To keep linter/code analyser happy 1ab

41builtins.__dict__['_'] = wx.GetTranslation 1ab

42 

43_CHECK_FOR_UPDATES_SETTING = "check for updates" 1ab

44_LAST_UPDATE_CHECK_SETTING = "last update check" 1ab

45SPC = " " 1ab

46 

47 

48class UpdateNotifierController(object): 1ab

49 

50 VERSION = version.VERSION 1ab

51 SECONDS_IN_WEEK = 60*60*24*7 1ab

52 

53 def __init__(self, settings, notebook): 1ab

54 self._settings = settings 1arhikjedfsnoglpqm

55 self._notebook = notebook 1arhikjedfsnoglpqm

56 

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) 

68 

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) 

75 

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

91 

92 def _get_newest_version(self): 1ab

93 return self.pyver 

94 

95 def _get_download_url(self): 1ab

96 return self.pyurl 

97 

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

104 

105 

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

130 

131 

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

147 

148 

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

153 

154 

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 

161 

162 

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 

175 

176 

177def start_upgraded(message): 1ab

178 __ = message 

179 import zipfile 

180 import requests 

181 

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) 

187 

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) 

209 

210 

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

216 

217 def OnLinkClicked(self, link): # Overrides wx method 1ab

218 wx.LaunchDefaultBrowser(link.GetHref()) 

219 

220 

221class UpdateDialog(RIDEDialog): 1ab

222 

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

273 

274 def on_remind_me_later(self, event): 1ab

275 __ = event 

276 self.Close(True) 

277 

278 def on_checkbox_change(self, event): 1ab

279 self._settings[_CHECK_FOR_UPDATES_SETTING] = not event.IsChecked() 

280 event.Skip() 

281 

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)