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