1 2# Copyright 2008-2018 Jaap Karssenberg <jaap.karssenberg@gmail.com> 3 4'''API documentation of the zim plugin framework. 5 6This file contains the base classes used to write plugins for zim. Each 7plugin is defined as a sub-module in the "zim.plugins" namespace. 8 9To be recognized as a plugin, a submodule of "zim.plugins" needs to 10define one (and only one) sub-class of L{PluginClass}. This class 11will define the main plugin object and contains meta data about the 12plugin and e.g. plugin preferences. 13 14The plugin object itself doesn't directly interact with the rest of the 15zim application. To actually add functionality to zim, the plugin module 16will also need to define one or more "extension" classes. These classes 17act as decorators for specific objects that appear in the application. 18They will be instantiated automatically whenever the target object is 19created. The extension object then has direct access to the API of the 20object that is being extended. 21 22Each extension object that is instantiated is linked to the plugin object 23that it belongs to. So it can access functions of the plugin object and 24it can use the plugin object to find other extension objects if it 25needs to cooperate. 26 27Also defined here is the L{PluginManager} class. This class is the 28interface towards the rest of the application to load/unload plugins and 29to let plugins extend specific application objects. 30''' 31 32 33from gi.repository import GObject 34import types 35import os 36import sys 37import logging 38import inspect 39import weakref 40 41try: 42 import collections.abc as abc 43except ImportError: 44 # python < version 3.3 45 import collections as abc 46 47from zim.newfs import LocalFolder, LocalFile 48 49from zim.signals import SignalEmitter, ConnectorMixin, SIGNAL_AFTER, SIGNAL_RUN_LAST, SignalHandler 50from zim.utils import classproperty, get_module, lookup_subclass, lookup_subclasses 51from zim.actions import hasaction, get_actions 52 53from zim.config import data_dirs, XDG_DATA_HOME, ConfigManager 54from zim.insertedobjects import InsertedObjectType 55 56 57logger = logging.getLogger('zim.plugins') 58 59 60# Extend path for importing and searching plugins 61# 62# Set C{__path__} for the C{zim.plugins} module. This determines what 63# directories are searched when importing plugin packages in the 64# C{zim.plugins} namespace. 65# 66# Originally this added to the C{__path__} folders based on C{sys.path} 67# however this leads to conflicts when multiple zim versions are 68# installed. By switching to XDG_DATA_HOME this conflict is removed 69# by separating custom plugins and default plugins from other versions. 70# Also this switch makes it easier to have a single instruction for 71# users where to put custom plugins. 72 73PLUGIN_FOLDER = XDG_DATA_HOME.subdir('zim/plugins') 74 75for dir in data_dirs('plugins'): 76 __path__.append(dir.path) 77 78__path__.append(__path__.pop(0)) # reshuffle real module path to the end 79__path__.insert(0, PLUGIN_FOLDER.path) # Should be redundant, but need to be sure 80 81#print("PLUGIN PATH:", __path__) 82 83 84class _BootstrapPluginManager(object): 85 86 def __init__(self): 87 self._extendables = [] 88 89 def register_new_extendable(self, extendable): 90 self._extendables.append(extendable) 91 92 93_bootstrappluginmanager = _BootstrapPluginManager() 94PluginManager = _bootstrappluginmanager 95 96 97def extendable(*extension_bases, register_after_init=True): 98 '''Class decorator to mark a class as "extendable" 99 @param extension_bases: base classes for extensions 100 @param register_after_init: if C{True} the class is registered with the L{PluginManager} 101 directly after it's C{__init__()} method has run. If C{False} the class 102 can call C{PluginManager.register_new_extendable(self)} explicitly whenever ready. 103 ''' 104 assert all(issubclass(ec, ExtensionBase) for ec in extension_bases) 105 106 def _extendable(cls): 107 orig_init = cls.__init__ 108 109 def _init_wrapper(self, *arg, **kwarg): 110 self._zim_extendable_registered = False 111 if not hasattr(self, '__zim_extension_objects__'): 112 self.__zim_extension_objects__ = [] 113 # Must be before orig_init to allow init to add "built-in" 114 # extensions for discoverability of actions (e.g. mainwindow._uiactions) 115 orig_init(self, *arg, **kwarg) 116 self.__zim_extension_bases__ = extension_bases 117 # Must be after orig_init to allow sub-classes of extendables 118 # to override the parent class 119 if register_after_init: 120 PluginManager.register_new_extendable(self) 121 122 cls.__init__ = _init_wrapper 123 124 return cls 125 126 return _extendable 127 128 129def find_extension(obj, klass): 130 '''Lookup an extension object 131 This function allows finding extension classes defined by any plugin. 132 So it can be used to find an defined by the same plugin, but also allows 133 cooperation by other plugins. 134 The lookup uses C{isinstance()}, so abstract classes can be used to define 135 interfaces between plugins if you don't want to depent on the exact 136 implementation class. 137 @param obj: the extended object 138 @param klass: the class of the extention object 139 @returns: a single extension object, if multiple extensions match, the 140 first is returned 141 @raises ValueError: if no extension was found 142 ''' 143 if hasattr(obj, '__zim_extension_objects__'): 144 for e in obj.__zim_extension_objects__: 145 if isinstance(e, klass): 146 return e 147 148 raise ValueError('No extension of class found: %s' % klass) 149 150 151def find_action(obj, actionname): 152 '''Lookup an action method 153 Returns an action method (defined with C{@action} or C{@toggle_action}) 154 for either the object itself, or any of it's extensions. 155 This allows cooperation between plugins by calling actions defined by 156 an other plugin action. 157 @param obj: the extended object 158 @param actionname: the name of the action 159 @returns: an action method 160 @raises ValueError: if no action was found 161 ''' 162 actionname = actionname.replace('-', '_') 163 if hasaction(obj, actionname): 164 return getattr(obj, actionname) 165 else: 166 if hasattr(obj, '__zim_extension_objects__'): 167 for e in obj.__zim_extension_objects__: 168 if hasaction(e, actionname): 169 return getattr(e, actionname) 170 raise ValueError('Action not found: %s' % actionname) 171 172 173def list_actions(obj): 174 '''List actions 175 Returns list of actions of C{obj} followed by all actions of 176 all of it's extensions. Each action is a 2-tuple of the action and it's name. 177 ''' 178 actions = get_actions(obj) 179 if hasattr(obj, '__zim_extension_objects__'): 180 for e in obj.__zim_extension_objects__: 181 actions.extend(get_actions(e)) 182 return actions 183 184 185class ExtensionBase(SignalEmitter, ConnectorMixin): 186 '''Base class for all extensions classes 187 @ivar plugin: the plugin object to which this extension belongs 188 @ivar obj: the extendable object 189 ''' 190 191 __signals__ = {} 192 193 def __init__(self, plugin, obj): 194 '''Constructor 195 @param plugin: the plugin object to which this extension belongs 196 @param obj: the object being extended 197 ''' 198 self.plugin = plugin 199 self.obj = obj 200 obj.__zim_extension_objects__.append(self) 201 202 def destroy(self): 203 '''Called when the plugin is being destroyed 204 Calls L{teardown()} followed by the C{teardown()} methods of 205 parent base classes. 206 ''' 207 def walk(klass): 208 yield klass 209 for base in klass.__bases__: 210 if issubclass(base, ExtensionBase): 211 for k in walk(base): # recurs 212 yield k 213 214 for klass in walk(self.__class__): 215 try: 216 klass.teardown(self) 217 except: 218 logger.exception('Exception while disconnecting %s (%s)', self, klass) 219 # in case you are wondering: issubclass(Foo, Foo) evaluates True 220 221 try: 222 self.obj.__zim_extension_objects__.remove(self) 223 except AttributeError: 224 pass 225 except ValueError: 226 pass 227 finally: 228 PluginManager.emit('extensions-changed', self.obj) 229 230 self.plugin.extensions.discard(self) 231 # Avoid waiting for garbage collection to take place 232 233 def teardown(self): 234 '''Remove changes made by B{this} class from the extended object 235 To be overloaded by child classes 236 @note: do not call parent class C{teardown()} here, that is 237 already taken care of by C{destroy()} 238 ''' 239 self.disconnect_all() 240 241 242class DialogExtensionBase(ExtensionBase): 243 '''Base class for extending Gtk dialogs based on C{Gtk.Dialog} 244 @ivar dialog: the C{Gtk.Dialog} object 245 ''' 246 247 def __init__(self, plugin, dialog): 248 ExtensionBase.__init__(self, plugin, dialog) 249 self.dialog = dialog 250 self._dialog_buttons = [] 251 self.connectto(dialog, 'destroy') 252 253 def on_destroy(self, dialog): 254 self.destroy() 255 256 def add_dialog_button(self, button): 257 '''Add a new button to the bottom area of the dialog 258 The button is placed left of the standard buttons like the 259 "OK" / "Cancel" or "Close" button of the dialog. 260 @param button: a C{Gtk.Button} or similar widget 261 ''' 262 # This logic adds the button to the action area and places 263 # it left of the left most primary button by reshuffling all 264 # other buttons after adding the new one 265 # 266 # TODO: check if this works correctly in RTL configuration 267 self.dialog.action_area.pack_end(button, False, True, 0) # puts button in right most position 268 self._dialog_buttons.append(button) 269 buttons = [b for b in self.dialog.action_area.get_children() 270 if not self.dialog.action_area.child_get_property(b, 'secondary')] 271 for b in buttons: 272 if b is not button: 273 self.dialog.action_area.reorder_child(b, -1) # reshuffle to the right 274 275 def teardown(self): 276 for b in self._dialog_buttons: 277 self.dialog.action_area.remove(b) 278 279 280class InsertedObjectTypeExtension(InsertedObjectType, ExtensionBase): 281 282 def __init__(self, plugin, objmap): 283 InsertedObjectType.__init__(self) 284 ExtensionBase.__init__(self, plugin, objmap) 285 objmap.register_object(self) 286 self._objmap = objmap 287 288 def teardown(self): 289 self._objmap.unregister_object(self) 290 291 292@extendable(InsertedObjectTypeExtension) 293class InsertedObjectTypeMap(SignalEmitter): 294 '''Mapping of L{InsertedObjectTypeExtension} objects. 295 This is a proxy for loading object types defined in plugins. 296 For convenience you can use C{PluginManager.insertedobjects} to access 297 an instance of this mapping. 298 ''' 299 300 # Note: Wanted to inherit from collections.abc.Mapping 301 # but conflicts with metaclass use for SignalEmitter 302 # .. fixing using _MyMeta gives other issues ... 303 304 __signals__ = { 305 'changed': (SIGNAL_RUN_LAST, None, ()), 306 } 307 308 def __init__(self): 309 self._objects = {} 310 311 def __getitem__(self, name): 312 return self._objects[name.lower()] 313 314 def __iter__(self): 315 return iter(sorted(self._objects.keys())) 316 # sort to make operation predictable - easier debugging 317 318 def __len__(self): 319 return len(self._objects) 320 321 def __contains__(self, name): 322 return name.lower() in self._objects 323 324 def keys(self): 325 return [k for k in self] 326 327 def items(self): 328 return [(k, self[v]) for k in self] 329 330 def values(self): 331 return [self[k] for k in self] 332 333 def get(self, name, default=None): 334 return self._objects.get(name.lower(), default) 335 336 def register_object(self, objecttype): 337 '''Register an object type 338 @param objecttype: an object derived from L{InsertedObjectType} 339 @raises AssertionError: if another object already uses the same name 340 ''' 341 key = objecttype.name.lower() 342 logger.debug('register_object: "%s"', key) 343 if key in self._objects: 344 raise AssertionError('InsertedObjectType "%s" already defined by %s' % (key, self._objects[key])) 345 else: 346 self._objects[key] = objecttype 347 self.emit('changed') 348 349 def unregister_object(self, objecttype): 350 '''Unregister a specific object type. 351 @param objecttype: an object derived from L{InsertedObjectType} 352 ''' 353 key = objecttype.name.lower() 354 logger.debug('unregister_object: "%s"', key) 355 if key in self._objects and self._objects[key] is objecttype: 356 self._objects.pop(key) 357 self.emit('changed') 358 359 360class _MyMeta(type(SignalEmitter), type(abc.Mapping)): 361 # Combine meta classes to resolve conflict 362 pass 363 364 365class PluginManagerClass(ConnectorMixin, SignalEmitter, abc.Mapping, metaclass=_MyMeta): 366 '''Manager that maintains a set of active plugins 367 368 This class is the interface towards the rest of the application to 369 load/unload plugins. It behaves as a dictionary with plugin object names as 370 keys and plugin objects as value 371 ''' 372 373 __signals__ = { 374 'extensions-changed': (SIGNAL_RUN_LAST, None, (object,)), 375 } 376 377 def __init__(self): 378 '''Constructor 379 Constructor will directly load a list of default plugins 380 based on the preferences in the config. Failures while loading 381 these plugins will be logged but not raise errors. 382 383 @param config: a L{ConfigManager} object that is passed along 384 to the plugins and is used to load plugin preferences. 385 Defaults to a L{VirtualConfigManager} for testing. 386 ''' 387 self._reset() 388 389 def _reset(self): 390 self._preferences = ConfigManager.preferences['General'] 391 self._preferences.setdefault('plugins', []) 392 393 self._plugins = {} 394 self._extendable_weakrefs = [] 395 self.failed = set() 396 397 self.insertedobjects = InsertedObjectTypeMap() 398 399 def _extendables(self): 400 # Used WeakSet before, but order of loading is important. This method 401 # returns the alive objects and cleans up the list in one go 402 extendables = [] 403 weakrefs = [] 404 for ref in self._extendable_weakrefs: 405 ext = ref() 406 if ext is not None: 407 extendables.append(ext) 408 weakrefs.append(ref) 409 self._extendable_weakrefs = weakrefs 410 return extendables 411 412 def load_plugins_from_preferences(self, names): 413 '''Calls L{load_plugin()} for each plugin in C{names} but does not 414 raise an exception when loading fails. 415 ''' 416 for name in names: 417 try: 418 self.load_plugin(name) 419 except Exception as exc: 420 if isinstance(exc, ImportError): 421 logger.info('No such plugin: %s', name) 422 else: 423 logger.exception('Exception while loading plugin: %s', name) 424 if name in self._preferences['plugins']: 425 self._preferences['plugins'].remove(name) 426 self.failed.add(name) 427 428 def __call__(self): 429 return self # singleton behavior if called as class 430 431 def __getitem__(self, name): 432 return self._plugins[name] 433 434 def __iter__(self): 435 return iter(sorted(self._plugins.keys())) 436 # sort to make operation predictable - easier debugging 437 438 def __len__(self): 439 return len(self._plugins) 440 441 @classmethod 442 def list_installed_plugins(klass): 443 '''Lists plugin names for all installed plugins 444 @returns: a set of plugin names 445 ''' 446 # List "zim.plugins" sub modules based on __path__ because this 447 # parameter determines what folders will considered when importing 448 # sub-modules of the this package once this module is loaded. 449 plugins = set() # THIS LINE IS REPLACED BY SETUP.PY - DON'T CHANGE IT 450 for folder in [f for f in map(LocalFolder, __path__) if f.exists()]: 451 for child in folder: 452 name = child.basename 453 if name.startswith('_') or name == 'base': 454 continue 455 elif isinstance(child, LocalFile) and name.endswith('.py'): 456 plugins.add(name[:-3]) 457 elif isinstance(child, LocalFolder) \ 458 and child.file('__init__.py').exists(): 459 plugins.add(name) 460 else: 461 pass 462 463 return plugins 464 465 @classmethod 466 def get_plugin_class(klass, name): 467 '''Get the plugin class for a given name 468 469 @param name: the plugin module name 470 @returns: the plugin class object 471 ''' 472 modname = 'zim.plugins.' + name 473 mod = get_module(modname) 474 return lookup_subclass(mod, PluginClass) 475 476 def register_new_extendable(self, obj): 477 '''Register an extendable object 478 This is called automatically by the L{extendable()} class decorator 479 unless the option c{register_after_init} was set to C{False}. 480 Relies on C{obj} already being setup correctly by the L{extendable} decorator. 481 ''' 482 logger.debug("New extendable: %s", obj) 483 assert not obj in self._extendables() 484 485 count = 0 486 for name, plugin in sorted(self._plugins.items()): 487 # sort to make operation predictable 488 count += self._extend(plugin, obj) 489 490 if count > 0: 491 self.emit('extensions-changed', obj) 492 493 self._extendable_weakrefs.append(weakref.ref(obj)) 494 obj._zim_extendable_registered = True 495 496 def _extend(self, plugin, obj): 497 count = 0 498 for ext_class in plugin.extension_classes: 499 if issubclass(ext_class, obj.__zim_extension_bases__): 500 logger.debug("Load extension: %s", ext_class) 501 try: 502 ext = ext_class(plugin, obj) 503 except: 504 logger.exception('Failed loading extension %s for plugin %s', ext_class, plugin) 505 else: 506 plugin.extensions.add(ext) 507 count += 1 508 return count 509 510 def load_plugin(self, name): 511 '''Load a single plugin by name 512 513 When the plugin was loaded already the existing object 514 will be returned. Thus for each plugin only one instance can be 515 active. 516 517 @param name: the plugin module name 518 @returns: the plugin object 519 @raises Exception: when loading the plugin failed 520 ''' 521 assert isinstance(name, str) 522 if name in self._plugins: 523 return self._plugins[name] 524 525 logger.debug('Loading plugin: %s', name) 526 klass = self.get_plugin_class(name) 527 if not klass.check_dependencies_ok(): 528 raise AssertionError('Dependencies failed for plugin %s' % name) 529 530 plugin = klass() 531 self._plugins[name] = plugin 532 533 for obj in self._extendables(): 534 count = self._extend(plugin, obj) 535 if count > 0: 536 self.emit('extensions-changed', obj) 537 538 if not name in self._preferences['plugins']: 539 self._preferences['plugins'].append(name) 540 self._preferences.changed() 541 542 return plugin 543 544 def remove_plugin(self, name): 545 '''Remove a plugin and it's extensions 546 Fails silently if the plugin is not loaded. 547 @param name: the plugin module name 548 ''' 549 if name in self._preferences['plugins']: 550 # Do this first regardless of exceptions etc. 551 self._preferences['plugins'].remove(name) 552 self._preferences.changed() 553 554 try: 555 plugin = self._plugins.pop(name) 556 self.disconnect_from(plugin) 557 except KeyError: 558 pass 559 else: 560 logger.debug('Unloading plugin %s', name) 561 plugin.destroy() 562 563 564PluginManager = PluginManagerClass() # singleton 565for _extendable in _bootstrappluginmanager._extendables: 566 PluginManager.register_new_extendable(_extendable) 567del _bootstrappluginmanager 568del _extendable 569 570 571def resetPluginManager(): 572 # used in test suite to reset singleton internal state 573 PluginManager._reset() 574 575 576class PluginClass(ConnectorMixin): 577 '''Base class for plugins objects. 578 579 To be recognized as a plugin, a submodule of "zim.plugins" needs to 580 define one (and only one) sub-class of L{PluginClass}. This class 581 will define the main plugin object and contains meta data about the 582 plugin and e.g. plugin preferences. 583 584 The plugin object itself doesn't directly interact with the rest of the 585 zim application. To actually add functionality to zim, the plugin module 586 will also need to define one or more "extension" classes. These classes 587 act as decorators for specific objects that appear in the application. 588 589 All extension classes defined in the same module 590 file as the plugin object are automatically linked to the plugin. 591 592 This class inherits from L{ConnectorMixin} and calls 593 L{ConnectorMixin.disconnect_all()} when the plugin is destroyed. 594 Therefore it is highly recommended to use the L{ConnectorMixin} 595 methods in sub-classes. 596 597 Plugin classes should at minimum define two class attributes: 598 C{plugin_info} and C{plugin_preferences}. When these are defined 599 no other code is needed to have a basic plugin up and running. 600 601 @cvar plugin_info: A dict with basic information about the plugin, 602 it should contain at least the following keys: 603 604 - C{name}: short name 605 - C{description}: one paragraph description 606 - C{author}: name of the author 607 - C{help}: page name in the manual (optional) 608 609 This info will be used e.g. in the plugin tab of the preferences 610 dialog. 611 612 @cvar plugin_preferences: A tuple or list defining the global 613 preferences for this plugin (if any). Each preference is defined 614 by a 4-tuple containing the following items: 615 616 1. the dict key of the option (used in the config file and in 617 the preferences dict) 618 2. an option type (see L{InputForm.add_inputs(){} for more details) 619 3. a (translatable) label to show in the preferences dialog for 620 this option 621 4. a default value 622 623 These preferences will be initialized to their default value if not 624 configured by the user and the values can be found in the 625 L{preferences} dict of the plugin object. The type and label will be 626 used to render a default config dialog when triggered from the 627 preferences dialog. 628 Changes to these preferences will be stored in a config file so 629 they are persistent. 630 631 @ivar preferences: a L{ConfigDict} with plugin preferences 632 633 Preferences are the global configuration of the plugin, they are 634 stored in the X{preferences.conf} config file. 635 636 @ivar config: a L{ConfigManager} object that can be used to lookup 637 additional config files for the plugin 638 639 @ivar extension_classes: a list with extension classes found 640 in the plugin module 641 642 @ivar extensions: a set with extension objects loaded by this plugin. 643 ''' 644 645 # define signals we want to use - (closure type, return type and arg types) 646 plugin_info = {} 647 648 plugin_preferences = () 649 plugin_notebook_properties = () 650 651 @classproperty 652 def config_key(klass): 653 '''The name of section used in the config files to store the 654 preferences for this plugin. 655 ''' 656 return klass.__name__ 657 658 @classmethod 659 def check_dependencies_ok(klass): 660 '''Checks minimum dependencies are met 661 662 @returns: C{True} if this plugin can be loaded based on 663 L{check_dependencies()} 664 ''' 665 check, dependencies = klass.check_dependencies() 666 return check 667 668 @classmethod 669 def check_dependencies(klass): 670 '''Checks what dependencies are met and gives details for 671 display in the preferences dialog 672 673 @returns: a boolean telling overall dependencies are met, 674 followed by a list with details. 675 676 This list consists of 3-tuples consisting of a (short) 677 description of the dependency, a boolean for dependency being 678 met, and a boolean for this dependency being optional or not. 679 680 @implementation: must be implemented in sub-classes that have 681 one or more (external) dependencies. Default always returns 682 C{True} with an empty list. 683 ''' 684 return (True, []) 685 686 def __init__(self): 687 assert 'name' in self.plugin_info, 'Missing "name" in plugin_info' 688 assert 'description' in self.plugin_info, 'Missing "description" in plugin_info' 689 assert 'author' in self.plugin_info, 'Missing "author" in plugin_info' 690 self.extensions = weakref.WeakSet() 691 692 if self.plugin_preferences: 693 assert isinstance(self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tuples' 694 695 self.preferences = ConfigManager.preferences[self.config_key] 696 self._init_config(self.preferences, self.plugin_preferences) 697 self._init_config(self.preferences, self.plugin_notebook_properties) # defaults for the properties are preferences 698 699 self.extension_classes = list(self.discover_classes(ExtensionBase)) 700 701 @staticmethod 702 def _init_config(config, definitions): 703 for pref in definitions: 704 if len(pref) == 4: 705 key, type, label, default = pref 706 config.setdefault(key, default) 707 else: 708 key, type, label, default, check = pref 709 config.setdefault(key, default, check=check) 710 711 @staticmethod 712 def form_fields(definitions): 713 fields = [] 714 for pref in definitions: 715 if len(pref) == 4: 716 key, type, label, default = pref 717 else: 718 key, type, label, default, check = pref 719 720 if type in ('int', 'choice'): 721 fields.append((key, type, label, check)) 722 else: 723 fields.append((key, type, label)) 724 725 return fields 726 727 def notebook_properties(self, notebook): 728 properties = notebook.config[self.config_key] 729 if not properties: 730 self._init_config(properties, self.plugin_notebook_properties) 731 732 # update defaults based on preference 733 for key, definition in properties.definitions.items(): 734 try: 735 definition.default = definition.check(self.preferences[key]) 736 except ValueError: 737 pass 738 739 return properties 740 741 @classmethod 742 def lookup_subclass(pluginklass, klass): 743 '''Returns first subclass of C{klass} found in the module of 744 this plugin. (Similar to L{zim.utils.lookup_subclass}). 745 @param pluginklass: plugin class 746 @param klass: base class of the wanted class 747 ''' 748 module = get_module(pluginklass.__module__) 749 return lookup_subclass(module, klass) 750 751 @classmethod 752 def discover_classes(pluginklass, baseclass): 753 '''Yields a list of classes derived from C{baseclass} and 754 defined in the same module as the plugin 755 ''' 756 module = get_module(pluginklass.__module__) 757 for klass in lookup_subclasses(module, baseclass): 758 yield klass 759 760 def destroy(self): 761 '''Destroy the plugin object and all extensions 762 It is only called when a user actually disables the plugin, 763 not when the application exits. 764 765 Destroys all active extensions and disconnects all signals. 766 This should revert any changes the plugin made to the 767 application (although preferences etc. can be left in place). 768 ''' 769 for obj in list(self.extensions): 770 obj.destroy() 771 772 try: 773 self.disconnect_all() 774 self.teardown() 775 except: 776 logger.exception('Exception while disconnecting %s', self) 777 778 def teardown(self): 779 '''Cleanup method called by C{destroy()}. 780 Can be implemented by sub-classes. 781 ''' 782 pass 783