%PDF- %PDF-
| Direktori : /lib/calibre/calibre/customize/ |
| Current File : //lib/calibre/calibre/customize/zipplugin.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import importlib
import os
import posixpath
import re
import sys
import threading
import zipfile
from collections import OrderedDict
from functools import partial
from importlib.machinery import ModuleSpec
from importlib.util import decode_source
from calibre import as_unicode
from calibre.customize import (
InvalidPlugin, Plugin, PluginNotFound, numeric_version, platform
)
from polyglot.builtins import itervalues, reload, string_or_bytes
# PEP 302 based plugin loading mechanism, works around the bug in zipimport in
# python 2.x that prevents importing from zip files in locations whose paths
# have non ASCII characters
def get_resources(zfp, name_or_list_of_names):
'''
Load resources from the plugin zip file
:param name_or_list_of_names: List of paths to resources in the zip file using / as
separator, or a single path
:return: A dictionary of the form ``{name : file_contents}``. Any names
that were not found in the zip file will not be present in the
dictionary. If a single path is passed in the return value will
be just the bytes of the resource or None if it wasn't found.
'''
names = name_or_list_of_names
if isinstance(names, string_or_bytes):
names = [names]
ans = {}
with zipfile.ZipFile(zfp) as zf:
for name in names:
try:
ans[name] = zf.read(name)
except:
import traceback
traceback.print_exc()
if len(names) == 1:
ans = ans.pop(names[0], None)
return ans
def get_icons(zfp, name_or_list_of_names):
'''
Load icons from the plugin zip file
:param name_or_list_of_names: List of paths to resources in the zip file using / as
separator, or a single path
:return: A dictionary of the form ``{name : QIcon}``. Any names
that were not found in the zip file will be null QIcons.
If a single path is passed in the return value will
be A QIcon.
'''
from qt.core import QIcon, QPixmap
names = name_or_list_of_names
ans = get_resources(zfp, names)
if isinstance(names, string_or_bytes):
names = [names]
if ans is None:
ans = {}
if isinstance(ans, string_or_bytes):
ans = dict([(names[0], ans)])
ians = {}
for name in names:
p = QPixmap()
raw = ans.get(name, None)
if raw:
p.loadFromData(raw)
ians[name] = QIcon(p)
if len(names) == 1:
ians = ians.pop(names[0])
return ians
_translations_cache = {}
def load_translations(namespace, zfp):
null = object()
trans = _translations_cache.get(zfp, null)
if trans is None:
return
if trans is null:
from calibre.utils.localization import get_lang
lang = get_lang()
if not lang or lang == 'en': # performance optimization
_translations_cache[zfp] = None
return
with zipfile.ZipFile(zfp) as zf:
try:
mo = zf.read('translations/%s.mo' % lang)
except KeyError:
mo = None # No translations for this language present
if mo is None:
_translations_cache[zfp] = None
return
from gettext import GNUTranslations
from io import BytesIO
trans = _translations_cache[zfp] = GNUTranslations(BytesIO(mo))
namespace['_'] = trans.gettext
namespace['ngettext'] = trans.ngettext
class CalibrePluginLoader:
__slots__ = (
'plugin_name', 'fullname_in_plugin', 'zip_file_path', '_is_package', 'names',
'filename', 'all_names'
)
def __init__(self, plugin_name, fullname_in_plugin, zip_file_path, names, filename, is_package, all_names):
self.plugin_name = plugin_name
self.fullname_in_plugin = fullname_in_plugin
self.zip_file_path = zip_file_path
self.names = names
self.filename = filename
self._is_package = is_package
self.all_names = all_names
def __eq__(self, other):
return (
self.__class__ == other.__class__ and
self.plugin_name == other.plugin_name and
self.fullname_in_plugin == other.fullname_in_plugin
)
def get_resource_reader(self, fullname=None):
return self
def __hash__(self):
return hash(self.name) ^ hash(self.plugin_name) ^ hash(self.fullname_in_plugin)
def create_module(self, spec):
pass
def is_package(self, fullname):
return self._is_package
def get_source_as_bytes(self, fullname=None):
src = b''
if self.plugin_name and self.fullname_in_plugin and self.zip_file_path:
zinfo = self.names.get(self.fullname_in_plugin)
if zinfo is not None:
with zipfile.ZipFile(self.zip_file_path) as zf:
try:
src = zf.read(zinfo)
except Exception:
# Maybe the zip file changed from under us
src = zf.read(zinfo.filename)
return src
def get_source(self, fullname=None):
raw = self.get_source_as_bytes(fullname)
return decode_source(raw)
def get_filename(self, fullname):
return self.filename
def get_code(self, fullname=None):
return compile(self.get_source_as_bytes(fullname), f'calibre_plugins.{self.plugin_name}.{self.fullname_in_plugin}',
'exec', dont_inherit=True)
def exec_module(self, module):
compiled = self.get_code()
module.__file__ = self.filename
if self.zip_file_path:
zfp = self.zip_file_path
module.__dict__['get_resources'] = partial(get_resources, zfp)
module.__dict__['get_icons'] = partial(get_icons, zfp)
module.__dict__['load_translations'] = partial(load_translations, module.__dict__, zfp)
exec(compiled, module.__dict__)
def resource_path(self, name):
raise FileNotFoundError(
f'{name} is not available as a filesystem path in calibre plugins')
def contents(self):
if not self._is_package:
return ()
zinfo = self.names.get(self.fullname_in_plugin)
if zinfo is None:
return ()
base = posixpath.dirname(zinfo.filename)
if base:
base += '/'
def is_ok(x):
if not base or x.startswith(base):
rest = x[len(base):]
return '/' not in rest
return False
return tuple(filter(is_ok, self.all_names))
def is_resource(self, name):
zinfo = self.names.get(self.fullname_in_plugin)
if zinfo is None:
return False
base = posixpath.dirname(zinfo.filename)
q = posixpath.join(base, name)
return q in self.all_names
def open_resource(self, name):
zinfo = self.names.get(self.fullname_in_plugin)
if zinfo is None:
raise FileNotFoundError(f'{self.fullname_in_plugin} not in plugin zip file')
base = posixpath.dirname(zinfo.filename)
q = posixpath.join(base, name)
with zipfile.ZipFile(self.zip_file_path) as zf:
return zf.open(q)
class CalibrePluginFinder:
def __init__(self):
self.loaded_plugins = {}
self._lock = threading.RLock()
self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*')
def find_spec(self, fullname, path, target=None):
if not fullname.startswith('calibre_plugins'):
return
parts = fullname.split('.')
if parts[0] != 'calibre_plugins':
return
plugin_name = fullname_in_plugin = zip_file_path = filename = None
all_names = frozenset()
names = OrderedDict()
if len(parts) > 1:
plugin_name = parts[1]
with self._lock:
zip_file_path, names, all_names = self.loaded_plugins.get(plugin_name, (None, None, None))
if zip_file_path is None:
return
fullname_in_plugin = '.'.join(parts[2:])
if not fullname_in_plugin:
fullname_in_plugin = '__init__'
if fullname_in_plugin not in names:
if fullname_in_plugin + '.__init__' in names:
fullname_in_plugin += '.__init__'
else:
return
is_package = bool(
fullname.count('.') < 2 or
fullname_in_plugin == '__init__' or
(fullname_in_plugin and fullname_in_plugin.endswith('.__init__'))
)
if zip_file_path:
filename = posixpath.join(zip_file_path, *fullname_in_plugin.split('.')) + '.py'
return ModuleSpec(
fullname,
CalibrePluginLoader(plugin_name, fullname_in_plugin, zip_file_path, names, filename, is_package, all_names),
is_package=is_package, origin=filename
)
def load(self, path_to_zip_file):
if not os.access(path_to_zip_file, os.R_OK):
raise PluginNotFound('Cannot access %r'%path_to_zip_file)
with zipfile.ZipFile(path_to_zip_file) as zf:
plugin_name = self._locate_code(zf, path_to_zip_file)
try:
ans = None
plugin_module = 'calibre_plugins.%s'%plugin_name
m = sys.modules.get(plugin_module, None)
if m is not None:
reload(m)
else:
m = importlib.import_module(plugin_module)
plugin_classes = []
for obj in itervalues(m.__dict__):
if isinstance(obj, type) and issubclass(obj, Plugin) and \
obj.name != 'Trivial Plugin':
plugin_classes.append(obj)
if not plugin_classes:
raise InvalidPlugin('No plugin class found in %s:%s'%(
as_unicode(path_to_zip_file), plugin_name))
if len(plugin_classes) > 1:
plugin_classes.sort(key=lambda c:(getattr(c, '__module__', None) or '').count('.'))
ans = plugin_classes[0]
if ans.minimum_calibre_version > numeric_version:
raise InvalidPlugin(
'The plugin at %s needs a version of calibre >= %s' %
(as_unicode(path_to_zip_file), '.'.join(map(str,
ans.minimum_calibre_version))))
if platform not in ans.supported_platforms:
raise InvalidPlugin(
'The plugin at %s cannot be used on %s' %
(as_unicode(path_to_zip_file), platform))
return ans
except:
with self._lock:
del self.loaded_plugins[plugin_name]
raise
def _locate_code(self, zf, path_to_zip_file):
all_names = frozenset(zf.namelist())
names = [x[1:] if x[0] == '/' else x for x in all_names]
plugin_name = None
for name in names:
name, ext = posixpath.splitext(name)
if name.startswith('plugin-import-name-') and ext == '.txt':
plugin_name = name.rpartition('-')[-1]
if plugin_name is None:
c = 0
while True:
c += 1
plugin_name = 'dummy%d'%c
if plugin_name not in self.loaded_plugins:
break
else:
if self._identifier_pat.match(plugin_name) is None:
raise InvalidPlugin(
'The plugin at %r uses an invalid import name: %r' %
(path_to_zip_file, plugin_name))
pynames = [x for x in names if x.endswith('.py')]
candidates = [posixpath.dirname(x) for x in pynames if
x.endswith('/__init__.py')]
candidates.sort(key=lambda x: x.count('/'))
valid_packages = set()
for candidate in candidates:
parts = candidate.split('/')
parent = '.'.join(parts[:-1])
if parent and parent not in valid_packages:
continue
valid_packages.add('.'.join(parts))
names = OrderedDict()
for candidate in pynames:
parts = posixpath.splitext(candidate)[0].split('/')
package = '.'.join(parts[:-1])
if package and package not in valid_packages:
continue
name = '.'.join(parts)
names[name] = zf.getinfo(candidate)
# Legacy plugins
if '__init__' not in names:
for name in tuple(names):
if '.' not in name and name.endswith('plugin'):
names['__init__'] = names[name]
break
if '__init__' not in names:
raise InvalidPlugin(('The plugin in %r is invalid. It does not '
'contain a top-level __init__.py file')
% path_to_zip_file)
with self._lock:
self.loaded_plugins[plugin_name] = path_to_zip_file, names, tuple(all_names)
return plugin_name
loader = CalibrePluginFinder()
sys.meta_path.append(loader)
if __name__ == '__main__':
from tempfile import NamedTemporaryFile
from calibre import CurrentDir
from calibre.customize.ui import add_plugin
path = sys.argv[-1]
with NamedTemporaryFile(suffix='.zip') as f:
with zipfile.ZipFile(f, 'w') as zf:
with CurrentDir(path):
for x in os.listdir('.'):
if x[0] != '.':
print('Adding', x)
zf.write(x)
if os.path.isdir(x):
for y in os.listdir(x):
zf.write(os.path.join(x, y))
add_plugin(f.name)
print('Added plugin from', sys.argv[-1])