|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the Plugin Manager. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import sys |
|
12 import zipfile |
|
13 import types |
|
14 import importlib |
|
15 import contextlib |
|
16 |
|
17 from PyQt5.QtCore import ( |
|
18 pyqtSignal, QObject, QDate, QFile, QFileInfo, QUrl, QIODevice |
|
19 ) |
|
20 from PyQt5.QtGui import QPixmap |
|
21 from PyQt5.QtNetwork import ( |
|
22 QNetworkAccessManager, QNetworkRequest, QNetworkReply |
|
23 ) |
|
24 |
|
25 from E5Gui import E5MessageBox |
|
26 from E5Gui.E5Application import e5App |
|
27 |
|
28 from E5Network.E5NetworkProxyFactory import proxyAuthenticationRequired |
|
29 try: |
|
30 from E5Network.E5SslErrorHandler import E5SslErrorHandler, E5SslErrorState |
|
31 SSL_AVAILABLE = True |
|
32 except ImportError: |
|
33 SSL_AVAILABLE = False |
|
34 |
|
35 from .PluginExceptions import ( |
|
36 PluginPathError, PluginModulesError, PluginLoadError, |
|
37 PluginActivationError, PluginModuleFormatError, PluginClassFormatError |
|
38 ) |
|
39 |
|
40 import UI.PixmapCache |
|
41 |
|
42 import Globals |
|
43 import Utilities |
|
44 import Preferences |
|
45 |
|
46 from eric6config import getConfig |
|
47 |
|
48 |
|
49 class PluginManager(QObject): |
|
50 """ |
|
51 Class implementing the Plugin Manager. |
|
52 |
|
53 @signal shutdown() emitted at shutdown of the IDE |
|
54 @signal pluginAboutToBeActivated(modulName, pluginObject) emitted just |
|
55 before a plugin is activated |
|
56 @signal pluginActivated(moduleName, pluginObject) emitted just after |
|
57 a plugin was activated |
|
58 @signal allPlugginsActivated() emitted at startup after all plugins have |
|
59 been activated |
|
60 @signal pluginAboutToBeDeactivated(moduleName, pluginObject) emitted just |
|
61 before a plugin is deactivated |
|
62 @signal pluginDeactivated(moduleName, pluginObject) emitted just after |
|
63 a plugin was deactivated |
|
64 """ |
|
65 shutdown = pyqtSignal() |
|
66 pluginAboutToBeActivated = pyqtSignal(str, object) |
|
67 pluginActivated = pyqtSignal(str, object) |
|
68 allPlugginsActivated = pyqtSignal() |
|
69 pluginAboutToBeDeactivated = pyqtSignal(str, object) |
|
70 pluginDeactivated = pyqtSignal(str, object) |
|
71 |
|
72 def __init__(self, parent=None, disabledPlugins=None, doLoadPlugins=True, |
|
73 develPlugin=None): |
|
74 """ |
|
75 Constructor |
|
76 |
|
77 The Plugin Manager deals with three different plugin directories. |
|
78 The first is the one, that is part of eric6 (eric6/Plugins). The |
|
79 second one is the global plugin directory called 'eric6plugins', |
|
80 which is located inside the site-packages directory. The last one |
|
81 is the user plugin directory located inside the .eric6 directory |
|
82 of the users home directory. |
|
83 |
|
84 @param parent reference to the parent object |
|
85 @type QObject |
|
86 @param disabledPlugins list of plug-ins that have been disabled via |
|
87 the command line parameters '--disable-plugin=' |
|
88 @type list of str |
|
89 @param doLoadPlugins flag indicating, that plug-ins should |
|
90 be loaded |
|
91 @type bool |
|
92 @param develPlugin filename of a plug-in to be loaded for |
|
93 development |
|
94 @type str |
|
95 @exception PluginPathError raised to indicate an invalid plug-in path |
|
96 @exception PluginModulesError raised to indicate the absence of |
|
97 plug-in modules |
|
98 """ |
|
99 super().__init__(parent) |
|
100 |
|
101 self.__ui = parent |
|
102 self.__develPluginFile = develPlugin |
|
103 self.__develPluginName = None |
|
104 if disabledPlugins is not None: |
|
105 self.__disabledPlugins = disabledPlugins[:] |
|
106 else: |
|
107 self.__disabledPlugins = [] |
|
108 |
|
109 self.__inactivePluginsKey = "PluginManager/InactivePlugins" |
|
110 |
|
111 self.pluginDirs = { |
|
112 "eric6": os.path.join(getConfig('ericDir'), "Plugins"), |
|
113 "global": os.path.join(Utilities.getPythonLibraryDirectory(), |
|
114 "eric6plugins"), |
|
115 "user": os.path.join(Utilities.getConfigDir(), "eric6plugins"), |
|
116 } |
|
117 self.__priorityOrder = ["eric6", "global", "user"] |
|
118 |
|
119 self.__defaultDownloadDir = os.path.join( |
|
120 Utilities.getConfigDir(), "Downloads") |
|
121 |
|
122 self.__activePlugins = {} |
|
123 self.__inactivePlugins = {} |
|
124 self.__onDemandActivePlugins = {} |
|
125 self.__onDemandInactivePlugins = {} |
|
126 self.__activeModules = {} |
|
127 self.__inactiveModules = {} |
|
128 self.__onDemandActiveModules = {} |
|
129 self.__onDemandInactiveModules = {} |
|
130 self.__failedModules = {} |
|
131 |
|
132 self.__foundCoreModules = [] |
|
133 self.__foundGlobalModules = [] |
|
134 self.__foundUserModules = [] |
|
135 |
|
136 self.__modulesCount = 0 |
|
137 |
|
138 pdirsExist, msg = self.__pluginDirectoriesExist() |
|
139 if not pdirsExist: |
|
140 raise PluginPathError(msg) |
|
141 |
|
142 if doLoadPlugins: |
|
143 if not self.__pluginModulesExist(): |
|
144 raise PluginModulesError |
|
145 |
|
146 self.__insertPluginsPaths() |
|
147 |
|
148 self.__loadPlugins() |
|
149 |
|
150 self.__checkPluginsDownloadDirectory() |
|
151 |
|
152 self.pluginRepositoryFile = os.path.join(Utilities.getConfigDir(), |
|
153 "PluginRepository") |
|
154 |
|
155 # attributes for the network objects |
|
156 self.__networkManager = QNetworkAccessManager(self) |
|
157 self.__networkManager.proxyAuthenticationRequired.connect( |
|
158 proxyAuthenticationRequired) |
|
159 if SSL_AVAILABLE: |
|
160 self.__sslErrorHandler = E5SslErrorHandler(self) |
|
161 self.__networkManager.sslErrors.connect(self.__sslErrors) |
|
162 self.__replies = [] |
|
163 |
|
164 with contextlib.suppress(AttributeError): |
|
165 self.__ui.onlineStateChanged.connect(self.__onlineStateChanged) |
|
166 |
|
167 def finalizeSetup(self): |
|
168 """ |
|
169 Public method to finalize the setup of the plugin manager. |
|
170 """ |
|
171 for module in ( |
|
172 list(self.__onDemandInactiveModules.values()) + |
|
173 list(self.__onDemandActiveModules.values()) |
|
174 ): |
|
175 if hasattr(module, "moduleSetup"): |
|
176 module.moduleSetup() |
|
177 |
|
178 def getPluginDir(self, key): |
|
179 """ |
|
180 Public method to get the path of a plugin directory. |
|
181 |
|
182 @param key key of the plug-in directory (string) |
|
183 @return path of the requested plugin directory (string) |
|
184 """ |
|
185 if key not in ["global", "user"]: |
|
186 return None |
|
187 else: |
|
188 try: |
|
189 return self.pluginDirs[key] |
|
190 except KeyError: |
|
191 return None |
|
192 |
|
193 def __pluginDirectoriesExist(self): |
|
194 """ |
|
195 Private method to check, if the plugin folders exist. |
|
196 |
|
197 If the plugin folders don't exist, they are created (if possible). |
|
198 |
|
199 @return tuple of a flag indicating existence of any of the plugin |
|
200 directories (boolean) and a message (string) |
|
201 """ |
|
202 if self.__develPluginFile: |
|
203 path = Utilities.splitPath(self.__develPluginFile)[0] |
|
204 fname = os.path.join(path, "__init__.py") |
|
205 if not os.path.exists(fname): |
|
206 try: |
|
207 with open(fname, "w"): |
|
208 pass |
|
209 except OSError: |
|
210 return ( |
|
211 False, |
|
212 self.tr("Could not create a package for {0}.") |
|
213 .format(self.__develPluginFile)) |
|
214 |
|
215 fname = os.path.join(self.pluginDirs["user"], "__init__.py") |
|
216 if not os.path.exists(fname): |
|
217 if not os.path.exists(self.pluginDirs["user"]): |
|
218 os.mkdir(self.pluginDirs["user"], 0o755) |
|
219 try: |
|
220 with open(fname, "w"): |
|
221 pass |
|
222 except OSError: |
|
223 del self.pluginDirs["user"] |
|
224 |
|
225 if not os.path.exists(self.pluginDirs["global"]): |
|
226 try: |
|
227 # create the global plugins directory |
|
228 os.mkdir(self.pluginDirs["global"], 0o755) |
|
229 fname = os.path.join(self.pluginDirs["global"], "__init__.py") |
|
230 with open(fname, "w", encoding="utf-8") as f: |
|
231 f.write('# -*- coding: utf-8 -*-' + "\n") |
|
232 f.write("\n") |
|
233 f.write('"""' + "\n") |
|
234 f.write('Package containing the global plugins.' + "\n") |
|
235 f.write('"""' + "\n") |
|
236 except OSError: |
|
237 del self.pluginDirs["global"] |
|
238 |
|
239 if not os.path.exists(self.pluginDirs["eric6"]): |
|
240 return ( |
|
241 False, |
|
242 self.tr( |
|
243 "The internal plugin directory <b>{0}</b>" |
|
244 " does not exits.").format(self.pluginDirs["eric6"])) |
|
245 |
|
246 return (True, "") |
|
247 |
|
248 def __pluginModulesExist(self): |
|
249 """ |
|
250 Private method to check, if there are plugins available. |
|
251 |
|
252 @return flag indicating the availability of plugins (boolean) |
|
253 """ |
|
254 if ( |
|
255 self.__develPluginFile and |
|
256 not os.path.exists(self.__develPluginFile) |
|
257 ): |
|
258 return False |
|
259 |
|
260 self.__foundCoreModules = self.getPluginModules( |
|
261 self.pluginDirs["eric6"]) |
|
262 if Preferences.getPluginManager("ActivateExternal"): |
|
263 if "global" in self.pluginDirs: |
|
264 self.__foundGlobalModules = self.getPluginModules( |
|
265 self.pluginDirs["global"]) |
|
266 if "user" in self.pluginDirs: |
|
267 self.__foundUserModules = self.getPluginModules( |
|
268 self.pluginDirs["user"]) |
|
269 |
|
270 return len(self.__foundCoreModules + self.__foundGlobalModules + |
|
271 self.__foundUserModules) > 0 |
|
272 |
|
273 def getPluginModules(self, pluginPath): |
|
274 """ |
|
275 Public method to get a list of plugin modules. |
|
276 |
|
277 @param pluginPath name of the path to search (string) |
|
278 @return list of plugin module names (list of string) |
|
279 """ |
|
280 pluginFiles = [f[:-3] for f in os.listdir(pluginPath) |
|
281 if self.isValidPluginName(f)] |
|
282 return pluginFiles[:] |
|
283 |
|
284 def isValidPluginName(self, pluginName): |
|
285 """ |
|
286 Public methode to check, if a file name is a valid plugin name. |
|
287 |
|
288 Plugin modules must start with "Plugin" and have the extension ".py". |
|
289 |
|
290 @param pluginName name of the file to be checked (string) |
|
291 @return flag indicating a valid plugin name (boolean) |
|
292 """ |
|
293 return pluginName.startswith("Plugin") and pluginName.endswith(".py") |
|
294 |
|
295 def __insertPluginsPaths(self): |
|
296 """ |
|
297 Private method to insert the valid plugin paths intos the search path. |
|
298 """ |
|
299 for key in self.__priorityOrder: |
|
300 if key in self.pluginDirs: |
|
301 if self.pluginDirs[key] not in sys.path: |
|
302 sys.path.insert(2, self.pluginDirs[key]) |
|
303 UI.PixmapCache.addSearchPath(self.pluginDirs[key]) |
|
304 |
|
305 if self.__develPluginFile: |
|
306 path = Utilities.splitPath(self.__develPluginFile)[0] |
|
307 if path not in sys.path: |
|
308 sys.path.insert(2, path) |
|
309 UI.PixmapCache.addSearchPath(path) |
|
310 |
|
311 def __loadPlugins(self): |
|
312 """ |
|
313 Private method to load the plugins found. |
|
314 """ |
|
315 develPluginName = "" |
|
316 if self.__develPluginFile: |
|
317 develPluginPath, develPluginName = Utilities.splitPath( |
|
318 self.__develPluginFile) |
|
319 if self.isValidPluginName(develPluginName): |
|
320 develPluginName = develPluginName[:-3] |
|
321 |
|
322 for pluginName in self.__foundGlobalModules: |
|
323 # user and core plug-ins have priority |
|
324 if ( |
|
325 pluginName not in self.__foundUserModules and |
|
326 pluginName not in self.__foundCoreModules and |
|
327 pluginName != develPluginName |
|
328 ): |
|
329 self.loadPlugin(pluginName, self.pluginDirs["global"]) |
|
330 |
|
331 for pluginName in self.__foundUserModules: |
|
332 # core plug-ins have priority |
|
333 if ( |
|
334 pluginName not in self.__foundCoreModules and |
|
335 pluginName != develPluginName |
|
336 ): |
|
337 self.loadPlugin(pluginName, self.pluginDirs["user"]) |
|
338 |
|
339 for pluginName in self.__foundCoreModules: |
|
340 # plug-in under development has priority |
|
341 if pluginName != develPluginName: |
|
342 self.loadPlugin(pluginName, self.pluginDirs["eric6"]) |
|
343 |
|
344 if develPluginName: |
|
345 self.loadPlugin(develPluginName, develPluginPath) |
|
346 self.__develPluginName = develPluginName |
|
347 |
|
348 def loadDocumentationSetPlugins(self): |
|
349 """ |
|
350 Public method to load just the documentation sets plugins. |
|
351 |
|
352 @exception PluginModulesError raised to indicate the absence of |
|
353 plug-in modules |
|
354 """ |
|
355 if not self.__pluginModulesExist(): |
|
356 raise PluginModulesError |
|
357 |
|
358 self.__insertPluginsPaths() |
|
359 |
|
360 for pluginName in self.__foundGlobalModules: |
|
361 # user and core plug-ins have priority |
|
362 if ( |
|
363 pluginName not in self.__foundUserModules and |
|
364 pluginName not in self.__foundCoreModules and |
|
365 pluginName.startswith("PluginDocumentationSets") |
|
366 ): |
|
367 self.loadPlugin(pluginName, self.pluginDirs["global"]) |
|
368 |
|
369 for pluginName in self.__foundUserModules: |
|
370 # core plug-ins have priority |
|
371 if ( |
|
372 pluginName not in self.__foundCoreModules and |
|
373 pluginName.startswith("PluginDocumentationSets") |
|
374 ): |
|
375 self.loadPlugin(pluginName, self.pluginDirs["user"]) |
|
376 |
|
377 for pluginName in self.__foundCoreModules: |
|
378 # plug-in under development has priority |
|
379 if pluginName.startswith("PluginDocumentationSets"): |
|
380 self.loadPlugin(pluginName, self.pluginDirs["eric6"]) |
|
381 |
|
382 def loadPlugin(self, name, directory, reload_=False, install=False): |
|
383 """ |
|
384 Public method to load a plugin module. |
|
385 |
|
386 Initially all modules are inactive. Modules that are requested on |
|
387 demand are sorted out and are added to the on demand list. Some |
|
388 basic validity checks are performed as well. Modules failing these |
|
389 checks are added to the failed modules list. |
|
390 |
|
391 @param name name of the module to be loaded |
|
392 @type str |
|
393 @param directory name of the plugin directory |
|
394 @type str |
|
395 @param reload_ flag indicating to reload the module |
|
396 @type bool |
|
397 @param install flag indicating a load operation as part of an |
|
398 installation process |
|
399 @type bool |
|
400 @exception PluginLoadError raised to indicate an issue loading |
|
401 the plug-in |
|
402 """ |
|
403 try: |
|
404 fname = "{0}.py".format(os.path.join(directory, name)) |
|
405 spec = importlib.util.spec_from_file_location(name, fname) |
|
406 module = importlib.util.module_from_spec(spec) |
|
407 sys.modules[module.__name__] = module |
|
408 spec.loader.exec_module(module) |
|
409 if not hasattr(module, "autoactivate"): |
|
410 module.error = self.tr( |
|
411 "Module is missing the 'autoactivate' attribute.") |
|
412 self.__failedModules[name] = module |
|
413 raise PluginLoadError(name) |
|
414 if getattr(module, "autoactivate", False): |
|
415 self.__inactiveModules[name] = module |
|
416 else: |
|
417 if ( |
|
418 not hasattr(module, "pluginType") or |
|
419 not hasattr(module, "pluginTypename") |
|
420 ): |
|
421 module.error = self.tr( |
|
422 "Module is missing the 'pluginType' " |
|
423 "and/or 'pluginTypename' attributes." |
|
424 ) |
|
425 self.__failedModules[name] = module |
|
426 raise PluginLoadError(name) |
|
427 else: |
|
428 self.__onDemandInactiveModules[name] = module |
|
429 module.eric6PluginModuleName = name |
|
430 module.eric6PluginModuleFilename = fname |
|
431 if install and hasattr(module, "installDependencies"): |
|
432 # ask the module to install its dependencies |
|
433 module.installDependencies(self.pipInstall) |
|
434 self.__modulesCount += 1 |
|
435 if reload_: |
|
436 importlib.reload(module) |
|
437 self.initOnDemandPlugin(name) |
|
438 with contextlib.suppress(KeyError, AttributeError): |
|
439 pluginObject = self.__onDemandInactivePlugins[name] |
|
440 pluginObject.initToolbar( |
|
441 self.__ui, e5App().getObject("ToolbarManager")) |
|
442 except PluginLoadError: |
|
443 print("Error loading plug-in module:", name) |
|
444 except Exception as err: |
|
445 module = types.ModuleType(name) |
|
446 module.error = self.tr( |
|
447 "Module failed to load. Error: {0}").format(str(err)) |
|
448 self.__failedModules[name] = module |
|
449 print("Error loading plug-in module:", name) |
|
450 print(str(err)) |
|
451 |
|
452 def unloadPlugin(self, name): |
|
453 """ |
|
454 Public method to unload a plugin module. |
|
455 |
|
456 @param name name of the module to be unloaded (string) |
|
457 @return flag indicating success (boolean) |
|
458 """ |
|
459 if name in self.__onDemandActiveModules: |
|
460 # cannot unload an ondemand plugin, that is in use |
|
461 return False |
|
462 |
|
463 if name in self.__activeModules: |
|
464 self.deactivatePlugin(name) |
|
465 |
|
466 if name in self.__inactiveModules: |
|
467 with contextlib.suppress(KeyError): |
|
468 pluginObject = self.__inactivePlugins[name] |
|
469 with contextlib.suppress(AttributeError): |
|
470 pluginObject.prepareUnload() |
|
471 del self.__inactivePlugins[name] |
|
472 del self.__inactiveModules[name] |
|
473 elif name in self.__onDemandInactiveModules: |
|
474 with contextlib.suppress(KeyError): |
|
475 pluginObject = self.__onDemandInactivePlugins[name] |
|
476 with contextlib.suppress(AttributeError): |
|
477 pluginObject.prepareUnload() |
|
478 del self.__onDemandInactivePlugins[name] |
|
479 del self.__onDemandInactiveModules[name] |
|
480 elif name in self.__failedModules: |
|
481 del self.__failedModules[name] |
|
482 |
|
483 self.__modulesCount -= 1 |
|
484 return True |
|
485 |
|
486 def removePluginFromSysModules(self, pluginName, package, |
|
487 internalPackages): |
|
488 """ |
|
489 Public method to remove a plugin and all related modules from |
|
490 sys.modules. |
|
491 |
|
492 @param pluginName name of the plugin module (string) |
|
493 @param package name of the plugin package (string) |
|
494 @param internalPackages list of intenal packages (list of string) |
|
495 @return flag indicating the plugin module was found in sys.modules |
|
496 (boolean) |
|
497 """ |
|
498 packages = [package] + internalPackages |
|
499 found = False |
|
500 if not package: |
|
501 package = "__None__" |
|
502 for moduleName in list(sys.modules.keys())[:]: |
|
503 if ( |
|
504 moduleName == pluginName or |
|
505 moduleName.split(".")[0] in packages |
|
506 ): |
|
507 found = True |
|
508 del sys.modules[moduleName] |
|
509 return found |
|
510 |
|
511 def initOnDemandPlugins(self): |
|
512 """ |
|
513 Public method to create plugin objects for all on demand plugins. |
|
514 |
|
515 Note: The plugins are not activated. |
|
516 """ |
|
517 names = sorted(self.__onDemandInactiveModules.keys()) |
|
518 for name in names: |
|
519 self.initOnDemandPlugin(name) |
|
520 |
|
521 def initOnDemandPlugin(self, name): |
|
522 """ |
|
523 Public method to create a plugin object for the named on demand plugin. |
|
524 |
|
525 Note: The plug-in is not activated. |
|
526 |
|
527 @param name name of the plug-in (string) |
|
528 @exception PluginActivationError raised to indicate an issue during the |
|
529 plug-in activation |
|
530 """ |
|
531 try: |
|
532 try: |
|
533 module = self.__onDemandInactiveModules[name] |
|
534 except KeyError: |
|
535 return |
|
536 |
|
537 if not self.__canActivatePlugin(module): |
|
538 raise PluginActivationError(module.eric6PluginModuleName) |
|
539 version = getattr(module, "version", "0.0.0") |
|
540 className = getattr(module, "className", "") |
|
541 pluginClass = getattr(module, className) |
|
542 pluginObject = None |
|
543 if name not in self.__onDemandInactivePlugins: |
|
544 pluginObject = pluginClass(self.__ui) |
|
545 pluginObject.eric6PluginModule = module |
|
546 pluginObject.eric6PluginName = className |
|
547 pluginObject.eric6PluginVersion = version |
|
548 self.__onDemandInactivePlugins[name] = pluginObject |
|
549 except PluginActivationError: |
|
550 return |
|
551 |
|
552 def initPluginToolbars(self, toolbarManager): |
|
553 """ |
|
554 Public method to initialize plug-in toolbars. |
|
555 |
|
556 @param toolbarManager reference to the toolbar manager object |
|
557 (E5ToolBarManager) |
|
558 """ |
|
559 self.initOnDemandPlugins() |
|
560 for pluginObject in self.__onDemandInactivePlugins.values(): |
|
561 with contextlib.suppress(AttributeError): |
|
562 pluginObject.initToolbar(self.__ui, toolbarManager) |
|
563 |
|
564 def activatePlugins(self): |
|
565 """ |
|
566 Public method to activate all plugins having the "autoactivate" |
|
567 attribute set to True. |
|
568 """ |
|
569 savedInactiveList = Preferences.Prefs.settings.value( |
|
570 self.__inactivePluginsKey) |
|
571 inactiveList = self.__disabledPlugins[:] |
|
572 if savedInactiveList is not None: |
|
573 inactiveList += [p for p in savedInactiveList |
|
574 if p not in self.__disabledPlugins] |
|
575 if ( |
|
576 self.__develPluginName is not None and |
|
577 self.__develPluginName in inactiveList |
|
578 ): |
|
579 inactiveList.remove(self.__develPluginName) |
|
580 names = sorted(self.__inactiveModules.keys()) |
|
581 for name in names: |
|
582 if name not in inactiveList: |
|
583 self.activatePlugin(name) |
|
584 self.allPlugginsActivated.emit() |
|
585 |
|
586 def activatePlugin(self, name, onDemand=False): |
|
587 """ |
|
588 Public method to activate a plugin. |
|
589 |
|
590 @param name name of the module to be activated |
|
591 @param onDemand flag indicating activation of an |
|
592 on demand plugin (boolean) |
|
593 @return reference to the initialized plugin object |
|
594 @exception PluginActivationError raised to indicate an issue during the |
|
595 plug-in activation |
|
596 """ |
|
597 try: |
|
598 try: |
|
599 module = ( |
|
600 self.__onDemandInactiveModules[name] |
|
601 if onDemand else |
|
602 self.__inactiveModules[name] |
|
603 ) |
|
604 except KeyError: |
|
605 return None |
|
606 |
|
607 if not self.__canActivatePlugin(module): |
|
608 raise PluginActivationError(module.eric6PluginModuleName) |
|
609 version = getattr(module, "version", "0.0.0") |
|
610 className = getattr(module, "className", "") |
|
611 pluginClass = getattr(module, className) |
|
612 pluginObject = None |
|
613 if onDemand and name in self.__onDemandInactivePlugins: |
|
614 pluginObject = self.__onDemandInactivePlugins[name] |
|
615 elif not onDemand and name in self.__inactivePlugins: |
|
616 pluginObject = self.__inactivePlugins[name] |
|
617 else: |
|
618 pluginObject = pluginClass(self.__ui) |
|
619 self.pluginAboutToBeActivated.emit(name, pluginObject) |
|
620 try: |
|
621 obj, ok = pluginObject.activate() |
|
622 except TypeError: |
|
623 module.error = self.tr( |
|
624 "Incompatible plugin activation method.") |
|
625 obj = None |
|
626 ok = True |
|
627 except Exception as err: |
|
628 module.error = str(err) |
|
629 obj = None |
|
630 ok = False |
|
631 if not ok: |
|
632 return None |
|
633 |
|
634 self.pluginActivated.emit(name, pluginObject) |
|
635 pluginObject.eric6PluginModule = module |
|
636 pluginObject.eric6PluginName = className |
|
637 pluginObject.eric6PluginVersion = version |
|
638 |
|
639 if onDemand: |
|
640 self.__onDemandInactiveModules.pop(name) |
|
641 with contextlib.suppress(KeyError): |
|
642 self.__onDemandInactivePlugins.pop(name) |
|
643 self.__onDemandActivePlugins[name] = pluginObject |
|
644 self.__onDemandActiveModules[name] = module |
|
645 else: |
|
646 self.__inactiveModules.pop(name) |
|
647 with contextlib.suppress(KeyError): |
|
648 self.__inactivePlugins.pop(name) |
|
649 self.__activePlugins[name] = pluginObject |
|
650 self.__activeModules[name] = module |
|
651 return obj |
|
652 except PluginActivationError: |
|
653 return None |
|
654 |
|
655 def __canActivatePlugin(self, module): |
|
656 """ |
|
657 Private method to check, if a plugin can be activated. |
|
658 |
|
659 @param module reference to the module to be activated |
|
660 @return flag indicating, if the module satisfies all requirements |
|
661 for being activated (boolean) |
|
662 @exception PluginModuleFormatError raised to indicate an invalid |
|
663 plug-in module format |
|
664 @exception PluginClassFormatError raised to indicate an invalid |
|
665 plug-in class format |
|
666 """ |
|
667 try: |
|
668 if not hasattr(module, "version"): |
|
669 raise PluginModuleFormatError( |
|
670 module.eric6PluginModuleName, "version") |
|
671 if not hasattr(module, "className"): |
|
672 raise PluginModuleFormatError( |
|
673 module.eric6PluginModuleName, "className") |
|
674 className = getattr(module, "className", "") |
|
675 if not className or not hasattr(module, className): |
|
676 raise PluginModuleFormatError( |
|
677 module.eric6PluginModuleName, className) |
|
678 pluginClass = getattr(module, className) |
|
679 if not hasattr(pluginClass, "__init__"): |
|
680 raise PluginClassFormatError( |
|
681 module.eric6PluginModuleName, |
|
682 className, "__init__") |
|
683 if not hasattr(pluginClass, "activate"): |
|
684 raise PluginClassFormatError( |
|
685 module.eric6PluginModuleName, |
|
686 className, "activate") |
|
687 if not hasattr(pluginClass, "deactivate"): |
|
688 raise PluginClassFormatError( |
|
689 module.eric6PluginModuleName, |
|
690 className, "deactivate") |
|
691 return True |
|
692 except PluginModuleFormatError as e: |
|
693 print(repr(e)) |
|
694 return False |
|
695 except PluginClassFormatError as e: |
|
696 print(repr(e)) |
|
697 return False |
|
698 |
|
699 def deactivatePlugin(self, name, onDemand=False): |
|
700 """ |
|
701 Public method to deactivate a plugin. |
|
702 |
|
703 @param name name of the module to be deactivated |
|
704 @param onDemand flag indicating deactivation of an |
|
705 on demand plugin (boolean) |
|
706 """ |
|
707 try: |
|
708 module = ( |
|
709 self.__onDemandActiveModules[name] |
|
710 if onDemand else |
|
711 self.__activeModules[name] |
|
712 ) |
|
713 except KeyError: |
|
714 return |
|
715 |
|
716 if self.__canDeactivatePlugin(module): |
|
717 pluginObject = None |
|
718 if onDemand and name in self.__onDemandActivePlugins: |
|
719 pluginObject = self.__onDemandActivePlugins[name] |
|
720 elif not onDemand and name in self.__activePlugins: |
|
721 pluginObject = self.__activePlugins[name] |
|
722 if pluginObject: |
|
723 self.pluginAboutToBeDeactivated.emit(name, pluginObject) |
|
724 pluginObject.deactivate() |
|
725 self.pluginDeactivated.emit(name, pluginObject) |
|
726 |
|
727 if onDemand: |
|
728 self.__onDemandActiveModules.pop(name) |
|
729 self.__onDemandActivePlugins.pop(name) |
|
730 self.__onDemandInactivePlugins[name] = pluginObject |
|
731 self.__onDemandInactiveModules[name] = module |
|
732 else: |
|
733 self.__activeModules.pop(name) |
|
734 with contextlib.suppress(KeyError): |
|
735 self.__activePlugins.pop(name) |
|
736 self.__inactivePlugins[name] = pluginObject |
|
737 self.__inactiveModules[name] = module |
|
738 |
|
739 def __canDeactivatePlugin(self, module): |
|
740 """ |
|
741 Private method to check, if a plugin can be deactivated. |
|
742 |
|
743 @param module reference to the module to be deactivated |
|
744 @return flag indicating, if the module satisfies all requirements |
|
745 for being deactivated (boolean) |
|
746 """ |
|
747 return getattr(module, "deactivateable", True) |
|
748 |
|
749 def getPluginObject(self, type_, typename, maybeActive=False): |
|
750 """ |
|
751 Public method to activate an ondemand plugin given by type and |
|
752 typename. |
|
753 |
|
754 @param type_ type of the plugin to be activated (string) |
|
755 @param typename name of the plugin within the type category (string) |
|
756 @param maybeActive flag indicating, that the plugin may be active |
|
757 already (boolean) |
|
758 @return reference to the initialized plugin object |
|
759 """ |
|
760 for name, module in list(self.__onDemandInactiveModules.items()): |
|
761 if ( |
|
762 getattr(module, "pluginType", "") == type_ and |
|
763 getattr(module, "pluginTypename", "") == typename |
|
764 ): |
|
765 return self.activatePlugin(name, onDemand=True) |
|
766 |
|
767 if maybeActive: |
|
768 for name, module in list(self.__onDemandActiveModules.items()): |
|
769 if ( |
|
770 getattr(module, "pluginType", "") == type_ and |
|
771 getattr(module, "pluginTypename", "") == typename |
|
772 ): |
|
773 self.deactivatePlugin(name, onDemand=True) |
|
774 return self.activatePlugin(name, onDemand=True) |
|
775 |
|
776 return None |
|
777 |
|
778 def getPluginInfos(self): |
|
779 """ |
|
780 Public method to get infos about all loaded plug-ins. |
|
781 |
|
782 @return list of dictionaries with keys "module_name", "plugin_name", |
|
783 "version", "auto_activate", "active", "short_desc", "error" |
|
784 @rtype list of dict ("module_name": str, "plugin_name": str, |
|
785 "version": str, "auto_activate": bool, "active": bool, |
|
786 "short_desc": str, "error": bool) |
|
787 """ |
|
788 infos = [] |
|
789 |
|
790 # 1. active, non-on-demand modules |
|
791 for name in list(self.__activeModules.keys()): |
|
792 info = self.__getShortInfo(self.__activeModules[name]) |
|
793 info.update({ |
|
794 "module_name": name, |
|
795 "auto_activate": True, |
|
796 "active": True, |
|
797 }) |
|
798 infos.append(info) |
|
799 |
|
800 # 2. inactive, non-on-demand modules |
|
801 for name in list(self.__inactiveModules.keys()): |
|
802 info = self.__getShortInfo(self.__inactiveModules[name]) |
|
803 info.update({ |
|
804 "module_name": name, |
|
805 "auto_activate": True, |
|
806 "active": False, |
|
807 }) |
|
808 infos.append(info) |
|
809 |
|
810 # 3. active, on-demand modules |
|
811 for name in list(self.__onDemandActiveModules.keys()): |
|
812 info = self.__getShortInfo(self.__onDemandActiveModules[name]) |
|
813 info.update({ |
|
814 "module_name": name, |
|
815 "auto_activate": False, |
|
816 "active": True, |
|
817 }) |
|
818 infos.append(info) |
|
819 |
|
820 # 4. inactive, non-on-demand modules |
|
821 for name in list(self.__onDemandInactiveModules.keys()): |
|
822 info = self.__getShortInfo(self.__onDemandInactiveModules[name]) |
|
823 info.update({ |
|
824 "module_name": name, |
|
825 "auto_activate": False, |
|
826 "active": False, |
|
827 }) |
|
828 infos.append(info) |
|
829 |
|
830 # 5. failed modules |
|
831 for name in list(self.__failedModules.keys()): |
|
832 info = self.__getShortInfo(self.__failedModules[name]) |
|
833 info.update({ |
|
834 "module_name": name, |
|
835 "auto_activate": False, |
|
836 "active": False, |
|
837 }) |
|
838 infos.append(info) |
|
839 |
|
840 return infos |
|
841 |
|
842 def __getShortInfo(self, module): |
|
843 """ |
|
844 Private method to extract the short info from a module. |
|
845 |
|
846 @param module module to extract short info from |
|
847 @return dictionay containing plug-in data |
|
848 @rtype dict ("plugin_name": str, "version": str, "short_desc": str, |
|
849 "error": bool) |
|
850 """ |
|
851 return { |
|
852 "plugin_name": getattr(module, "name", ""), |
|
853 "version": getattr(module, "version", ""), |
|
854 "short_desc": getattr(module, "shortDescription", ""), |
|
855 "error": bool(getattr(module, "error", "")), |
|
856 } |
|
857 |
|
858 def getPluginDetails(self, name): |
|
859 """ |
|
860 Public method to get detailed information about a plugin. |
|
861 |
|
862 @param name name of the module to get detailed infos about (string) |
|
863 @return details of the plugin as a dictionary |
|
864 """ |
|
865 details = {} |
|
866 |
|
867 autoactivate = True |
|
868 active = True |
|
869 |
|
870 if name in self.__activeModules: |
|
871 module = self.__activeModules[name] |
|
872 elif name in self.__inactiveModules: |
|
873 module = self.__inactiveModules[name] |
|
874 active = False |
|
875 elif name in self.__onDemandActiveModules: |
|
876 module = self.__onDemandActiveModules[name] |
|
877 autoactivate = False |
|
878 elif name in self.__onDemandInactiveModules: |
|
879 module = self.__onDemandInactiveModules[name] |
|
880 autoactivate = False |
|
881 active = False |
|
882 elif name in self.__failedModules: |
|
883 module = self.__failedModules[name] |
|
884 autoactivate = False |
|
885 active = False |
|
886 elif "_" in name: |
|
887 # try stripping of a postfix |
|
888 return self.getPluginDetails(name.rsplit("_", 1)[0]) |
|
889 else: |
|
890 # should not happen |
|
891 return None |
|
892 |
|
893 details["moduleName"] = name |
|
894 details["moduleFileName"] = getattr( |
|
895 module, "eric6PluginModuleFilename", "") |
|
896 details["pluginName"] = getattr(module, "name", "") |
|
897 details["version"] = getattr(module, "version", "") |
|
898 details["author"] = getattr(module, "author", "") |
|
899 details["description"] = getattr(module, "longDescription", "") |
|
900 details["autoactivate"] = autoactivate |
|
901 details["active"] = active |
|
902 details["error"] = getattr(module, "error", "") |
|
903 |
|
904 return details |
|
905 |
|
906 def doShutdown(self): |
|
907 """ |
|
908 Public method called to perform actions upon shutdown of the IDE. |
|
909 """ |
|
910 names = [] |
|
911 for name in list(self.__inactiveModules.keys()): |
|
912 names.append(name) |
|
913 Preferences.Prefs.settings.setValue(self.__inactivePluginsKey, names) |
|
914 |
|
915 self.shutdown.emit() |
|
916 |
|
917 def getPluginDisplayStrings(self, type_): |
|
918 """ |
|
919 Public method to get the display strings of all plugins of a specific |
|
920 type. |
|
921 |
|
922 @param type_ type of the plugins (string) |
|
923 @return dictionary with name as key and display string as value |
|
924 (dictionary of string) |
|
925 """ |
|
926 pluginDict = {} |
|
927 |
|
928 for module in ( |
|
929 list(self.__onDemandActiveModules.values()) + |
|
930 list(self.__onDemandInactiveModules.values()) |
|
931 ): |
|
932 if ( |
|
933 getattr(module, "pluginType", "") == type_ and |
|
934 getattr(module, "error", "") == "" |
|
935 ): |
|
936 plugin_name = getattr(module, "pluginTypename", "") |
|
937 if plugin_name: |
|
938 if hasattr(module, "displayString"): |
|
939 try: |
|
940 disp = module.displayString() |
|
941 except TypeError: |
|
942 disp = getattr(module, "displayString", "") |
|
943 if disp != "": |
|
944 pluginDict[plugin_name] = disp |
|
945 else: |
|
946 pluginDict[plugin_name] = plugin_name |
|
947 |
|
948 return pluginDict |
|
949 |
|
950 def getPluginPreviewPixmap(self, type_, name): |
|
951 """ |
|
952 Public method to get a preview pixmap of a plugin of a specific type. |
|
953 |
|
954 @param type_ type of the plugin (string) |
|
955 @param name name of the plugin type (string) |
|
956 @return preview pixmap (QPixmap) |
|
957 """ |
|
958 for module in ( |
|
959 list(self.__onDemandActiveModules.values()) + |
|
960 list(self.__onDemandInactiveModules.values()) |
|
961 ): |
|
962 if ( |
|
963 getattr(module, "pluginType", "") == type_ and |
|
964 getattr(module, "pluginTypename", "") == name |
|
965 ): |
|
966 if hasattr(module, "previewPix"): |
|
967 return module.previewPix() |
|
968 else: |
|
969 return QPixmap() |
|
970 |
|
971 return QPixmap() |
|
972 |
|
973 def getPluginApiFiles(self, language): |
|
974 """ |
|
975 Public method to get the list of API files installed by a plugin. |
|
976 |
|
977 @param language language of the requested API files (string) |
|
978 @return list of API filenames (list of string) |
|
979 """ |
|
980 apis = [] |
|
981 |
|
982 for module in ( |
|
983 list(self.__activeModules.values()) + |
|
984 list(self.__onDemandActiveModules.values()) |
|
985 ): |
|
986 if hasattr(module, "apiFiles"): |
|
987 apis.extend(module.apiFiles(language)) |
|
988 |
|
989 return apis |
|
990 |
|
991 def getPluginQtHelpFiles(self): |
|
992 """ |
|
993 Public method to get the list of QtHelp documentation files provided |
|
994 by a plug-in. |
|
995 |
|
996 @return dictionary with documentation type as key and list of files |
|
997 as value |
|
998 @rtype dict (key: str, value: list of str) |
|
999 """ |
|
1000 helpFiles = {} |
|
1001 for module in ( |
|
1002 list(self.__activeModules.values()) + |
|
1003 list(self.__onDemandActiveModules.values()) |
|
1004 ): |
|
1005 if hasattr(module, "helpFiles"): |
|
1006 helpFiles.update(module.helpFiles()) |
|
1007 |
|
1008 return helpFiles |
|
1009 |
|
1010 def getPluginExeDisplayData(self): |
|
1011 """ |
|
1012 Public method to get data to display information about a plugins |
|
1013 external tool. |
|
1014 |
|
1015 @return list of dictionaries containing the data. Each dictionary must |
|
1016 either contain data for the determination or the data to be |
|
1017 displayed.<br /> |
|
1018 A dictionary of the first form must have the following entries: |
|
1019 <ul> |
|
1020 <li>programEntry - indicator for this dictionary form |
|
1021 (boolean), always True</li> |
|
1022 <li>header - string to be diplayed as a header (string)</li> |
|
1023 <li>exe - the executable (string)</li> |
|
1024 <li>versionCommand - commandline parameter for the exe |
|
1025 (string)</li> |
|
1026 <li>versionStartsWith - indicator for the output line |
|
1027 containing the version (string)</li> |
|
1028 <li>versionPosition - number of element containing the |
|
1029 version (integer)</li> |
|
1030 <li>version - version to be used as default (string)</li> |
|
1031 <li>versionCleanup - tuple of two integers giving string |
|
1032 positions start and stop for the version string |
|
1033 (tuple of integers)</li> |
|
1034 </ul> |
|
1035 A dictionary of the second form must have the following entries: |
|
1036 <ul> |
|
1037 <li>programEntry - indicator for this dictionary form |
|
1038 (boolean), always False</li> |
|
1039 <li>header - string to be diplayed as a header (string)</li> |
|
1040 <li>text - entry text to be shown (string)</li> |
|
1041 <li>version - version text to be shown (string)</li> |
|
1042 </ul> |
|
1043 """ |
|
1044 infos = [] |
|
1045 |
|
1046 for module in ( |
|
1047 list(self.__activeModules.values()) + |
|
1048 list(self.__inactiveModules.values()) |
|
1049 ): |
|
1050 if hasattr(module, "exeDisplayDataList"): |
|
1051 infos.extend(module.exeDisplayDataList()) |
|
1052 elif hasattr(module, "exeDisplayData"): |
|
1053 infos.append(module.exeDisplayData()) |
|
1054 for module in ( |
|
1055 list(self.__onDemandActiveModules.values()) + |
|
1056 list(self.__onDemandInactiveModules.values()) |
|
1057 ): |
|
1058 if hasattr(module, "exeDisplayDataList"): |
|
1059 infos.extend(module.exeDisplayDataList()) |
|
1060 elif hasattr(module, "exeDisplayData"): |
|
1061 infos.append(module.exeDisplayData()) |
|
1062 |
|
1063 return infos |
|
1064 |
|
1065 def getPluginConfigData(self): |
|
1066 """ |
|
1067 Public method to get the config data of all active, non on-demand |
|
1068 plugins used by the configuration dialog. |
|
1069 |
|
1070 Plugins supporting this functionality must provide the plugin module |
|
1071 function 'getConfigData' returning a dictionary with unique keys |
|
1072 of lists with the following list contents: |
|
1073 <dl> |
|
1074 <dt>display string</dt> |
|
1075 <dd>string shown in the selection area of the configuration page. |
|
1076 This should be a localized string</dd> |
|
1077 <dt>pixmap name</dt> |
|
1078 <dd>filename of the pixmap to be shown next to the display |
|
1079 string</dd> |
|
1080 <dt>page creation function</dt> |
|
1081 <dd>plugin module function to be called to create the configuration |
|
1082 page. The page must be subclasses from |
|
1083 Preferences.ConfigurationPages.ConfigurationPageBase and must |
|
1084 implement a method called 'save' to save the settings. A parent |
|
1085 entry will be created in the selection list, if this value is |
|
1086 None.</dd> |
|
1087 <dt>parent key</dt> |
|
1088 <dd>dictionary key of the parent entry or None, if this defines a |
|
1089 toplevel entry.</dd> |
|
1090 <dt>reference to configuration page</dt> |
|
1091 <dd>This will be used by the configuration dialog and must always |
|
1092 be None</dd> |
|
1093 </dl> |
|
1094 |
|
1095 @return plug-in configuration data |
|
1096 """ |
|
1097 configData = {} |
|
1098 for module in ( |
|
1099 list(self.__activeModules.values()) + |
|
1100 list(self.__onDemandActiveModules.values()) + |
|
1101 list(self.__onDemandInactiveModules.values()) |
|
1102 ): |
|
1103 if hasattr(module, 'getConfigData'): |
|
1104 configData.update(module.getConfigData()) |
|
1105 return configData |
|
1106 |
|
1107 def isPluginLoaded(self, pluginName): |
|
1108 """ |
|
1109 Public method to check, if a certain plugin is loaded. |
|
1110 |
|
1111 @param pluginName name of the plugin to check for (string) |
|
1112 @return flag indicating, if the plugin is loaded (boolean) |
|
1113 """ |
|
1114 return ( |
|
1115 pluginName in self.__activeModules or |
|
1116 pluginName in self.__inactiveModules or |
|
1117 pluginName in self.__onDemandActiveModules or |
|
1118 pluginName in self.__onDemandInactiveModules |
|
1119 ) |
|
1120 |
|
1121 def isPluginActive(self, pluginName): |
|
1122 """ |
|
1123 Public method to check, if a certain plugin is active. |
|
1124 |
|
1125 @param pluginName name of the plugin to check for (string) |
|
1126 @return flag indicating, if the plugin is active (boolean) |
|
1127 """ |
|
1128 return ( |
|
1129 pluginName in self.__activeModules or |
|
1130 pluginName in self.__onDemandActiveModules |
|
1131 ) |
|
1132 |
|
1133 ########################################################################### |
|
1134 ## Specialized plug-in module handling methods below |
|
1135 ########################################################################### |
|
1136 |
|
1137 ########################################################################### |
|
1138 ## VCS related methods below |
|
1139 ########################################################################### |
|
1140 |
|
1141 def getVcsSystemIndicators(self): |
|
1142 """ |
|
1143 Public method to get the Vcs System indicators. |
|
1144 |
|
1145 Plugins supporting this functionality must support the module function |
|
1146 getVcsSystemIndicator returning a dictionary with indicator as key and |
|
1147 a tuple with the vcs name (string) and vcs display string (string). |
|
1148 |
|
1149 @return dictionary with indicator as key and a list of tuples as |
|
1150 values. Each tuple contains the vcs name (string) and vcs display |
|
1151 string (string). |
|
1152 """ |
|
1153 vcsDict = {} |
|
1154 |
|
1155 for module in ( |
|
1156 list(self.__onDemandActiveModules.values()) + |
|
1157 list(self.__onDemandInactiveModules.values()) |
|
1158 ): |
|
1159 if ( |
|
1160 getattr(module, "pluginType", "") == "version_control" and |
|
1161 hasattr(module, "getVcsSystemIndicator") |
|
1162 ): |
|
1163 res = module.getVcsSystemIndicator() |
|
1164 for indicator, vcsData in list(res.items()): |
|
1165 if indicator in vcsDict: |
|
1166 vcsDict[indicator].append(vcsData) |
|
1167 else: |
|
1168 vcsDict[indicator] = [vcsData] |
|
1169 |
|
1170 return vcsDict |
|
1171 |
|
1172 def deactivateVcsPlugins(self): |
|
1173 """ |
|
1174 Public method to deactivated all activated VCS plugins. |
|
1175 """ |
|
1176 for name, module in list(self.__onDemandActiveModules.items()): |
|
1177 if getattr(module, "pluginType", "") == "version_control": |
|
1178 self.deactivatePlugin(name, True) |
|
1179 |
|
1180 ######################################################################## |
|
1181 ## Methods for the creation of the plug-ins download directory |
|
1182 ######################################################################## |
|
1183 |
|
1184 def __checkPluginsDownloadDirectory(self): |
|
1185 """ |
|
1186 Private slot to check for the existence of the plugins download |
|
1187 directory. |
|
1188 """ |
|
1189 downloadDir = Preferences.getPluginManager("DownloadPath") |
|
1190 if not downloadDir: |
|
1191 downloadDir = self.__defaultDownloadDir |
|
1192 |
|
1193 if not os.path.exists(downloadDir): |
|
1194 try: |
|
1195 os.mkdir(downloadDir, 0o755) |
|
1196 except OSError: |
|
1197 # try again with (possibly) new default |
|
1198 downloadDir = self.__defaultDownloadDir |
|
1199 if not os.path.exists(downloadDir): |
|
1200 try: |
|
1201 os.mkdir(downloadDir, 0o755) |
|
1202 except OSError as err: |
|
1203 E5MessageBox.critical( |
|
1204 self.__ui, |
|
1205 self.tr("Plugin Manager Error"), |
|
1206 self.tr( |
|
1207 """<p>The plugin download directory""" |
|
1208 """ <b>{0}</b> could not be created. Please""" |
|
1209 """ configure it via the configuration""" |
|
1210 """ dialog.</p><p>Reason: {1}</p>""") |
|
1211 .format(downloadDir, str(err))) |
|
1212 downloadDir = "" |
|
1213 |
|
1214 Preferences.setPluginManager("DownloadPath", downloadDir) |
|
1215 |
|
1216 def preferencesChanged(self): |
|
1217 """ |
|
1218 Public slot to react to changes in configuration. |
|
1219 """ |
|
1220 self.__checkPluginsDownloadDirectory() |
|
1221 |
|
1222 ######################################################################## |
|
1223 ## Methods for automatic plug-in update check below |
|
1224 ######################################################################## |
|
1225 |
|
1226 def __onlineStateChanged(self, online): |
|
1227 """ |
|
1228 Private slot handling changes in online state. |
|
1229 |
|
1230 @param online flag indicating the online state |
|
1231 @type bool |
|
1232 """ |
|
1233 if online: |
|
1234 self.checkPluginUpdatesAvailable() |
|
1235 |
|
1236 def checkPluginUpdatesAvailable(self): |
|
1237 """ |
|
1238 Public method to check the availability of updates of plug-ins. |
|
1239 """ |
|
1240 period = Preferences.getPluginManager("UpdatesCheckInterval") |
|
1241 if period == 0: |
|
1242 return |
|
1243 elif period in [1, 2, 3]: |
|
1244 lastModified = QFileInfo(self.pluginRepositoryFile).lastModified() |
|
1245 if lastModified.isValid() and lastModified.date().isValid(): |
|
1246 lastModifiedDate = lastModified.date() |
|
1247 now = QDate.currentDate() |
|
1248 if ( |
|
1249 (period == 1 and lastModifiedDate.day() == now.day()) or |
|
1250 (period == 2 and lastModifiedDate.daysTo(now) < 7) or |
|
1251 (period == 3 and (lastModifiedDate.daysTo(now) < |
|
1252 lastModifiedDate.daysInMonth())) |
|
1253 ): |
|
1254 # daily, weekly, monthly |
|
1255 return |
|
1256 |
|
1257 self.__updateAvailable = False |
|
1258 |
|
1259 request = QNetworkRequest( |
|
1260 QUrl(Preferences.getUI("PluginRepositoryUrl6"))) |
|
1261 request.setAttribute( |
|
1262 QNetworkRequest.Attribute.CacheLoadControlAttribute, |
|
1263 QNetworkRequest.CacheLoadControl.AlwaysNetwork) |
|
1264 reply = self.__networkManager.get(request) |
|
1265 reply.finished.connect( |
|
1266 lambda: self.__downloadRepositoryFileDone(reply)) |
|
1267 self.__replies.append(reply) |
|
1268 |
|
1269 def __downloadRepositoryFileDone(self, reply): |
|
1270 """ |
|
1271 Private method called after the repository file was downloaded. |
|
1272 |
|
1273 @param reply reference to the reply object of the download |
|
1274 @type QNetworkReply |
|
1275 """ |
|
1276 if reply in self.__replies: |
|
1277 self.__replies.remove(reply) |
|
1278 |
|
1279 if reply.error() != QNetworkReply.NetworkError.NoError: |
|
1280 E5MessageBox.warning( |
|
1281 None, |
|
1282 self.tr("Error downloading file"), |
|
1283 self.tr( |
|
1284 """<p>Could not download the requested file""" |
|
1285 """ from {0}.</p><p>Error: {1}</p>""" |
|
1286 ).format(Preferences.getUI("PluginRepositoryUrl6"), |
|
1287 reply.errorString()) |
|
1288 ) |
|
1289 reply.deleteLater() |
|
1290 return |
|
1291 |
|
1292 ioDevice = QFile(self.pluginRepositoryFile + ".tmp") |
|
1293 ioDevice.open(QIODevice.OpenModeFlag.WriteOnly) |
|
1294 ioDevice.write(reply.readAll()) |
|
1295 ioDevice.close() |
|
1296 if QFile.exists(self.pluginRepositoryFile): |
|
1297 QFile.remove(self.pluginRepositoryFile) |
|
1298 ioDevice.rename(self.pluginRepositoryFile) |
|
1299 reply.deleteLater() |
|
1300 |
|
1301 if os.path.exists(self.pluginRepositoryFile): |
|
1302 f = QFile(self.pluginRepositoryFile) |
|
1303 if f.open(QIODevice.OpenModeFlag.ReadOnly): |
|
1304 # save current URL |
|
1305 url = Preferences.getUI("PluginRepositoryUrl6") |
|
1306 |
|
1307 # read the repository file |
|
1308 from E5XML.PluginRepositoryReader import PluginRepositoryReader |
|
1309 reader = PluginRepositoryReader(f, self.checkPluginEntry) |
|
1310 reader.readXML() |
|
1311 if url != Preferences.getUI("PluginRepositoryUrl6"): |
|
1312 # redo if it is a redirect |
|
1313 self.checkPluginUpdatesAvailable() |
|
1314 return |
|
1315 |
|
1316 if self.__updateAvailable: |
|
1317 res = E5MessageBox.information( |
|
1318 None, |
|
1319 self.tr("New plugin versions available"), |
|
1320 self.tr("<p>There are new plug-ins or plug-in" |
|
1321 " updates available. Use the plug-in" |
|
1322 " repository dialog to get them.</p>"), |
|
1323 E5MessageBox.StandardButtons( |
|
1324 E5MessageBox.Ignore | |
|
1325 E5MessageBox.Open), |
|
1326 E5MessageBox.Open) |
|
1327 if res == E5MessageBox.Open: |
|
1328 self.__ui.showPluginsAvailable() |
|
1329 |
|
1330 def checkPluginEntry(self, name, short, description, url, author, version, |
|
1331 filename, status): |
|
1332 """ |
|
1333 Public method to check a plug-in's data for an update. |
|
1334 |
|
1335 @param name data for the name field (string) |
|
1336 @param short data for the short field (string) |
|
1337 @param description data for the description field (list of strings) |
|
1338 @param url data for the url field (string) |
|
1339 @param author data for the author field (string) |
|
1340 @param version data for the version field (string) |
|
1341 @param filename data for the filename field (string) |
|
1342 @param status status of the plugin (string [stable, unstable, unknown]) |
|
1343 """ |
|
1344 # ignore hidden plug-ins |
|
1345 pluginName = os.path.splitext(url.rsplit("/", 1)[1])[0] |
|
1346 if pluginName in Preferences.getPluginManager("HiddenPlugins"): |
|
1347 return |
|
1348 |
|
1349 archive = os.path.join(Preferences.getPluginManager("DownloadPath"), |
|
1350 filename) |
|
1351 |
|
1352 # Check against installed/loaded plug-ins |
|
1353 pluginDetails = self.getPluginDetails(pluginName) |
|
1354 if pluginDetails is None: |
|
1355 if not Preferences.getPluginManager("CheckInstalledOnly"): |
|
1356 self.__updateAvailable = True |
|
1357 return |
|
1358 |
|
1359 versionTuple = Globals.versionToTuple(version)[:3] |
|
1360 pluginVersionTuple = Globals.versionToTuple( |
|
1361 pluginDetails["version"])[:3] |
|
1362 |
|
1363 if pluginVersionTuple < versionTuple: |
|
1364 self.__updateAvailable = True |
|
1365 return |
|
1366 |
|
1367 if not Preferences.getPluginManager("CheckInstalledOnly"): |
|
1368 # Check against downloaded plugin archives |
|
1369 # 1. Check, if the archive file exists |
|
1370 if not os.path.exists(archive): |
|
1371 if pluginDetails["moduleName"] != pluginName: |
|
1372 self.__updateAvailable = True |
|
1373 return |
|
1374 |
|
1375 # 2. Check, if the archive is a valid zip file |
|
1376 if not zipfile.is_zipfile(archive): |
|
1377 self.__updateAvailable = True |
|
1378 return |
|
1379 |
|
1380 # 3. Check the version of the archive file |
|
1381 zipFile = zipfile.ZipFile(archive, "r") |
|
1382 try: |
|
1383 aversion = zipFile.read("VERSION").decode("utf-8") |
|
1384 except KeyError: |
|
1385 aversion = "0.0.0" |
|
1386 zipFile.close() |
|
1387 |
|
1388 aversionTuple = Globals.versionToTuple(aversion)[:3] |
|
1389 if aversionTuple != versionTuple: |
|
1390 self.__updateAvailable = True |
|
1391 |
|
1392 def __sslErrors(self, reply, errors): |
|
1393 """ |
|
1394 Private slot to handle SSL errors. |
|
1395 |
|
1396 @param reply reference to the reply object (QNetworkReply) |
|
1397 @param errors list of SSL errors (list of QSslError) |
|
1398 """ |
|
1399 ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0] |
|
1400 if ignored == E5SslErrorState.NOT_IGNORED: |
|
1401 self.__downloadCancelled = True |
|
1402 |
|
1403 ######################################################################## |
|
1404 ## Methods to clear private data of plug-ins below |
|
1405 ######################################################################## |
|
1406 |
|
1407 def clearPluginsPrivateData(self, type_): |
|
1408 """ |
|
1409 Public method to clear the private data of plug-ins of a specified |
|
1410 type. |
|
1411 |
|
1412 Plugins supporting this functionality must support the module function |
|
1413 clearPrivateData() and have the module level attribute pluginType. |
|
1414 |
|
1415 @param type_ type of the plugin to clear private data for (string) |
|
1416 """ |
|
1417 for module in ( |
|
1418 list(self.__onDemandActiveModules.values()) + |
|
1419 list(self.__onDemandInactiveModules.values()) + |
|
1420 list(self.__activeModules.values()) + |
|
1421 list(self.__inactiveModules.values()) |
|
1422 ): |
|
1423 if ( |
|
1424 getattr(module, "pluginType", "") == type_ and |
|
1425 hasattr(module, "clearPrivateData") |
|
1426 ): |
|
1427 module.clearPrivateData() |
|
1428 |
|
1429 ######################################################################## |
|
1430 ## Methods to install a plug-in module dependency via pip |
|
1431 ######################################################################## |
|
1432 |
|
1433 def pipInstall(self, packages): |
|
1434 """ |
|
1435 Public method to install the given package via pip. |
|
1436 |
|
1437 @param packages list of packages to install |
|
1438 @type list of str |
|
1439 """ |
|
1440 try: |
|
1441 pip = e5App().getObject("Pip") |
|
1442 except KeyError: |
|
1443 # Installation is performed via the plug-in installation script. |
|
1444 from PipInterface.Pip import Pip |
|
1445 pip = Pip(self) |
|
1446 pip.installPackages(packages, interpreter=sys.executable) |
|
1447 |
|
1448 # |
|
1449 # eflag: noqa = M801 |