mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
load plugins from system-wide directory
https://www.mobileread.com/forums/showthread.php?t=333893
This commit is contained in:
parent
03ef47c8a5
commit
3981f79a34
@ -166,6 +166,8 @@ def cache_dir():
|
|||||||
|
|
||||||
# plugins {{{
|
# plugins {{{
|
||||||
plugins_loc = sys.extensions_location
|
plugins_loc = sys.extensions_location
|
||||||
|
system_plugins_loc = getattr(sys, 'system_plugins_location', None)
|
||||||
|
|
||||||
from importlib.machinery import ModuleSpec, EXTENSION_SUFFIXES, ExtensionFileLoader
|
from importlib.machinery import ModuleSpec, EXTENSION_SUFFIXES, ExtensionFileLoader
|
||||||
from importlib.util import find_spec
|
from importlib.util import find_spec
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import os, sys, zipfile, importlib
|
import os, sys, zipfile, importlib, enum
|
||||||
|
|
||||||
from calibre.constants import numeric_version, iswindows, ismacos
|
from calibre.constants import numeric_version, iswindows, ismacos
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
@ -24,10 +24,17 @@ class InvalidPlugin(ValueError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInstallationType(enum.Enum):
|
||||||
|
BUILTIN = enum.auto()
|
||||||
|
SYSTEM = enum.auto()
|
||||||
|
EXTERNAL = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
class Plugin(object): # {{{
|
class Plugin(object): # {{{
|
||||||
'''
|
'''
|
||||||
A calibre plugin. Useful members include:
|
A calibre plugin. Useful members include:
|
||||||
|
|
||||||
|
* ``self.installation_type``: Stores how the plugin was installed.
|
||||||
* ``self.plugin_path``: Stores path to the ZIP file that contains
|
* ``self.plugin_path``: Stores path to the ZIP file that contains
|
||||||
this plugin or None if it is a builtin
|
this plugin or None if it is a builtin
|
||||||
plugin
|
plugin
|
||||||
@ -73,6 +80,9 @@ class Plugin(object): # {{{
|
|||||||
#: The earliest version of calibre this plugin requires
|
#: The earliest version of calibre this plugin requires
|
||||||
minimum_calibre_version = (0, 4, 118)
|
minimum_calibre_version = (0, 4, 118)
|
||||||
|
|
||||||
|
#: The way this plugin is installed
|
||||||
|
installation_type = None
|
||||||
|
|
||||||
#: If False, the user will not be able to disable this plugin. Use with
|
#: If False, the user will not be able to disable this plugin. Use with
|
||||||
#: care.
|
#: care.
|
||||||
can_be_disabled = True
|
can_be_disabled = True
|
||||||
|
@ -4,14 +4,14 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import os, shutil, traceback, functools, sys
|
import os, shutil, traceback, functools, sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from itertools import chain
|
from itertools import chain, repeat
|
||||||
|
|
||||||
from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
|
from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
|
||||||
MetadataReaderPlugin, MetadataWriterPlugin,
|
MetadataReaderPlugin, MetadataWriterPlugin,
|
||||||
InterfaceActionBase as InterfaceAction,
|
InterfaceActionBase as InterfaceAction,
|
||||||
PreferencesPlugin, platform, InvalidPlugin,
|
PreferencesPlugin, platform, InvalidPlugin,
|
||||||
StoreBase as Store, EditBookToolPlugin,
|
StoreBase as Store, EditBookToolPlugin,
|
||||||
LibraryClosedPlugin)
|
LibraryClosedPlugin, PluginInstallationType)
|
||||||
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
||||||
from calibre.customize.zipplugin import loader
|
from calibre.customize.zipplugin import loader
|
||||||
from calibre.customize.profiles import InputProfile, OutputProfile
|
from calibre.customize.profiles import InputProfile, OutputProfile
|
||||||
@ -21,13 +21,17 @@ from calibre.ebooks.metadata import MetaInformation
|
|||||||
from calibre.utils.config import (make_config_dir, Config, ConfigProxy,
|
from calibre.utils.config import (make_config_dir, Config, ConfigProxy,
|
||||||
plugin_dir, OptionParser)
|
plugin_dir, OptionParser)
|
||||||
from calibre.ebooks.metadata.sources.base import Source
|
from calibre.ebooks.metadata.sources.base import Source
|
||||||
from calibre.constants import DEBUG, numeric_version
|
from calibre.constants import DEBUG, numeric_version, system_plugins_loc
|
||||||
from polyglot.builtins import iteritems, itervalues, unicode_type
|
from polyglot.builtins import iteritems, itervalues, unicode_type
|
||||||
|
|
||||||
builtin_names = frozenset(p.name for p in builtin_plugins)
|
builtin_names = frozenset(p.name for p in builtin_plugins)
|
||||||
BLACKLISTED_PLUGINS = frozenset({'Marvin XD', 'iOS reader applications'})
|
BLACKLISTED_PLUGINS = frozenset({'Marvin XD', 'iOS reader applications'})
|
||||||
|
|
||||||
|
|
||||||
|
def zip_value(iterable, value):
|
||||||
|
return zip(iterable, repeat(value))
|
||||||
|
|
||||||
|
|
||||||
class NameConflict(ValueError):
|
class NameConflict(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -341,10 +345,19 @@ def reread_metadata_plugins():
|
|||||||
for ft in plugin.file_types:
|
for ft in plugin.file_types:
|
||||||
_metadata_writers[ft].append(plugin)
|
_metadata_writers[ft].append(plugin)
|
||||||
|
|
||||||
# Ensure custom metadata plugins are used in preference to builtin
|
# Ensure the following metadata plugin preference is used:
|
||||||
# ones for a given filetype
|
# external > system > builtin
|
||||||
def key(plugin):
|
def key(plugin):
|
||||||
return (1 if plugin.plugin_path is None else 0), plugin.name
|
installation_type = plugin.installation_type
|
||||||
|
if installation_type is PluginInstallationType.BUILTIN:
|
||||||
|
order = 2
|
||||||
|
elif installation_type is PluginInstallationType.SYSTEM:
|
||||||
|
order = 1
|
||||||
|
elif installation_type is PluginInstallationType.EXTERNAL:
|
||||||
|
order = 0
|
||||||
|
else:
|
||||||
|
raise ValueError(installation_type)
|
||||||
|
return order, plugin.name
|
||||||
|
|
||||||
for group in (_metadata_readers, _metadata_writers):
|
for group in (_metadata_readers, _metadata_writers):
|
||||||
for plugins in itervalues(group):
|
for plugins in itervalues(group):
|
||||||
@ -473,7 +486,10 @@ def add_plugin(path_to_zip_file):
|
|||||||
if plugin.name in builtin_names:
|
if plugin.name in builtin_names:
|
||||||
raise NameConflict(
|
raise NameConflict(
|
||||||
'A builtin plugin with the name %r already exists' % plugin.name)
|
'A builtin plugin with the name %r already exists' % plugin.name)
|
||||||
plugin = initialize_plugin(plugin, path_to_zip_file)
|
if plugin.name in _list_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']
|
plugins = config['plugins']
|
||||||
zfp = os.path.join(plugin_dir, plugin.name+'.zip')
|
zfp = os.path.join(plugin_dir, plugin.name+'.zip')
|
||||||
if os.path.exists(zfp):
|
if os.path.exists(zfp):
|
||||||
@ -659,9 +675,10 @@ def all_edit_book_tool_plugins():
|
|||||||
_initialized_plugins = []
|
_initialized_plugins = []
|
||||||
|
|
||||||
|
|
||||||
def initialize_plugin(plugin, path_to_zip_file):
|
def initialize_plugin(plugin, path_to_zip_file, installation_type):
|
||||||
try:
|
try:
|
||||||
p = plugin(path_to_zip_file)
|
p = plugin(path_to_zip_file)
|
||||||
|
p.installation_type = installation_type
|
||||||
p.initialize()
|
p.initialize()
|
||||||
return p
|
return p
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -676,36 +693,67 @@ def has_external_plugins():
|
|||||||
return bool(config['plugins'])
|
return bool(config['plugins'])
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=None)
|
||||||
|
def _list_system_plugins():
|
||||||
|
if not system_plugins_loc:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
plugin_file_names = os.listdir(system_plugins_loc)
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
|
||||||
|
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'):
|
||||||
|
yield (os.path.splitext(plugin_file_name)[0], plugin_path)
|
||||||
|
|
||||||
|
|
||||||
def initialize_plugins(perf=False):
|
def initialize_plugins(perf=False):
|
||||||
global _initialized_plugins
|
global _initialized_plugins, _system_plugins
|
||||||
_initialized_plugins = []
|
_initialized_plugins = []
|
||||||
|
_system_plugins = []
|
||||||
|
system_plugins = dict(_list_system_plugins())
|
||||||
conflicts = [name for name in config['plugins'] if name in
|
conflicts = [name for name in config['plugins'] if name in
|
||||||
builtin_names]
|
builtin_names or name in system_plugins]
|
||||||
for p in conflicts:
|
for p in conflicts:
|
||||||
remove_plugin(p)
|
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()
|
external_plugins = config['plugins'].copy()
|
||||||
for name in BLACKLISTED_PLUGINS:
|
for name in BLACKLISTED_PLUGINS:
|
||||||
external_plugins.pop(name, None)
|
external_plugins.pop(name, None)
|
||||||
|
system_plugins.pop(name, None)
|
||||||
ostdout, ostderr = sys.stdout, sys.stderr
|
ostdout, ostderr = sys.stdout, sys.stderr
|
||||||
if perf:
|
if perf:
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import time
|
import time
|
||||||
times = defaultdict(lambda:0)
|
times = defaultdict(lambda:0)
|
||||||
for zfp in list(external_plugins) + builtin_plugins:
|
|
||||||
|
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:
|
try:
|
||||||
if not isinstance(zfp, type):
|
if not isinstance(zfp, type):
|
||||||
# We have a plugin name
|
# We have a plugin name
|
||||||
pname = zfp
|
pname, path = zfp
|
||||||
zfp = os.path.join(plugin_dir, zfp+'.zip')
|
zfp = os.path.join(plugin_dir, pname+'.zip')
|
||||||
if not os.path.exists(zfp):
|
if not os.path.exists(zfp):
|
||||||
zfp = external_plugins[pname]
|
zfp = path
|
||||||
try:
|
try:
|
||||||
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
|
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
|
||||||
except PluginNotFound:
|
except PluginNotFound:
|
||||||
continue
|
continue
|
||||||
if perf:
|
if perf:
|
||||||
st = time.time()
|
st = time.time()
|
||||||
plugin = initialize_plugin(plugin, None if isinstance(zfp, type) else zfp)
|
plugin = initialize_plugin(
|
||||||
|
plugin,
|
||||||
|
None if isinstance(zfp, type) else zfp, installation_type,
|
||||||
|
)
|
||||||
if perf:
|
if perf:
|
||||||
times[plugin.name] = time.time() - st
|
times[plugin.name] = time.time() - st
|
||||||
_initialized_plugins.append(plugin)
|
_initialized_plugins.append(plugin)
|
||||||
|
@ -20,6 +20,7 @@ from calibre import prints
|
|||||||
from calibre.constants import (
|
from calibre.constants import (
|
||||||
DEBUG, __appname__, __version__, ismacos, iswindows, numeric_version
|
DEBUG, __appname__, __version__, ismacos, iswindows, numeric_version
|
||||||
)
|
)
|
||||||
|
from calibre.customize import PluginInstallationType
|
||||||
from calibre.customize.ui import (
|
from calibre.customize.ui import (
|
||||||
NameConflict, add_plugin, disable_plugin, enable_plugin, has_external_plugins,
|
NameConflict, add_plugin, disable_plugin, enable_plugin, has_external_plugins,
|
||||||
initialized_plugins, is_disabled, remove_plugin
|
initialized_plugins, is_disabled, remove_plugin
|
||||||
@ -56,7 +57,8 @@ def get_plugin_updates_available(raise_error=False):
|
|||||||
|
|
||||||
|
|
||||||
def filter_upgradeable_plugins(display_plugin):
|
def filter_upgradeable_plugins(display_plugin):
|
||||||
return display_plugin.is_upgrade_available()
|
return display_plugin.installation_type is PluginInstallationType.EXTERNAL \
|
||||||
|
and display_plugin.is_upgrade_available()
|
||||||
|
|
||||||
|
|
||||||
def filter_not_installed_plugins(display_plugin):
|
def filter_not_installed_plugins(display_plugin):
|
||||||
@ -96,7 +98,8 @@ def get_installed_plugin_status(display_plugin):
|
|||||||
display_plugin.installed_version = None
|
display_plugin.installed_version = None
|
||||||
display_plugin.plugin = None
|
display_plugin.plugin = None
|
||||||
for plugin in initialized_plugins():
|
for plugin in initialized_plugins():
|
||||||
if plugin.name == display_plugin.qname and plugin.plugin_path is not None:
|
if plugin.name == display_plugin.qname \
|
||||||
|
and plugin.installation_type is not PluginInstallationType.BUILTIN:
|
||||||
display_plugin.plugin = plugin
|
display_plugin.plugin = plugin
|
||||||
display_plugin.installed_version = plugin.version
|
display_plugin.installed_version = plugin.version
|
||||||
break
|
break
|
||||||
@ -195,6 +198,7 @@ class DisplayPlugin(object):
|
|||||||
self.uninstall_plugins = plugin['uninstall'] or []
|
self.uninstall_plugins = plugin['uninstall'] or []
|
||||||
self.has_changelog = plugin['history']
|
self.has_changelog = plugin['history']
|
||||||
self.is_deprecated = plugin['deprecated']
|
self.is_deprecated = plugin['deprecated']
|
||||||
|
self.installation_type = plugin['installation_type']
|
||||||
|
|
||||||
def is_disabled(self):
|
def is_disabled(self):
|
||||||
if self.plugin is None:
|
if self.plugin is None:
|
||||||
|
@ -14,6 +14,7 @@ from qt.core import (Qt, QModelIndex, QAbstractItemModel, QIcon,
|
|||||||
|
|
||||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||||
from calibre.gui2.preferences.plugins_ui import Ui_Form
|
from calibre.gui2.preferences.plugins_ui import Ui_Form
|
||||||
|
from calibre.customize import PluginInstallationType
|
||||||
from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin,
|
from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin,
|
||||||
disable_plugin, plugin_customization, add_plugin,
|
disable_plugin, plugin_customization, add_plugin,
|
||||||
remove_plugin, NameConflict)
|
remove_plugin, NameConflict)
|
||||||
@ -211,6 +212,8 @@ class PluginModel(QAbstractItemModel, AdaptSQP): # {{{
|
|||||||
ans += _('\nCustomization: ')+c
|
ans += _('\nCustomization: ')+c
|
||||||
if disabled:
|
if disabled:
|
||||||
ans += _('\n\nThis plugin has been disabled')
|
ans += _('\n\nThis plugin has been disabled')
|
||||||
|
if plugin.installation_type is PluginInstallationType.SYSTEM:
|
||||||
|
ans += _('\n\nThis plugin is installed system-wide and can not be managed from within calibre')
|
||||||
return (ans)
|
return (ans)
|
||||||
if role == Qt.ItemDataRole.DecorationRole:
|
if role == Qt.ItemDataRole.DecorationRole:
|
||||||
return self.disabled_icon if disabled else self.icon
|
return self.disabled_icon if disabled else self.icon
|
||||||
|
@ -28,6 +28,7 @@ from calibre import detect_ncpus, force_unicode, prints
|
|||||||
from calibre.constants import (
|
from calibre.constants import (
|
||||||
DEBUG, __appname__, config_dir, filesystem_encoding, ismacos, iswindows
|
DEBUG, __appname__, config_dir, filesystem_encoding, ismacos, iswindows
|
||||||
)
|
)
|
||||||
|
from calibre.customize import PluginInstallationType
|
||||||
from calibre.customize.ui import available_store_plugins, interface_actions
|
from calibre.customize.ui import available_store_plugins, interface_actions
|
||||||
from calibre.db.legacy import LibraryDatabase
|
from calibre.db.legacy import LibraryDatabase
|
||||||
from calibre.gui2 import (
|
from calibre.gui2 import (
|
||||||
@ -127,7 +128,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
self.iactions = OrderedDict()
|
self.iactions = OrderedDict()
|
||||||
# Actions
|
# Actions
|
||||||
for action in interface_actions():
|
for action in interface_actions():
|
||||||
if opts.ignore_plugins and action.plugin_path is not None:
|
if opts.ignore_plugins \
|
||||||
|
and action.installation_type is not PluginInstallationType.BUILTIN:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
ac = self.init_iaction(action)
|
ac = self.init_iaction(action)
|
||||||
@ -139,7 +141,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
except Exception:
|
except Exception:
|
||||||
if action.plugin_path:
|
if action.plugin_path:
|
||||||
print('Failed to load Interface Action plugin:', action.plugin_path, file=sys.stderr)
|
print('Failed to load Interface Action plugin:', action.plugin_path, file=sys.stderr)
|
||||||
if action.plugin_path is None:
|
if action.installation_type is PluginInstallationType.BUILTIN:
|
||||||
raise
|
raise
|
||||||
continue
|
continue
|
||||||
ac.plugin_path = action.plugin_path
|
ac.plugin_path = action.plugin_path
|
||||||
@ -166,7 +168,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
from calibre.gui2.store.loader import Stores
|
from calibre.gui2.store.loader import Stores
|
||||||
self.istores = Stores()
|
self.istores = Stores()
|
||||||
for store in available_store_plugins():
|
for store in available_store_plugins():
|
||||||
if self.opts.ignore_plugins and store.plugin_path is not None:
|
if self.opts.ignore_plugins \
|
||||||
|
and store.installation_type is not PluginInstallationType.BUILTIN:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
st = self.init_istore(store)
|
st = self.init_istore(store)
|
||||||
@ -175,7 +178,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
# Ignore errors in loading user supplied plugins
|
# Ignore errors in loading user supplied plugins
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
if store.plugin_path is None:
|
if store.installation_type is PluginInstallationType.BUILTIN:
|
||||||
raise
|
raise
|
||||||
continue
|
continue
|
||||||
self.istores.builtins_loaded()
|
self.istores.builtins_loaded()
|
||||||
@ -369,7 +372,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
if ac.plugin_path is None:
|
if ac.installation_type is PluginInstallationType.BUILTIN:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if config['autolaunch_server']:
|
if config['autolaunch_server']:
|
||||||
@ -389,7 +392,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
if ac.plugin_path is None:
|
if ac.installation_type is PluginInstallationType.BUILTIN:
|
||||||
raise
|
raise
|
||||||
self.set_current_library_information(current_library_name(), db.library_id,
|
self.set_current_library_information(current_library_name(), db.library_id,
|
||||||
db.field_metadata)
|
db.field_metadata)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user