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_loc = sys.extensions_location
|
||||
system_plugins_loc = getattr(sys, 'system_plugins_location', None)
|
||||
|
||||
from importlib.machinery import ModuleSpec, EXTENSION_SUFFIXES, ExtensionFileLoader
|
||||
from importlib.util import find_spec
|
||||
from importlib import import_module
|
||||
|
@ -2,7 +2,7 @@
|
||||
__license__ = 'GPL v3'
|
||||
__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.ptempfile import PersistentTemporaryFile
|
||||
@ -24,10 +24,17 @@ class InvalidPlugin(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class PluginInstallationType(enum.Enum):
|
||||
BUILTIN = enum.auto()
|
||||
SYSTEM = enum.auto()
|
||||
EXTERNAL = enum.auto()
|
||||
|
||||
|
||||
class Plugin(object): # {{{
|
||||
'''
|
||||
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
|
||||
this plugin or None if it is a builtin
|
||||
plugin
|
||||
@ -73,6 +80,9 @@ class Plugin(object): # {{{
|
||||
#: The earliest version of calibre this plugin requires
|
||||
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
|
||||
#: care.
|
||||
can_be_disabled = True
|
||||
|
@ -4,14 +4,14 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import os, shutil, traceback, functools, sys
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
from itertools import chain, repeat
|
||||
|
||||
from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
|
||||
MetadataReaderPlugin, MetadataWriterPlugin,
|
||||
InterfaceActionBase as InterfaceAction,
|
||||
PreferencesPlugin, platform, InvalidPlugin,
|
||||
StoreBase as Store, EditBookToolPlugin,
|
||||
LibraryClosedPlugin)
|
||||
LibraryClosedPlugin, PluginInstallationType)
|
||||
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
||||
from calibre.customize.zipplugin import loader
|
||||
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,
|
||||
plugin_dir, OptionParser)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@ -341,10 +345,19 @@ def reread_metadata_plugins():
|
||||
for ft in plugin.file_types:
|
||||
_metadata_writers[ft].append(plugin)
|
||||
|
||||
# Ensure custom metadata plugins are used in preference to builtin
|
||||
# ones for a given filetype
|
||||
# Ensure the following metadata plugin preference is used:
|
||||
# external > system > builtin
|
||||
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 plugins in itervalues(group):
|
||||
@ -473,7 +486,10 @@ def add_plugin(path_to_zip_file):
|
||||
if plugin.name in builtin_names:
|
||||
raise NameConflict(
|
||||
'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']
|
||||
zfp = os.path.join(plugin_dir, plugin.name+'.zip')
|
||||
if os.path.exists(zfp):
|
||||
@ -659,9 +675,10 @@ def all_edit_book_tool_plugins():
|
||||
_initialized_plugins = []
|
||||
|
||||
|
||||
def initialize_plugin(plugin, path_to_zip_file):
|
||||
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:
|
||||
@ -676,36 +693,67 @@ def has_external_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):
|
||||
global _initialized_plugins
|
||||
global _initialized_plugins, _system_plugins
|
||||
_initialized_plugins = []
|
||||
_system_plugins = []
|
||||
system_plugins = dict(_list_system_plugins())
|
||||
conflicts = [name for name in config['plugins'] if name in
|
||||
builtin_names]
|
||||
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:
|
||||
from collections import defaultdict
|
||||
import time
|
||||
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:
|
||||
if not isinstance(zfp, type):
|
||||
# We have a plugin name
|
||||
pname = zfp
|
||||
zfp = os.path.join(plugin_dir, zfp+'.zip')
|
||||
pname, path = zfp
|
||||
zfp = os.path.join(plugin_dir, pname+'.zip')
|
||||
if not os.path.exists(zfp):
|
||||
zfp = external_plugins[pname]
|
||||
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)
|
||||
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)
|
||||
|
@ -20,6 +20,7 @@ from calibre import prints
|
||||
from calibre.constants import (
|
||||
DEBUG, __appname__, __version__, ismacos, iswindows, numeric_version
|
||||
)
|
||||
from calibre.customize import PluginInstallationType
|
||||
from calibre.customize.ui import (
|
||||
NameConflict, add_plugin, disable_plugin, enable_plugin, has_external_plugins,
|
||||
initialized_plugins, is_disabled, remove_plugin
|
||||
@ -56,7 +57,8 @@ def get_plugin_updates_available(raise_error=False):
|
||||
|
||||
|
||||
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):
|
||||
@ -96,7 +98,8 @@ def get_installed_plugin_status(display_plugin):
|
||||
display_plugin.installed_version = None
|
||||
display_plugin.plugin = None
|
||||
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.installed_version = plugin.version
|
||||
break
|
||||
@ -195,6 +198,7 @@ class DisplayPlugin(object):
|
||||
self.uninstall_plugins = plugin['uninstall'] or []
|
||||
self.has_changelog = plugin['history']
|
||||
self.is_deprecated = plugin['deprecated']
|
||||
self.installation_type = plugin['installation_type']
|
||||
|
||||
def is_disabled(self):
|
||||
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.plugins_ui import Ui_Form
|
||||
from calibre.customize import PluginInstallationType
|
||||
from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin,
|
||||
disable_plugin, plugin_customization, add_plugin,
|
||||
remove_plugin, NameConflict)
|
||||
@ -211,6 +212,8 @@ class PluginModel(QAbstractItemModel, AdaptSQP): # {{{
|
||||
ans += _('\nCustomization: ')+c
|
||||
if 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)
|
||||
if role == Qt.ItemDataRole.DecorationRole:
|
||||
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 (
|
||||
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.db.legacy import LibraryDatabase
|
||||
from calibre.gui2 import (
|
||||
@ -127,7 +128,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
self.iactions = OrderedDict()
|
||||
# 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
|
||||
try:
|
||||
ac = self.init_iaction(action)
|
||||
@ -139,7 +141,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
except Exception:
|
||||
if action.plugin_path:
|
||||
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
|
||||
continue
|
||||
ac.plugin_path = action.plugin_path
|
||||
@ -166,7 +168,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
from calibre.gui2.store.loader import Stores
|
||||
self.istores = Stores()
|
||||
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
|
||||
try:
|
||||
st = self.init_istore(store)
|
||||
@ -175,7 +178,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
# Ignore errors in loading user supplied plugins
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if store.plugin_path is None:
|
||||
if store.installation_type is PluginInstallationType.BUILTIN:
|
||||
raise
|
||||
continue
|
||||
self.istores.builtins_loaded()
|
||||
@ -369,7 +372,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if ac.plugin_path is None:
|
||||
if ac.installation_type is PluginInstallationType.BUILTIN:
|
||||
raise
|
||||
|
||||
if config['autolaunch_server']:
|
||||
@ -389,7 +392,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if ac.plugin_path is None:
|
||||
if ac.installation_type is PluginInstallationType.BUILTIN:
|
||||
raise
|
||||
self.set_current_library_information(current_library_name(), db.library_id,
|
||||
db.field_metadata)
|
||||
|
Loading…
x
Reference in New Issue
Block a user