Codebase list python-lml / run/4304ab7e-498b-405d-8768-a61b00f64537/upstream lml / loader.py
run/4304ab7e-498b-405d-8768-a61b00f64537/upstream

Tree @run/4304ab7e-498b-405d-8768-a61b00f64537/upstream (Download .tar.gz)

loader.py @run/4304ab7e-498b-405d-8768-a61b00f64537/upstreamraw · history · blame

"""
    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-2020 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