Coverage for src/robotide/pluginapi/plugin.py: 70%

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

15 

16import inspect 1ab

17 

18import wx 1ab

19 

20from .. import utils 1ab

21from ..action.actioninfo import ActionInfo 1ab

22from ..publish import PUBLISHER 1ab

23 

24 

25class Plugin(object): 1ab

26 """Entry point to RIDE plugin API -- all plugins must extend this class. 

27 

28 Plugins can use the helper methods implemented in this class to interact 

29 with the core application. The methods and their arguments are kept stable 

30 across the different RIDE releases to the extent that it is possible. 

31 

32 If the provided methods are not enough, plugins can also interact with the 

33 core directly using properties `tree`, `menubar`, `toolbar`, `notebook` and 

34 `model`. Although these attributes themselves are stable, the functionality 

35 behind them may still change between releases. Users are thus recommended 

36 to propose new helper methods, preferably with patches, for often needed 

37 functionality that is only available through these properties. 

38 

39 :IVariables: 

40 name 

41 Plugin name. Set in `__init__` based on the given name or the class name. 

42 doc 

43 Plugin documentation. Set in `__init__` based on the given doc or 

44 the class docstring. 

45 metadata 

46 Plugin metadata. Set in `__init__` based on the given metadata. 

47 initially_enabled 

48 Specifies should the plugin be enabled when first loaded. 

49 Set in `__init__`. 

50 """ 

51 tree = property(lambda self: self.__frame.tree, doc='Provides access to the suite and resource tree') 1abrCkDEFGHIJlmnoi

52 filemgr = property(lambda self: self.__frame.filemgr, doc='Provides access to the files and folders explorer') 1abi

53 menubar = property(lambda self: self.__frame.GetMenuBar(), doc='Provides access to the application menubar') 1abi

54 toolbar = property(lambda self: self.__frame.GetToolBar(), doc='Provides access to the application toolbar') 1abi

55 statusbar = property(lambda self: self.__frame.FindWindowByName("StatusBar", self.__frame), 1abqi

56 doc='Provides access to the application statusbar') 

57 notebook = property(lambda self: self.__frame.notebook, doc='Provides access to the tabbed notebook') 1abqrklKi

58 model = property(lambda self: self.__app.model, doc='Provides access to the data model') 1abkmnoi

59 frame = property(lambda self: self.__frame, doc='Reference to the RIDE main frame') 1abi

60 datafile = property(lambda self: self.get_selected_datafile(), doc='Currently selected datafile') 1abki

61 global_settings = property(lambda self: self.__app.settings, doc='Settings read from settings.cfg') 1abi

62 

63 def __init__(self, application, name=None, doc=None, metadata=None, default_settings=None, initially_enabled=True): 1ab

64 """Initialize the plugin with the provided data. 

65 

66 The provided information is mainly used by the plugin manager. Simple 

67 plugins are often fine with the defaults. If this method is overridden, 

68 the plugin must call it explicitly:: 

69 

70 from robotide.pluginapi import Plugin 

71 

72 class MyCoolPluginExample(Plugin): 

73 \"\"\"This extra cool docstring is used as the plugin doc.\"\"\" 

74 def __init__(self, application): 

75 Plugin.__init__(self, application, metadata={'version': '0.1'}, 

76 default_settings={'color': 'red', 'x': 42}) 

77 

78 Plugins should not create any user interface elements at this point but 

79 wait until the `enable` method is called. 

80 

81 :Parameters: 

82 application 

83 RIDE application reference. 

84 name 

85 Name of the plugin. If not specified, the name is got from the 

86 plugin class name-dropping possible ``Plugin`` from the end. 

87 doc 

88 Plugin documentation. If not specified, the doc is got from the 

89 plugin class docstring. 

90 metadata 

91 A dictionary of free metadata shown on the plugin manager. Values 

92 containing URLs will be shown as links. 

93 default_settings 

94 A dictionary of settings and their default values. Settings are 

95 automatically stored onto RIDE configuration file, can be 

96 accessed using direct attribute access via `__getattr__`, and new 

97 settings can be saved using `save_setting`. 

98 initially_enabled 

99 Specifies should the plugin be enabled when loaded for the first 

100 time. Users can change the status later from the plugin manager. 

101 """ 

102 self.name = name or utils.name_from_class(self, drop='Plugin') 1ajefcdgh

103 self.doc = self._get_doc(doc) 1ajefcdgh

104 self.metadata = metadata or {} 1ajefcdgh

105 self.initially_enabled = initially_enabled 1ajefcdgh

106 self._save_timer = None 1ajefcdgh

107 self.__app = application 1ajefcdgh

108 self.__frame = application.frame 1ajefcdgh

109 self.__namespace = application.namespace 1ajefcdgh

110 self.__settings = application.settings['Plugins'].add_section(self.name) 1ajefcdgh

111 self.__settings.set_defaults(default_settings) 1ajefcdgh

112 self.__actions = [] 1ajefcdgh

113 

114 def _get_doc(self, given_doc): 1ab

115 if given_doc: 115 ↛ 116line 115 didn't jump to line 116 because the condition on line 115 was never true1ajefcdgh

116 return given_doc 

117 if self.__doc__ == Plugin.__doc__: 1ajefcdgh

118 return '' 1efcdgh

119 return inspect.getdoc(self) or '' 1aj

120 

121 def __getattr__(self, name): 1ab

122 """Provides convenient attribute access to saved settings. 

123 

124 For example, setting ``color`` can be accessed directly like 

125 ``self.color``. 

126 """ 

127 if name in ('_Plugin__settings', '_parent'): 1aqrkmnoiefcdgh

128 return 1ai

129 if '__settings' not in name and self.__settings.has_setting(name): 1aqrkmnoiefcdgh

130 return self.__settings[name] 1aecdgh

131 raise AttributeError("No attribute or settings with name '%s' found" % name) 1aqrkmnof

132 

133 def save_setting(self, name, value, override=True, delay=0): 1ab

134 """Saves the specified setting into the RIDE configuration file. 

135 

136 ``override`` controls whether a possibly already existing value is 

137 overridden or not. Saved settings can be accessed using direct attribute 

138 access via `__getattr__`. 

139 ``delay`` is number defining how many seconds is waited before setting 

140 is saved. This can be used to prevent saving the value while user is 

141 typing it. 

142 """ 

143 self.__settings.set(name, value, autosave=delay == 0, override=override) 1acd

144 self._delay_saving(delay) 1acd

145 

146 def _delay_saving(self, delay): 1ab

147 if not delay: 147 ↛ 149line 147 didn't jump to line 149 because the condition on line 147 was always true1acd

148 return 1acd

149 delay = delay * 1000 

150 if not self._save_timer: 

151 self._save_timer = wx.CallLater(delay, self._save_setting_after_delay) 

152 else: 

153 self._save_timer.Restart(delay) 

154 

155 def _save_setting_after_delay(self): 1ab

156 self.__settings.save() 

157 self._save_timer = None 

158 

159 def enable(self): 1ab

160 """This method is called by RIDE when the plugin is enabled. 

161 

162 Possible integration to UI should be done in this method and removed 

163 when the `disable` method is called. 

164 """ 

165 pass 1p

166 

167 def disable(self): 1ab

168 """Called by RIDE when the plugin is disabled. 

169 

170 Undo whatever was done in the `enable` method. 

171 """ 

172 pass 

173 

174 def config_panel(self, parent): 1ab

175 """Called by RIDE to get the plugin configuration panel. 

176 

177 The panel returned will be integrated into the plugin manager UI, and 

178 can be used e.g. to display configurable settings. 

179 

180 By default, there is no configuration panel. 

181 """ 

182 _ = parent 

183 return None 

184 

185 def register_action(self, action_info): 1ab

186 """Registers a menu entry and optionally a shortcut and a toolbar icon. 

187 

188 ``action_info`` is an instance of `ActionInfo` class containing needed 

189 information to create menu entry, keyboard shortcut and/or toolbar 

190 button for the action. 

191 

192 All registered actions can be un-registered using the 

193 `unregister_actions` method. 

194 

195 If register action is used in menu event handler, and it modifies the 

196 menu that triggered the event, it is safest to wrap register action 

197 call inside wx.CallAfter function. 

198 

199 Returns created `Action` object. 

200 """ 

201 action = self.__frame.actions.register_action(action_info) 1apl

202 self.__actions.append(action) 1apl

203 return action 1apl

204 

205 def register_shortcut(self, shortcut, callback): 1ab

206 action_info = ActionInfo(None, None, action=callback, shortcut=shortcut) 1al

207 action = self.__frame.actions.register_shortcut(action_info) 1al

208 self.__actions.append(action) 1al

209 return action 1al

210 

211 def register_actions(self, action_infos): 1ab

212 """Registers multiple menu entries and shortcuts/icons. 

213 

214 ``action_infos`` is a list of same `ActionInfo` objects that 

215 `register_action` method accepts. 

216 

217 Returns list of created `Action` objects. 

218 """ 

219 return [self.register_action(info) for info in action_infos] 1al

220 

221 def register_search_action(self, description, handler, icon, default=False): 1ab

222 self.__frame.toolbar.register_search_handler(description, handler, icon, default=default) 

223 

224 def unregister_actions(self): 1ab

225 """Unregisters all actions registered by this plugin.""" 

226 for action in self.__actions: 1ap

227 action.unregister() 

228 self.__actions = [] 1ap

229 

230 def add_tab(self, tab, title, allow_closing=True): 1ab

231 """Adds the ``tab`` with the ``title`` to the tabbed notebook and shows it. 

232 

233 The ``tab`` can be any wxPython container. ``allow_closing`` defines 

234 can users close the tab while the plugin is enabled. 

235 """ 

236 self.notebook.add_tab(tab, title, allow_closing) 

237 

238 def show_tab(self, tab): 1ab

239 """Makes the ``tab`` added using `add_tab` visible.""" 

240 self.notebook.show_tab(tab) 

241 

242 def delete_tab(self, tab): 1ab

243 """Deletes the ``tab`` added using `add_tab`.""" 

244 self.notebook.delete_tab(tab) 

245 

246 def allow_tab_closing(self, tab): 1ab

247 """Allows closing a tab that has been created using allow_closing=False.""" 

248 self.notebook.allow_closing(tab) 

249 

250 def disallow_tab_closing(self, tab): 1ab

251 """Disallows closing a tab by user""" 

252 self.notebook.disallow_closing(tab) 

253 

254 def tab_is_visible(self, tab): 1ab

255 """Returns is the ``tab`` added using `add_tab` visible or not.""" 

256 return self.notebook.tab_is_visible(tab) 

257 

258 def new_suite_can_be_opened(self): 1ab

259 """Checks are there modified files and asks user what to do if there are. 

260 

261 Returns False if there were modified files and user canceled the dialog, 

262 otherwise returns True. 

263 """ 

264 return self.__app.ok_to_open_new() 

265 

266 def open_suite(self, path): 1ab

267 """Opens a test suite specified by the ``path``. 

268 

269 No suite is opened if parsing the suite fails. 

270 """ 

271 self.__frame.open_suite(path) 

272 

273 def get_selected_datafile(self): 1ab

274 """Returns the data file that is currently selected in the tree. 

275 

276 If a test case or a keyword is selected, the data file containing the 

277 selected item is returned. 

278 

279 :rtype: 

280 `InitFile`, `TestCaseFile` or `ResourceFile` 

281 """ 

282 if not self.tree: 282 ↛ 283line 282 didn't jump to line 283 because the condition on line 282 was never true1aki

283 return 

284 return self.tree.get_selected_datafile() 1ak

285 

286 def save_selected_datafile(self): 1ab

287 """Saves the data file that is currently selected in the tree. 

288 

289 If a test case or a keyword is selected, the data file containing the 

290 selected item is saved. 

291 """ 

292 self.__frame.save(self.tree.get_selected_datafile_controller()) 

293 

294 def is_unsaved_changes(self): 1ab

295 """Returns True if there is any unsaved changes, otherwise False""" 

296 return self.__frame.has_unsaved_changes() 

297 

298 def save_all_unsaved_changes(self): 1ab

299 """Saves all the data files that are modified.""" 

300 self.__frame.save_all() 

301 

302 def get_selected_item(self): 1ab

303 """Returns the item that is currently selected in the tree. 

304 

305 The item can be a test suite, a resource file, a test case or a keyword. 

306 

307 :rtype: 

308 `InitFile`, `TestCaseFile`, `ResourceFile`, `TestCase` or `UserKeyword` 

309 """ 

310 if not self.tree: 310 ↛ 311line 310 didn't jump to line 311 because the condition on line 310 was never true1akmno

311 return 

312 if hasattr(self, 'model'): 1akmno

313 return self.tree.get_selected_item() or self.model 1ak

314 else: 

315 return self.tree.get_selected_item() 1mno

316 

317 def content_assist_values(self, value=''): 1ab

318 """Returns content assist values for currently selected item.""" 

319 return self.__namespace.get_suggestions_for(self.get_selected_item(), value) 1km

320 

321 def get_user_keyword(self, name): 1ab

322 """Returns user keyword instance whose name is ``name`` or None.""" 

323 keyword_info = self.__namespace.find_user_keyword(self.datafile, name) 

324 return keyword_info.item if keyword_info else None 

325 

326 def select_user_keyword_node(self, uk): 1ab

327 """Selects node containing the given ``uk`` in the tree.""" 

328 if not self.tree: 

329 return 

330 self.tree.select_user_keyword_node(uk) 

331 

332 def get_keyword(self, name): 1ab

333 """Returns the keyword object with the given name or None""" 

334 return self.__namespace.find_keyword(self.datafile, name) 

335 

336 def get_keyword_details(self, name): 1ab

337 """Returns details (documentation, source) of keyword with name ``name``. 

338 

339 Returns None if no matching keyword is found. 

340 """ 

341 return self.__namespace.keyword_details(self.datafile, name) 

342 

343 def is_user_keyword(self, name): 1ab

344 """Returns whether ``name`` is a user keyword of current datafile. 

345 

346 Checks both the datafile's own and imported user keywords for match. 

347 """ 

348 return self.__namespace.is_user_keyword(self.datafile, name) 

349 

350 def is_library_keyword(self, name): 1ab

351 """Returns whether ``name`` is a keyword imported by current datafile.""" 

352 return self.__namespace.is_library_keyword(self.datafile, name) 

353 

354 def all_testcases(self): 1ab

355 """Returns all test cases from all suites in one, unsorted list""" 

356 return self.model.all_testcases() 

357 

358 def register_content_assist_hook(self, hook): 1ab

359 """Allows plugin to insert values in content assist dialog. 

360 

361 ``hook`` must be a callable, which should take two arguments and 

362 return a list of instances of `ItemInfo` class. When content 

363 assist is requested by user, ``hook`` will be called with the current 

364 dataitem and current value of cell as parameters. 

365 """ 

366 self.__namespace.register_content_assist_hook(hook) 1j

367 

368 def get_plugins(self): 1ab

369 """Returns list containing plugin wrapper for every loaded plugin. 

370 

371 Wrapper is an instance of `PluginConnector` if the plugin has loaded 

372 successfully, otherwise it's an instance of `BrokenPlugin`.""" 

373 return self.__app.get_plugins() 1L

374 

375 def publish(self, topic, data): 1ab

376 """Publishes a message with given topic and client data. 

377 

378 Purpose of this method is to support inter-plugin communication which 

379 is not possible to achieve using custom message classes. 

380 

381 `data` will be passed as an argument to registered listener methods. 

382 """ 

383 PUBLISHER.publish(topic, data) 

384 

385 def subscribe(self, listener, *topics): 1ab

386 """Start to listen to messages with the given ``topics``. 

387 

388 See the documentation of the `robotide.publish` module for more 

389 information about subscribing to messages and the messaging system 

390 

391 `unsubscribe` and `unsubscribe_all` can be used to stop listening to 

392 certain or all messages. 

393 """ 

394 for topic in topics: 1aplstu

395 PUBLISHER.subscribe(listener, topic) 1aplstu

396 

397 def unsubscribe(self, listener, *topics): 1ab

398 """Stops listening to messages with the given ``topics``. 

399 

400 ``listener`` and ``topics`` have the same meaning as in `subscribe` 

401 and a listener/topic combination is unsubscribed only when both of them 

402 match. 

403 """ 

404 for topic in topics: 1vwxyzAB

405 PUBLISHER.unsubscribe(listener, topic) 1vwxyzAB

406 

407 def unsubscribe_all(self): 1ab

408 """Stops to listen to all messages this plugin has subscribed to.""" 

409 PUBLISHER.unsubscribe_all(self) 1ai

410 

411 def register_editor(self, item_class, editor_class, activate=True): 1ab

412 """Register ``editor_class`` as an editor class for model items of type ``item_class`` 

413 

414 If ``activate`` is True, the given editor is automatically activated 

415 using `set_active_editor`. 

416 """ 

417 self.__app.register_editor(item_class, editor_class, activate) 

418 

419 def unregister_editor(self, item_class, editor_class): 1ab

420 """Unregisters ``editor_class`` as an editor class for model items of type ``item_class``""" 

421 self.__app.unregister_editor(item_class, editor_class) 

422 

423 def set_active_editor(self, item_class, editor_class): 1ab

424 """Activates the specified editor to be used with the specified model item. 

425 

426 The editor must have been registered first by using `register_editor`. 

427 """ 

428 self.__app.activate_editor(item_class, editor_class) 

429 

430 def get_editors(self, item_class): 1ab

431 """Return all registered editors for the given model item class. 

432 

433 The last editor in the list is the currently active editor. 

434 """ 

435 return self.__app.get_editors(item_class) 

436 

437 def get_editor(self, item_class): 1ab

438 """Return the current editor class for the given model item class""" 

439 return self.__app.get_editor(item_class) 

440 

441 def highlight_cell(self, tcuk, obj=None, row=-1, column=-1): 1ab

442 """Highlight a specific row/column of a test case or user keyword""" 

443 if not self.tree: 

444 return 

445 self.tree.select_node_by_data(tcuk) 

446 self.__app.editor.highlight_cell(obj, row, column) 

447 

448 def highlight(self, data, text): 1ab

449 """Highlight a specific text of a given data's editor""" 

450 if not self.tree: 

451 return 

452 self.tree.highlight(data, text) 

453 

454 def statusbar_message(self, text, ttl=0): 1ab

455 """Set a text message at Plugin area of StatusBar 

456 :Parameters: 

457 text 

458 Message to show in StatusBar 

459 ttl 

460 Time to live in milliseconds, default = 0, means does not expire 

461 """ 

462 if text is None: 462 ↛ 463line 462 didn't jump to line 463 because the condition on line 462 was never true1q

463 text = '' 

464 self.statusbar.SetStatusText(text, 1) 1q

465 wx.CallAfter(self._delayed_clear, ttl) 

466 

467 def _delayed_clear(self, ttl): 1ab

468 if ttl >= 100: 

469 wx.CallLater(ttl, self.statusbar_clear) 

470 

471 def statusbar_clear(self): 1ab

472 """Clears the message at Plugin area of StatusBar""" 

473 self.statusbar.SetStatusText('', 1)