41f5962
'''Script to perform import of each module given to %%py_check_import
41f5962
'''
41f5962
import argparse
41f5962
import importlib
41f5962
import fnmatch
41f5962
import re
41f5962
import sys
41f5962
41f5962
from contextlib import contextmanager
41f5962
from pathlib import Path
41f5962
41f5962
41f5962
def read_modules_files(file_paths):
41f5962
    '''Read module names from the files (modules must be newline separated).
41f5962
41f5962
    Return the module names list or, if no files were provided, an empty list.
41f5962
    '''
41f5962
41f5962
    if not file_paths:
41f5962
        return []
41f5962
41f5962
    modules = []
41f5962
    for file in file_paths:
41f5962
        file_contents = file.read_text()
41f5962
        modules.extend(file_contents.split())
41f5962
    return modules
41f5962
41f5962
41f5962
def read_modules_from_cli(argv):
41f5962
    '''Read module names from command-line arguments (space or comma separated).
41f5962
41f5962
    Return the module names list.
41f5962
    '''
41f5962
41f5962
    if not argv:
41f5962
        return []
41f5962
41f5962
    # %%py3_check_import allows to separate module list with comma or whitespace,
41f5962
    # we need to unify the output to a list of particular elements
41f5962
    modules_as_str = ' '.join(argv)
41f5962
    modules = re.split(r'[\s,]+', modules_as_str)
208372b
    # Because of shell expansion in some less typical cases it may happen
208372b
    # that a trailing space will occur at the end of the list.
208372b
    # Remove the empty items from the list before passing it further
208372b
    modules = [m for m in modules if m]
41f5962
    return modules
41f5962
41f5962
41f5962
def filter_top_level_modules_only(modules):
41f5962
    '''Filter out entries with nested modules (containing dot) ie. 'foo.bar'.
41f5962
41f5962
    Return the list of top-level modules.
41f5962
    '''
41f5962
41f5962
    return [module for module in modules if '.' not in module]
41f5962
41f5962
41f5962
def any_match(text, globs):
41f5962
    '''Return True if any of given globs fnmatchcase's the given text.'''
41f5962
41f5962
    return any(fnmatch.fnmatchcase(text, g) for g in globs)
41f5962
41f5962
41f5962
def exclude_unwanted_module_globs(globs, modules):
41f5962
    '''Filter out entries which match the either of the globs given as argv.
41f5962
41f5962
    Return the list of filtered modules.
41f5962
    '''
41f5962
41f5962
    return [m for m in modules if not any_match(m, globs)]
41f5962
41f5962
41f5962
def read_modules_from_all_args(args):
41f5962
    '''Return a joined list of modules from all given command-line arguments.
41f5962
    '''
41f5962
41f5962
    modules = read_modules_files(args.filename)
41f5962
    modules.extend(read_modules_from_cli(args.modules))
41f5962
    if args.exclude:
41f5962
        modules = exclude_unwanted_module_globs(args.exclude, modules)
41f5962
41f5962
    if args.top_level:
41f5962
        modules = filter_top_level_modules_only(modules)
41f5962
41f5962
    # Error when someone accidentally managed to filter out everything
41f5962
    if len(modules) == 0:
41f5962
        raise ValueError('No modules to check were left')
41f5962
41f5962
    return modules
41f5962
41f5962
41f5962
def import_modules(modules):
41f5962
    '''Procedure to perform import check for each module name from the given list of modules.
41f5962
    '''
41f5962
41f5962
    for module in modules:
41f5962
        print('Check import:', module, file=sys.stderr)
41f5962
        importlib.import_module(module)
41f5962
41f5962
41f5962
def argparser():
41f5962
    parser = argparse.ArgumentParser(
41f5962
        description='Generate list of all importable modules for import check.'
41f5962
    )
41f5962
    parser.add_argument(
41f5962
        'modules', nargs='*',
41f5962
        help=('Add modules to check the import (space or comma separated).'),
41f5962
    )
41f5962
    parser.add_argument(
41f5962
        '-f', '--filename', action='append', type=Path,
41f5962
        help='Add importable module names list from file.',
41f5962
    )
41f5962
    parser.add_argument(
41f5962
        '-t', '--top-level', action='store_true',
41f5962
        help='Check only top-level modules.',
41f5962
    )
41f5962
    parser.add_argument(
41f5962
        '-e', '--exclude', action='append',
41f5962
        help='Provide modules globs to be excluded from the check.',
41f5962
    )
41f5962
    return parser
41f5962
41f5962
41f5962
@contextmanager
41f5962
def remove_unwanteds_from_sys_path():
41f5962
    '''Remove cwd and this script's parent from sys.path for the import test.
41f5962
    Bring the original contents back after import is done (or failed)
41f5962
    '''
41f5962
41f5962
    cwd_absolute = Path.cwd().absolute()
41f5962
    this_file_parent = Path(__file__).parent.absolute()
41f5962
    old_sys_path = list(sys.path)
41f5962
    for path in old_sys_path:
41f5962
        if Path(path).absolute() in (cwd_absolute, this_file_parent):
41f5962
            sys.path.remove(path)
41f5962
    try:
41f5962
        yield
41f5962
    finally:
41f5962
        sys.path = old_sys_path
41f5962
41f5962
41f5962
def main(argv=None):
41f5962
41f5962
    cli_args = argparser().parse_args(argv)
41f5962
41f5962
    if not cli_args.modules and not cli_args.filename:
41f5962
        raise ValueError('No modules to check were provided')
41f5962
41f5962
    modules = read_modules_from_all_args(cli_args)
41f5962
41f5962
    with remove_unwanteds_from_sys_path():
41f5962
        import_modules(modules)
41f5962
41f5962
41f5962
if __name__ == '__main__':
41f5962
    main()