eric7/PluginManager/PluginManager.py

branch
eric7
changeset 8312
800c432b34c8
parent 8306
2bfd53096b5f
child 8314
e3642a6a1e71
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
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

eric ide

mercurial