src/eric7/PluginManager/PluginManager.py

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

eric ide

mercurial