First stab at a PEP 302 based plugin importer

This commit is contained in:
Kovid Goyal 2011-03-26 17:13:27 -06:00
parent 75a8f43770
commit 2c6abe6a44

View File

@ -2,12 +2,13 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import, from __future__ import (unicode_literals, division, absolute_import,
print_function) print_function)
from future_builtins import map
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, zipfile, posixpath, importlib, threading, re import os, zipfile, posixpath, importlib, threading, re, imp, sys
from collections import OrderedDict from collections import OrderedDict
from calibre.customize import (Plugin, numeric_version, platform, from calibre.customize import (Plugin, numeric_version, platform,
@ -20,28 +21,67 @@ from calibre.customize import (Plugin, numeric_version, platform,
class PluginLoader(object): 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): def __init__(self):
self.loaded_plugins = {} self.loaded_plugins = {}
self._lock = threading.RLock() self._lock = threading.RLock()
self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*') self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*')
def _get_actual_fullname(self, fullname):
parts = fullname.split('.')
if parts[0] == 'calibre_plugins':
if len(parts) == 1:
return parts[0], None
plugin_name = parts[1]
with self._lock:
names = self.loaded_plugins.get(plugin_name, None)[1]
if names is None:
raise ImportError('No plugin named %r loaded'%plugin_name)
fullname = '.'.join(parts[2:])
if not fullname:
fullname = '__init__'
if fullname in names:
return fullname, plugin_name
if fullname+'.__init__' in names:
return fullname+'.__init__', plugin_name
return None, None
def find_module(self, fullname, path=None):
fullname, plugin_name = self._get_actual_fullname(fullname)
if fullname is None and plugin_name is None:
return None
return self
def load_module(self, fullname):
import_name, plugin_name = self._get_actual_fullname(fullname)
if import_name is None and plugin_name is None:
raise ImportError('No plugin named %r is loaded'%fullname)
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
mod.__file__ = "<calibre Plugin Loader>"
mod.__loader__ = self
if import_name.endswith('.__init__') or import_name in ('__init__',
'calibre_plugins'):
# We have a package
mod.__path__ = []
if plugin_name is not None:
# We have some actual code to load
with self._lock:
zfp, names = self.loaded_plugins.get(plugin_name, (None, None))
if names is None:
raise ImportError('No plugin named %r loaded'%plugin_name)
zinfo = names.get(import_name, None)
if zinfo is None:
raise ImportError('Plugin %r has no module named %r' %
(plugin_name, import_name))
with zipfile.ZipFile(zfp) as zf:
code = zf.read(zinfo)
compiled = compile(code, 'import_name', 'exec', dont_inherit=True)
exec compiled in mod.__dict__
return mod
def load(self, path_to_zip_file): def load(self, path_to_zip_file):
if not os.access(path_to_zip_file, os.R_OK): if not os.access(path_to_zip_file, os.R_OK):
raise PluginNotFound('Cannot access %r'%path_to_zip_file) raise PluginNotFound('Cannot access %r'%path_to_zip_file)
@ -52,7 +92,7 @@ class PluginLoader(object):
try: try:
ans = None ans = None
m = importlib.import_module( m = importlib.import_module(
'calibre_plugins.%s.__init__'%plugin_name) 'calibre_plugins.%s'%plugin_name)
for obj in m.__dict__.itervalues(): for obj in m.__dict__.itervalues():
if isinstance(obj, type) and issubclass(obj, Plugin) and \ if isinstance(obj, type) and issubclass(obj, Plugin) and \
obj.name != 'Trivial Plugin': obj.name != 'Trivial Plugin':
@ -62,10 +102,11 @@ class PluginLoader(object):
raise InvalidPlugin('No plugin class found in %r:%r'%( raise InvalidPlugin('No plugin class found in %r:%r'%(
path_to_zip_file, plugin_name)) path_to_zip_file, plugin_name))
if ans.minimum_calibre_version < numeric_version: if ans.minimum_calibre_version > numeric_version:
raise InvalidPlugin( raise InvalidPlugin(
'The plugin at %r needs a version of calibre >= %r' % 'The plugin at %r needs a version of calibre >= %r' %
(path_to_zip_file, '.'.join(ans.minimum_calibre_version))) (path_to_zip_file, '.'.join(map(str,
ans.minimum_calibre_version))))
if platform not in ans.supported_platforms: if platform not in ans.supported_platforms:
raise InvalidPlugin( raise InvalidPlugin(
@ -123,7 +164,7 @@ class PluginLoader(object):
names = OrderedDict() names = OrderedDict()
for candidate in names: for candidate in pynames:
parts = posixpath.splitext(candidate)[0].split('/') parts = posixpath.splitext(candidate)[0].split('/')
package = '.'.join(parts[:-1]) package = '.'.join(parts[:-1])
if package and package not in valid_packages: if package and package not in valid_packages:
@ -150,5 +191,6 @@ class PluginLoader(object):
loader = PluginLoader() loader = PluginLoader()
sys.meta_path.insert(0, loader)