eric6/PluginManager/PluginManager.py

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

eric ide

mercurial