mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
First stab at a PEP 302 based plugin importer
This commit is contained in:
parent
75a8f43770
commit
2c6abe6a44
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user