Blame SOURCES/import_all_modules.py

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