This commit is contained in:
Kovid Goyal 2011-03-26 13:06:47 -06:00
parent 197d37f358
commit 75a8f43770
4 changed files with 175 additions and 55 deletions

View File

@ -4,9 +4,22 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, zipfile, importlib import os, sys, zipfile, importlib
from calibre.constants import numeric_version from calibre.constants import numeric_version, iswindows, isosx
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
platform = 'linux'
if iswindows:
platform = 'windows'
elif isosx:
platform = 'osx'
class PluginNotFound(ValueError):
pass
class InvalidPlugin(ValueError):
pass
class Plugin(object): # {{{ class Plugin(object): # {{{
''' '''

View File

@ -2,17 +2,16 @@ from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, shutil, traceback, functools, sys, re import os, shutil, traceback, functools, sys
from contextlib import closing
from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \ from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
MetadataReaderPlugin, MetadataWriterPlugin, \ MetadataReaderPlugin, MetadataWriterPlugin,
InterfaceActionBase as InterfaceAction, \ InterfaceActionBase as InterfaceAction,
PreferencesPlugin PreferencesPlugin, platform, InvalidPlugin)
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
from calibre.customize.zipplugin import loader
from calibre.customize.profiles import InputProfile, OutputProfile from calibre.customize.profiles import InputProfile, OutputProfile
from calibre.customize.builtins import plugins as builtin_plugins from calibre.customize.builtins import plugins as builtin_plugins
from calibre.constants import numeric_version as version, iswindows, isosx
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.covers import CoverDownload from calibre.ebooks.metadata.covers import CoverDownload
@ -22,14 +21,6 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
from calibre.ebooks.epub.fix import ePubFixer from calibre.ebooks.epub.fix import ePubFixer
from calibre.ebooks.metadata.sources.base import Source from calibre.ebooks.metadata.sources.base import Source
platform = 'linux'
if iswindows:
platform = 'windows'
elif isosx:
platform = 'osx'
from zipfile import ZipFile
def _config(): def _config():
c = Config('customize') c = Config('customize')
c.add_opt('plugins', default={}, help=_('Installed plugins')) c.add_opt('plugins', default={}, help=_('Installed plugins'))
@ -42,11 +33,6 @@ def _config():
config = _config() config = _config()
class InvalidPlugin(ValueError):
pass
class PluginNotFound(ValueError):
pass
def find_plugin(name): def find_plugin(name):
for plugin in _initialized_plugins: for plugin in _initialized_plugins:
@ -60,38 +46,7 @@ def load_plugin(path_to_zip_file): # {{{
:return: A :class:`Plugin` instance. :return: A :class:`Plugin` instance.
''' '''
#print 'Loading plugin from', path_to_zip_file return loader.load(path_to_zip_file)
if not os.access(path_to_zip_file, os.R_OK):
raise PluginNotFound
with closing(ZipFile(path_to_zip_file)) as zf:
for name in zf.namelist():
if name.lower().endswith('plugin.py'):
locals = {}
raw = zf.read(name)
lines, encoding = raw.splitlines(), 'utf-8'
cr = re.compile(r'coding[:=]\s*([-\w.]+)')
raw = []
for l in lines[:2]:
match = cr.search(l)
if match is not None:
encoding = match.group(1)
else:
raw.append(l)
raw += lines[2:]
raw = '\n'.join(raw)
raw = raw.decode(encoding)
raw = re.sub('\r\n', '\n', raw)
exec raw in locals
for x in locals.values():
if isinstance(x, type) and issubclass(x, Plugin) and \
x.name != 'Trivial Plugin':
if x.minimum_calibre_version > version or \
platform not in x.supported_platforms:
continue
return x
raise InvalidPlugin(_('No valid plugin found in ')+path_to_zip_file)
# }}} # }}}

View File

@ -0,0 +1,154 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, zipfile, posixpath, importlib, threading, re
from collections import OrderedDict
from calibre.customize import (Plugin, numeric_version, platform,
InvalidPlugin, PluginNotFound)
# 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
class PluginLoader(object):
'''
The restrictions that a zip file must obey to be a valid calibre plugin
are:
* The .py file that defines the main plugin class must have a name
that:
* Ends in plugin.py
* Is a valid python identifier (contains only English alphabets,
underscores and numbers and starts with an alphabet). This
applies to the file name minus the .py extension, obviously.
* Try to make this name as distinct as possible, as it will be
put into a global namespace of all plugins.
* The zip file must contain a .py file that defines the main plugin
class at the top level. That is, it must not be in a subdirectory.
The filename must follow the restrictions outlined above.
'''
def __init__(self):
self.loaded_plugins = {}
self._lock = threading.RLock()
self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*')
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
m = importlib.import_module(
'calibre_plugins.%s.__init__'%plugin_name)
for obj in m.__dict__.itervalues():
if isinstance(obj, type) and issubclass(obj, Plugin) and \
obj.name != 'Trivial Plugin':
ans = obj
break
if ans is None:
raise InvalidPlugin('No plugin class found in %r:%r'%(
path_to_zip_file, plugin_name))
if ans.minimum_calibre_version < numeric_version:
raise InvalidPlugin(
'The plugin at %r needs a version of calibre >= %r' %
(path_to_zip_file, '.'.join(ans.minimum_calibre_version)))
if platform not in ans.supported_platforms:
raise InvalidPlugin(
'The plugin at %r cannot be used on %s' %
(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):
names = [x if isinstance(x, unicode) else x.decode('utf-8') for x in
zf.namelist()]
names = [x[1:] if x[0] == '/' else x for x in 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 plugin_name in self.loaded_plugins:
raise InvalidPlugin((
'The plugin in %r uses an import name %r that is already'
' used by another plugin') % (path_to_zip_file, plugin_name))
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 names:
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 list(names.iterkeys()):
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)
return plugin_name
loader = PluginLoader()

View File

@ -165,8 +165,6 @@ class TXTInput(InputFormatPlugin):
elif options.formatting_type == 'textile': elif options.formatting_type == 'textile':
log.debug('Running text through textile conversion...') log.debug('Running text through textile conversion...')
html = convert_textile(txt) html = convert_textile(txt)
# Textile input already runs smartypants
options.smarten_punctuation = False
else: else:
log.debug('Running text through basic conversion...') log.debug('Running text through basic conversion...')
flow_size = getattr(options, 'flow_size', 0) flow_size = getattr(options, 'flow_size', 0)