un-pogaz 8d28380515 add/remove blank-line (extra-edit)
ruff 'E302,E303,E304,E305,W391'
2025-01-24 11:14:21 +01:00

929 lines
28 KiB
Python

__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import functools
import os
import shutil
import sys
import traceback
from collections import defaultdict
from itertools import chain, repeat
from calibre.constants import DEBUG, ismacos, numeric_version, system_plugins_loc
from calibre.customize import (
CatalogPlugin,
EditBookToolPlugin,
FileTypePlugin,
InvalidPlugin,
LibraryClosedPlugin,
MetadataReaderPlugin,
MetadataWriterPlugin,
PluginInstallationType,
PluginNotFound,
PreferencesPlugin,
platform,
)
from calibre.customize import InterfaceActionBase as InterfaceAction
from calibre.customize import StoreBase as Store
from calibre.customize.builtins import plugins as builtin_plugins
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
from calibre.customize.profiles import InputProfile, OutputProfile
from calibre.customize.zipplugin import loader
from calibre.devices.interface import DevicePlugin
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.sources.base import Source
from calibre.utils.config import Config, ConfigProxy, OptionParser, make_config_dir, plugin_dir
from polyglot.builtins import iteritems, itervalues
builtin_names = frozenset(p.name for p in builtin_plugins)
BLACKLISTED_PLUGINS = frozenset({'Marvin XD', 'iOS reader applications'})
def zip_value(iterable, value):
return zip(iterable, repeat(value))
class NameConflict(ValueError):
pass
def _config():
c = Config('customize')
c.add_opt('plugins', default={}, help=_('Installed plugins'))
c.add_opt('filetype_mapping', default={}, help=_('Mapping for filetype plugins'))
c.add_opt('plugin_customization', default={}, help=_('Local plugin customization'))
c.add_opt('disabled_plugins', default=set(), help=_('Disabled plugins'))
c.add_opt('enabled_plugins', default=set(), help=_('Enabled plugins'))
return ConfigProxy(c)
config = _config()
def find_plugin(name):
for plugin in _initialized_plugins:
if plugin.name == name:
return plugin
def load_plugin(path_to_zip_file): # {{{
'''
Load plugin from ZIP file or raise InvalidPlugin error
:return: A :class:`Plugin` instance.
'''
return loader.load(path_to_zip_file)
# }}}
# Enable/disable plugins {{{
def disable_plugin(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
plugin = find_plugin(x)
if plugin is None:
raise ValueError(f'No plugin named: {x} found')
if not plugin.can_be_disabled:
raise ValueError('Plugin %s cannot be disabled'%x)
dp = config['disabled_plugins']
dp.add(x)
config['disabled_plugins'] = dp
ep = config['enabled_plugins']
if x in ep:
ep.remove(x)
config['enabled_plugins'] = ep
def enable_plugin(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
dp = config['disabled_plugins']
if x in dp:
dp.remove(x)
config['disabled_plugins'] = dp
ep = config['enabled_plugins']
ep.add(x)
config['enabled_plugins'] = ep
def restore_plugin_state_to_default(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
dp = config['disabled_plugins']
if x in dp:
dp.remove(x)
config['disabled_plugins'] = dp
ep = config['enabled_plugins']
if x in ep:
ep.remove(x)
config['enabled_plugins'] = ep
default_disabled_plugins = {
'Overdrive', 'Douban Books', 'OZON.ru', 'Edelweiss', 'Google Images', 'Big Book Search',
}
def is_disabled(plugin):
if plugin.name in config['enabled_plugins']:
return False
return plugin.name in config['disabled_plugins'] or \
plugin.name in default_disabled_plugins
# }}}
# File type plugins {{{
_on_import = {}
_on_postimport = {}
_on_postconvert = {}
_on_postdelete = {}
_on_preprocess = {}
_on_postprocess = {}
_on_postadd = []
def reread_filetype_plugins():
global _on_import, _on_postimport, _on_postconvert, _on_postdelete, _on_preprocess, _on_postprocess, _on_postadd
_on_import = defaultdict(list)
_on_postimport = defaultdict(list)
_on_postconvert = defaultdict(list)
_on_postdelete = defaultdict(list)
_on_preprocess = defaultdict(list)
_on_postprocess = defaultdict(list)
_on_postadd = []
for plugin in _initialized_plugins:
if isinstance(plugin, FileTypePlugin):
if ismacos and plugin.name == 'DeDRM' and plugin.version < (10, 0, 3):
print(f'Blacklisting the {plugin.name} plugin as it is too old and causes crashes', file=sys.stderr)
continue
for ft in plugin.file_types:
if plugin.on_import:
_on_import[ft].append(plugin)
if plugin.on_postimport:
_on_postimport[ft].append(plugin)
_on_postadd.append(plugin)
if plugin.on_postconvert:
_on_postconvert[ft].append(plugin)
if plugin.on_postdelete:
_on_postdelete[ft].append(plugin)
if plugin.on_preprocess:
_on_preprocess[ft].append(plugin)
if plugin.on_postprocess:
_on_postprocess[ft].append(plugin)
def plugins_for_ft(ft, occasion):
op = {
'import':_on_import, 'preprocess':_on_preprocess, 'postprocess':_on_postprocess, 'postimport':_on_postimport,
'postconvert':_on_postconvert, 'postdelete':_on_postdelete,
}[occasion]
for p in chain(op.get(ft, ()), op.get('*', ())):
if not is_disabled(p):
yield p
def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'):
customization = config['plugin_customization']
if ft is None:
ft = os.path.splitext(path_to_file)[-1].lower().replace('.', '')
nfp = path_to_file
for plugin in plugins_for_ft(ft, occasion):
plugin.site_customization = customization.get(plugin.name, '')
oo, oe = sys.stdout, sys.stderr # Some file type plugins out there override the output streams with buggy implementations
with plugin:
try:
plugin.original_path_to_file = path_to_file
except Exception:
pass
try:
nfp = plugin.run(nfp) or nfp
except:
print('Running file type plugin %s failed with traceback:'%plugin.name, file=oe)
traceback.print_exc(file=oe)
sys.stdout, sys.stderr = oo, oe
def x(j):
return os.path.normpath(os.path.normcase(j))
if occasion == 'postprocess' and x(nfp) != x(path_to_file):
shutil.copyfile(nfp, path_to_file)
nfp = path_to_file
return nfp
run_plugins_on_import = functools.partial(_run_filetype_plugins, occasion='import')
run_plugins_on_preprocess = functools.partial(_run_filetype_plugins, occasion='preprocess')
run_plugins_on_postprocess = functools.partial(_run_filetype_plugins, occasion='postprocess')
def run_plugins_on_postimport(db, book_id, fmt):
customization = config['plugin_customization']
fmt = fmt.lower()
for plugin in plugins_for_ft(fmt, 'postimport'):
plugin.site_customization = customization.get(plugin.name, '')
with plugin:
try:
plugin.postimport(book_id, fmt, db)
except Exception:
print(f'Running file type plugin {plugin.name} failed with traceback:', file=sys.stderr)
traceback.print_exc()
def run_plugins_on_postconvert(db, book_id, fmt):
customization = config['plugin_customization']
fmt = fmt.lower()
for plugin in plugins_for_ft(fmt, 'postconvert'):
plugin.site_customization = customization.get(plugin.name, '')
with plugin:
try:
plugin.postconvert(book_id, fmt, db)
except Exception:
print(f'Running file type plugin {plugin.name} failed with traceback:', file=sys.stderr)
traceback.print_exc()
def run_plugins_on_postdelete(db, book_id, fmt):
customization = config['plugin_customization']
fmt = fmt.lower()
for plugin in plugins_for_ft(fmt, 'postdelete'):
plugin.site_customization = customization.get(plugin.name, '')
with plugin:
try:
plugin.postdelete(book_id, fmt, db)
except Exception:
print(f'Running file type plugin {plugin.name} failed with traceback:', file=sys.stderr)
traceback.print_exc()
def run_plugins_on_postadd(db, book_id, fmt_map):
customization = config['plugin_customization']
for plugin in _on_postadd:
if is_disabled(plugin):
continue
plugin.site_customization = customization.get(plugin.name, '')
with plugin:
try:
plugin.postadd(book_id, fmt_map, db)
except Exception:
print(f'Running file type plugin {plugin.name} failed with traceback:', file=sys.stderr)
traceback.print_exc()
# }}}
# Plugin customization {{{
def customize_plugin(plugin, custom):
d = config['plugin_customization']
d[plugin.name] = custom.strip()
config['plugin_customization'] = d
def plugin_customization(plugin):
return config['plugin_customization'].get(plugin.name, '')
# }}}
# Input/Output profiles {{{
def input_profiles():
for plugin in _initialized_plugins:
if isinstance(plugin, InputProfile):
yield plugin
def output_profiles():
for plugin in _initialized_plugins:
if isinstance(plugin, OutputProfile):
yield plugin
# }}}
# Interface Actions # {{{
def interface_actions():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, InterfaceAction):
if not is_disabled(plugin):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
# }}}
# Preferences Plugins # {{{
def preferences_plugins():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, PreferencesPlugin):
if not is_disabled(plugin):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
# }}}
# Library Closed Plugins # {{{
def available_library_closed_plugins():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, LibraryClosedPlugin):
if not is_disabled(plugin):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
def has_library_closed_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, LibraryClosedPlugin):
if not is_disabled(plugin):
return True
return False
# }}}
# Store Plugins # {{{
def store_plugins():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, Store):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
def available_store_plugins():
for plugin in store_plugins():
if not is_disabled(plugin):
yield plugin
def stores():
stores = set()
for plugin in store_plugins():
stores.add(plugin.name)
return stores
def available_stores():
stores = set()
for plugin in available_store_plugins():
stores.add(plugin.name)
return stores
# }}}
# Metadata read/write {{{
_metadata_readers = {}
_metadata_writers = {}
def reread_metadata_plugins():
global _metadata_readers
global _metadata_writers
_metadata_readers = defaultdict(list)
_metadata_writers = defaultdict(list)
for plugin in _initialized_plugins:
if isinstance(plugin, MetadataReaderPlugin):
for ft in plugin.file_types:
_metadata_readers[ft].append(plugin)
elif isinstance(plugin, MetadataWriterPlugin):
for ft in plugin.file_types:
_metadata_writers[ft].append(plugin)
# Ensure the following metadata plugin preference is used:
# external > system > builtin
def key(plugin):
order = sys.maxsize if plugin.installation_type is None else plugin.installation_type
return order, plugin.name
for group in (_metadata_readers, _metadata_writers):
for plugins in itervalues(group):
if len(plugins) > 1:
plugins.sort(key=key)
def metadata_readers():
ans = set()
for plugins in _metadata_readers.values():
for plugin in plugins:
ans.add(plugin)
return ans
def metadata_writers():
ans = set()
for plugins in _metadata_writers.values():
for plugin in plugins:
ans.add(plugin)
return ans
class QuickMetadata:
def __init__(self):
self.quick = False
def __enter__(self):
self.quick = True
def __exit__(self, *args):
self.quick = False
quick_metadata = QuickMetadata()
class ApplyNullMetadata:
def __init__(self):
self.apply_null = False
def __enter__(self):
self.apply_null = True
def __exit__(self, *args):
self.apply_null = False
apply_null_metadata = ApplyNullMetadata()
class ForceIdentifiers:
def __init__(self):
self.force_identifiers = False
def __enter__(self):
self.force_identifiers = True
def __exit__(self, *args):
self.force_identifiers = False
force_identifiers = ForceIdentifiers()
def get_file_type_metadata(stream, ftype):
mi = MetaInformation(None, None)
ftype = ftype.lower().strip()
if ftype in _metadata_readers:
for plugin in _metadata_readers[ftype]:
if not is_disabled(plugin):
with plugin:
try:
plugin.quick = quick_metadata.quick
if hasattr(stream, 'seek'):
stream.seek(0)
mi = plugin.get_metadata(stream, ftype.lower().strip())
break
except:
traceback.print_exc()
continue
return mi
def set_file_type_metadata(stream, mi, ftype, report_error=None):
ftype = ftype.lower().strip()
if ftype in _metadata_writers:
customization = config['plugin_customization']
for plugin in _metadata_writers[ftype]:
if not is_disabled(plugin):
with plugin:
try:
plugin.apply_null = apply_null_metadata.apply_null
plugin.force_identifiers = force_identifiers.force_identifiers
plugin.site_customization = customization.get(plugin.name, '')
plugin.set_metadata(stream, mi, ftype.lower().strip())
break
except:
if report_error is None:
from calibre import prints
prints('Failed to set metadata for the', ftype.upper(), 'format of:', getattr(mi, 'title', ''), file=sys.stderr)
traceback.print_exc()
else:
report_error(mi, ftype, traceback.format_exc())
def can_set_metadata(ftype):
ftype = ftype.lower().strip()
for plugin in _metadata_writers.get(ftype, ()):
if not is_disabled(plugin):
return True
return False
# }}}
# Add/remove plugins {{{
def add_plugin(path_to_zip_file):
make_config_dir()
plugin = load_plugin(path_to_zip_file)
if plugin.name in builtin_names:
raise NameConflict(
'A builtin plugin with the name %r already exists' % plugin.name)
if plugin.name in get_system_plugins():
raise NameConflict(
'A system plugin with the name %r already exists' % plugin.name)
plugin = initialize_plugin(plugin, path_to_zip_file, PluginInstallationType.EXTERNAL)
plugins = config['plugins']
zfp = os.path.join(plugin_dir, plugin.name+'.zip')
if os.path.exists(zfp):
os.remove(zfp)
shutil.copyfile(path_to_zip_file, zfp)
plugins[plugin.name] = zfp
config['plugins'] = plugins
initialize_plugins()
return plugin
def remove_plugin(plugin_or_name):
name = getattr(plugin_or_name, 'name', plugin_or_name)
plugins = config['plugins']
removed = False
if name in plugins:
removed = True
try:
zfp = os.path.join(plugin_dir, name+'.zip')
if os.path.exists(zfp):
os.remove(zfp)
zfp = plugins[name]
if os.path.exists(zfp):
os.remove(zfp)
except:
pass
plugins.pop(name)
config['plugins'] = plugins
initialize_plugins()
return removed
# }}}
# Input/Output format plugins {{{
def input_format_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, InputFormatPlugin):
yield plugin
def plugin_for_input_format(fmt):
customization = config['plugin_customization']
for plugin in input_format_plugins():
if fmt.lower() in plugin.file_types:
plugin.site_customization = customization.get(plugin.name, None)
return plugin
def all_input_formats():
formats = set()
for plugin in input_format_plugins():
for format in plugin.file_types:
formats.add(format)
return formats
def available_input_formats():
formats = set()
for plugin in input_format_plugins():
if not is_disabled(plugin):
for format in plugin.file_types:
formats.add(format)
formats.add('zip'), formats.add('rar')
return formats
def output_format_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, OutputFormatPlugin):
yield plugin
def plugin_for_output_format(fmt):
customization = config['plugin_customization']
for plugin in output_format_plugins():
if fmt.lower() == plugin.file_type:
plugin.site_customization = customization.get(plugin.name, None)
return plugin
def available_output_formats():
formats = set()
for plugin in output_format_plugins():
if not is_disabled(plugin):
formats.add(plugin.file_type)
return formats
# }}}
# Catalog plugins {{{
def catalog_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, CatalogPlugin):
yield plugin
def available_catalog_formats():
formats = set()
for plugin in catalog_plugins():
if not is_disabled(plugin):
for format in plugin.file_types:
formats.add(format)
return formats
def plugin_for_catalog_format(fmt):
for plugin in catalog_plugins():
if fmt.lower() in plugin.file_types:
return plugin
# }}}
# Device plugins {{{
def device_plugins(include_disabled=False):
for plugin in _initialized_plugins:
if isinstance(plugin, DevicePlugin):
if include_disabled or not is_disabled(plugin):
if platform in plugin.supported_platforms:
if getattr(plugin, 'plugin_needs_delayed_initialization',
False):
plugin.do_delayed_plugin_initialization()
yield plugin
def disabled_device_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, DevicePlugin):
if is_disabled(plugin):
if platform in plugin.supported_platforms:
yield plugin
# }}}
# Metadata sources2 {{{
def metadata_plugins(capabilities):
capabilities = frozenset(capabilities)
for plugin in all_metadata_plugins():
if plugin.capabilities.intersection(capabilities) and \
not is_disabled(plugin):
yield plugin
def all_metadata_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, Source):
yield plugin
def patch_metadata_plugins(possibly_updated_plugins):
patches = {}
for i, plugin in enumerate(_initialized_plugins):
if isinstance(plugin, Source) and plugin.name in builtin_names:
pup = possibly_updated_plugins.get(plugin.name)
if pup is not None:
if pup.version > plugin.version and pup.minimum_calibre_version <= numeric_version:
patches[i] = pup(None)
# Metadata source plugins dont use initialize() but that
# might change in the future, so be safe.
patches[i].initialize()
for i, pup in iteritems(patches):
_initialized_plugins[i] = pup
# }}}
# Editor plugins {{{
def all_edit_book_tool_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, EditBookToolPlugin):
yield plugin
# }}}
# Initialize plugins {{{
_initialized_plugins = []
def initialize_plugin(plugin, path_to_zip_file, installation_type):
try:
p = plugin(path_to_zip_file)
p.installation_type = installation_type
p.initialize()
return p
except Exception:
print('Failed to initialize plugin:', plugin.name, plugin.version)
tb = traceback.format_exc()
raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:')
%tb) + '\n'+tb)
def has_external_plugins():
'True if there are updateable (ZIP file based) plugins'
return bool(config['plugins'])
@functools.lru_cache(maxsize=2)
def get_system_plugins():
if not system_plugins_loc:
return {}
try:
plugin_file_names = os.listdir(system_plugins_loc)
except OSError:
return {}
ans = []
for plugin_file_name in plugin_file_names:
plugin_path = os.path.join(system_plugins_loc, plugin_file_name)
if os.path.isfile(plugin_path) and plugin_file_name.endswith('.zip'):
ans.append((os.path.splitext(plugin_file_name)[0], plugin_path))
return dict(ans)
def initialize_plugins(perf=False):
global _initialized_plugins
_initialized_plugins = []
system_plugins = get_system_plugins().copy()
conflicts = {name for name in config['plugins'] if name in
builtin_names or name in system_plugins}
for p in conflicts:
remove_plugin(p)
system_conflicts = [name for name in system_plugins if name in
builtin_names]
for p in system_conflicts:
system_plugins.pop(p, None)
external_plugins = config['plugins'].copy()
for name in BLACKLISTED_PLUGINS:
external_plugins.pop(name, None)
system_plugins.pop(name, None)
ostdout, ostderr = sys.stdout, sys.stderr
if perf:
import time
from collections import defaultdict
times = defaultdict(int)
for zfp, installation_type in chain(
zip_value(external_plugins.items(), PluginInstallationType.EXTERNAL),
zip_value(system_plugins.items(), PluginInstallationType.SYSTEM),
zip_value(builtin_plugins, PluginInstallationType.BUILTIN),
):
try:
if not isinstance(zfp, type):
# We have a plugin name
pname, path = zfp
zfp = os.path.join(plugin_dir, pname+'.zip')
if not os.path.exists(zfp):
zfp = path
try:
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
except PluginNotFound:
continue
if perf:
st = time.time()
plugin = initialize_plugin(
plugin,
None if isinstance(zfp, type) else zfp, installation_type,
)
if perf:
times[plugin.name] = time.time() - st
_initialized_plugins.append(plugin)
except:
print('Failed to initialize plugin:', repr(zfp), file=sys.stderr)
if DEBUG:
traceback.print_exc()
# Prevent a custom plugin from overriding stdout/stderr as this breaks
# ipython
sys.stdout, sys.stderr = ostdout, ostderr
if perf:
for x in sorted(times, key=lambda x: times[x]):
print('%50s: %.3f'%(x, times[x]))
_initialized_plugins.sort(key=lambda x: x.priority, reverse=True)
reread_filetype_plugins()
reread_metadata_plugins()
initialize_plugins()
def initialized_plugins():
yield from _initialized_plugins
# }}}
# CLI {{{
def build_plugin(path):
from calibre import prints
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.zipfile import ZIP_STORED, ZipFile
path = str(path)
names = frozenset(os.listdir(path))
if '__init__.py' not in names:
prints(path, ' is not a valid plugin')
raise SystemExit(1)
t = PersistentTemporaryFile('.zip')
with ZipFile(t, 'w', ZIP_STORED) as zf:
zf.add_dir(path, simple_filter=lambda x:x in {'.git', '.bzr', '.svn', '.hg'})
t.close()
plugin = add_plugin(t.name)
os.remove(t.name)
prints('Plugin updated:', plugin.name, plugin.version)
def option_parser():
parser = OptionParser(usage=_('''\
%prog options
Customize calibre by loading external plugins.
'''))
parser.add_option('-a', '--add-plugin', default=None,
help=_('Add a plugin by specifying the path to the ZIP file containing it.'))
parser.add_option('-b', '--build-plugin', default=None,
help=_('For plugin developers: Path to the folder where you are'
' developing the plugin. This command will automatically zip '
'up the plugin and update it in calibre.'))
parser.add_option('-r', '--remove-plugin', default=None,
help=_('Remove a custom plugin by name. Has no effect on builtin plugins'))
parser.add_option('--customize-plugin', default=None,
help=_('Customize plugin. Specify name of plugin and customization string separated by a comma.'
' The customization string is the same as you would enter when customizing the plugin in the main calibre GUI.'))
parser.add_option('-l', '--list-plugins', default=False, action='store_true',
help=_('List all installed plugins'))
parser.add_option('--enable-plugin', default=None,
help=_('Enable the named plugin'))
parser.add_option('--disable-plugin', default=None,
help=_('Disable the named plugin'))
return parser
def main(args=sys.argv):
parser = option_parser()
if len(args) < 2:
parser.print_help()
return 1
opts, args = parser.parse_args(args)
if opts.add_plugin is not None:
plugin = add_plugin(opts.add_plugin)
print('Plugin added:', plugin.name, plugin.version)
if opts.build_plugin is not None:
build_plugin(opts.build_plugin)
if opts.remove_plugin is not None:
if remove_plugin(opts.remove_plugin):
print('Plugin removed')
else:
print('No custom plugin named', opts.remove_plugin)
if opts.customize_plugin is not None:
try:
name, custom = opts.customize_plugin.split(',')
except ValueError:
name, custom = opts.customize_plugin, ''
plugin = find_plugin(name.strip())
if plugin is None:
print('No plugin with the name %s exists'%name)
return 1
customize_plugin(plugin, custom)
if opts.enable_plugin is not None:
enable_plugin(opts.enable_plugin.strip())
if opts.disable_plugin is not None:
disable_plugin(opts.disable_plugin.strip())
if opts.list_plugins:
type_len = name_len = 0
for plugin in initialized_plugins():
type_len, name_len = max(type_len, len(plugin.type)), max(name_len, len(plugin.name))
fmt = f'%-{type_len+1}s%-{name_len+1}s%-15s%-15s%s'
print(fmt%tuple('Type|Name|Version|Disabled|Site Customization'.split('|')))
print()
for plugin in initialized_plugins():
print(fmt%(
plugin.type, plugin.name,
plugin.version, is_disabled(plugin),
plugin_customization(plugin)
))
print('\t', plugin.description)
if plugin.is_customizable():
try:
print('\t', plugin.customization_help())
except NotImplementedError:
pass
print()
return 0
if __name__ == '__main__':
sys.exit(main())
# }}}