debian-python-lml/lml/loader.py

167 lines
4.9 KiB
Python

"""
lml.loader
~~~~~~~~~~~~~~~~~~~
Plugin discovery module. It supports plugins installed via pip tools
and pyinstaller. :func:`~lml.loader.scan_plugins` is expected to be
called in the main package of yours at an earliest time of convenience.
:copyright: (c) 2017-2018 by Onni Software Ltd.
:license: New BSD License, see LICENSE for more details
"""
import re
import logging
import pkgutil
import warnings
from itertools import chain
from lml.utils import do_import
log = logging.getLogger(__name__)
def scan_plugins(
prefix,
pyinstaller_path,
black_list=None,
white_list=None,
plugin_name_patterns=None,
):
"""
Implicitly discover plugins via pkgutil and pyinstaller path
Parameters
-----------------
prefix:string
module prefix. This prefix should become the prefix of the module name
of all plugins.
In the tutorial, robotchef-britishcuisine is a plugin package
of robotchef and its module name is 'robotchef_britishcuisine'. When
robotchef call scan_plugins to load its cuisine plugins, it specifies
its prefix as "robotchef_". All modules that starts with 'robotchef_'
will be auto-loaded: robotchef_britishcuisine, robotchef_chinesecuisine,
etc.
pyinstaller_path:string
used in pyinstaller only. When your end developer would package
your main library and its plugins using pyinstaller, this path
helps pyinstaller to find the plugins.
black_list:list
a list of module names that should be skipped.
white_list:list
a list of modules that comes with your main module. If you have a
built-in module, the module name should be inserted into the list.
For example, robot_cuisine is a built-in module inside robotchef. It
is listed in white_list.
"""
__plugin_name_patterns = "^%s.+$" % prefix
warnings.warn(
"Deprecated! since version 0.0.3. Please use scan_plugins_regex!"
)
scan_plugins_regex(
plugin_name_patterns=__plugin_name_patterns,
pyinstaller_path=pyinstaller_path,
black_list=black_list,
white_list=white_list,
)
def scan_plugins_regex(
plugin_name_patterns=None,
pyinstaller_path=None,
black_list=None,
white_list=None,
):
"""
Implicitly discover plugins via pkgutil and pyinstaller path using
regular expression
Parameters
-----------------
plugin_name_patterns: python regular expression
it is used to match all your plugins, either it is a prefix,
a suffix, some text in the middle or all.
pyinstaller_path:string
used in pyinstaller only. When your end developer would package
your main library and its plugins using pyinstaller, this path
helps pyinstaller to find the plugins.
black_list:list
a list of module names that should be skipped.
white_list:list
a list of modules that comes with your main module. If you have a
built-in module, the module name should be inserted into the list.
For example, robot_cuisine is a built-in module inside robotchef. It
is listed in white_list.
"""
log.debug("scanning for plugins...")
if black_list is None:
black_list = []
if white_list is None:
white_list = []
# scan pkgutil.iter_modules
module_names = (
module_info[1]
for module_info in pkgutil.iter_modules()
if module_info[2] and re.match(plugin_name_patterns, module_info[1])
)
# scan pyinstaller
module_names_from_pyinstaller = scan_from_pyinstaller(
plugin_name_patterns, pyinstaller_path
)
all_modules = chain(
module_names, module_names_from_pyinstaller, white_list
)
# loop through modules and find our plug ins
for module_name in all_modules:
if module_name in black_list:
log.debug("ignored " + module_name)
continue
try:
do_import(module_name)
except ImportError as e:
log.debug(module_name)
log.debug(e)
continue
log.debug("scanning done")
# load modules to work based with and without pyinstaller
# from: https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py
# see: https://github.com/pyinstaller/pyinstaller/issues/1905
# load modules using iter_modules()
# (should find all plug ins in normal build, but not pyinstaller)
def scan_from_pyinstaller(plugin_name_patterns, path):
"""
Discover plugins from pyinstaller
"""
table_of_content = set()
for a_toc in (
importer.toc
for importer in map(pkgutil.get_importer, path)
if hasattr(importer, "toc")
):
table_of_content |= a_toc
for module_name in table_of_content:
if "." in module_name:
continue
if re.match(plugin_name_patterns, module_name):
yield module_name