#!/usr/bin/env python3 
 | 
# 
 | 
# SPDX-License-Identifier: GPL-2.0-only 
 | 
# 
 | 
# Determine dependencies of python scripts or available python modules in a search path. 
 | 
# 
 | 
# Given the -d argument and a filename/filenames, returns the modules imported by those files. 
 | 
# Given the -d argument and a directory/directories, recurses to find all 
 | 
# python packages and modules, returns the modules imported by these. 
 | 
# Given the -p argument and a path or paths, scans that path for available python modules/packages. 
 | 
  
 | 
import argparse 
 | 
import ast 
 | 
import importlib 
 | 
from importlib import machinery 
 | 
import logging 
 | 
import os.path 
 | 
import sys 
 | 
  
 | 
  
 | 
logger = logging.getLogger('pythondeps') 
 | 
  
 | 
suffixes = importlib.machinery.all_suffixes() 
 | 
  
 | 
class PythonDepError(Exception): 
 | 
    pass 
 | 
  
 | 
  
 | 
class DependError(PythonDepError): 
 | 
    def __init__(self, path, error): 
 | 
        self.path = path 
 | 
        self.error = error 
 | 
        PythonDepError.__init__(self, error) 
 | 
  
 | 
    def __str__(self): 
 | 
        return "Failure determining dependencies of {}: {}".format(self.path, self.error) 
 | 
  
 | 
  
 | 
class ImportVisitor(ast.NodeVisitor): 
 | 
    def __init__(self): 
 | 
        self.imports = set() 
 | 
        self.importsfrom = [] 
 | 
  
 | 
    def visit_Import(self, node): 
 | 
        for alias in node.names: 
 | 
            self.imports.add(alias.name) 
 | 
  
 | 
    def visit_ImportFrom(self, node): 
 | 
        self.importsfrom.append((node.module, [a.name for a in node.names], node.level)) 
 | 
  
 | 
  
 | 
def walk_up(path): 
 | 
    while path: 
 | 
        yield path 
 | 
        path, _, _ = path.rpartition(os.sep) 
 | 
  
 | 
  
 | 
def get_provides(path): 
 | 
    path = os.path.realpath(path) 
 | 
  
 | 
    def get_fn_name(fn): 
 | 
        for suffix in suffixes: 
 | 
            if fn.endswith(suffix): 
 | 
                return fn[:-len(suffix)] 
 | 
  
 | 
    isdir = os.path.isdir(path) 
 | 
    if isdir: 
 | 
        pkg_path = path 
 | 
        walk_path = path 
 | 
    else: 
 | 
        pkg_path = get_fn_name(path) 
 | 
        if pkg_path is None: 
 | 
            return 
 | 
        walk_path = os.path.dirname(path) 
 | 
  
 | 
    for curpath in walk_up(walk_path): 
 | 
        if not os.path.exists(os.path.join(curpath, '__init__.py')): 
 | 
            libdir = curpath 
 | 
            break 
 | 
    else: 
 | 
        libdir = '' 
 | 
  
 | 
    package_relpath = pkg_path[len(libdir)+1:] 
 | 
    package = '.'.join(package_relpath.split(os.sep)) 
 | 
    if not isdir: 
 | 
        yield package, path 
 | 
    else: 
 | 
        if os.path.exists(os.path.join(path, '__init__.py')): 
 | 
            yield package, path 
 | 
  
 | 
        for dirpath, dirnames, filenames in os.walk(path): 
 | 
            relpath = dirpath[len(path)+1:] 
 | 
            if relpath: 
 | 
                if '__init__.py' not in filenames: 
 | 
                    dirnames[:] = [] 
 | 
                    continue 
 | 
                else: 
 | 
                    context = '.'.join(relpath.split(os.sep)) 
 | 
                    if package: 
 | 
                        context = package + '.' + context 
 | 
                    yield context, dirpath 
 | 
            else: 
 | 
                context = package 
 | 
  
 | 
            for fn in filenames: 
 | 
                adjusted_fn = get_fn_name(fn) 
 | 
                if not adjusted_fn or adjusted_fn == '__init__': 
 | 
                    continue 
 | 
  
 | 
                fullfn = os.path.join(dirpath, fn) 
 | 
                if context: 
 | 
                    yield context + '.' + adjusted_fn, fullfn 
 | 
                else: 
 | 
                    yield adjusted_fn, fullfn 
 | 
  
 | 
  
 | 
def get_code_depends(code_string, path=None, provide=None, ispkg=False): 
 | 
    try: 
 | 
        code = ast.parse(code_string, path) 
 | 
    except TypeError as exc: 
 | 
        raise DependError(path, exc) 
 | 
    except SyntaxError as exc: 
 | 
        raise DependError(path, exc) 
 | 
  
 | 
    visitor = ImportVisitor() 
 | 
    visitor.visit(code) 
 | 
    for builtin_module in sys.builtin_module_names: 
 | 
        if builtin_module in visitor.imports: 
 | 
            visitor.imports.remove(builtin_module) 
 | 
  
 | 
    if provide: 
 | 
        provide_elements = provide.split('.') 
 | 
        if ispkg: 
 | 
            provide_elements.append("__self__") 
 | 
        context = '.'.join(provide_elements[:-1]) 
 | 
        package_path = os.path.dirname(path) 
 | 
    else: 
 | 
        context = None 
 | 
        package_path = None 
 | 
  
 | 
    levelzero_importsfrom = (module for module, names, level in visitor.importsfrom 
 | 
                             if level == 0) 
 | 
    for module in visitor.imports | set(levelzero_importsfrom): 
 | 
        if context and path: 
 | 
            module_basepath = os.path.join(package_path, module.replace('.', '/')) 
 | 
            if os.path.exists(module_basepath): 
 | 
                # Implicit relative import 
 | 
                yield context + '.' + module, path 
 | 
                continue 
 | 
  
 | 
            for suffix in suffixes: 
 | 
                if os.path.exists(module_basepath + suffix): 
 | 
                    # Implicit relative import 
 | 
                    yield context + '.' + module, path 
 | 
                    break 
 | 
            else: 
 | 
                yield module, path 
 | 
        else: 
 | 
            yield module, path 
 | 
  
 | 
    for module, names, level in visitor.importsfrom: 
 | 
        if level == 0: 
 | 
            continue 
 | 
        elif not provide: 
 | 
            raise DependError("Error: ImportFrom non-zero level outside of a package: {0}".format((module, names, level)), path) 
 | 
        elif level > len(provide_elements): 
 | 
            raise DependError("Error: ImportFrom level exceeds package depth: {0}".format((module, names, level)), path) 
 | 
        else: 
 | 
            context = '.'.join(provide_elements[:-level]) 
 | 
            if module: 
 | 
                if context: 
 | 
                    yield context + '.' + module, path 
 | 
                else: 
 | 
                    yield module, path 
 | 
  
 | 
  
 | 
def get_file_depends(path): 
 | 
    try: 
 | 
        code_string = open(path, 'r').read() 
 | 
    except (OSError, IOError) as exc: 
 | 
        raise DependError(path, exc) 
 | 
  
 | 
    return get_code_depends(code_string, path) 
 | 
  
 | 
  
 | 
def get_depends_recursive(directory): 
 | 
    directory = os.path.realpath(directory) 
 | 
  
 | 
    provides = dict((v, k) for k, v in get_provides(directory)) 
 | 
    for filename, provide in provides.items(): 
 | 
        if os.path.isdir(filename): 
 | 
            filename = os.path.join(filename, '__init__.py') 
 | 
            ispkg = True 
 | 
        elif not filename.endswith('.py'): 
 | 
            continue 
 | 
        else: 
 | 
            ispkg = False 
 | 
  
 | 
        with open(filename, 'r') as f: 
 | 
            source = f.read() 
 | 
  
 | 
        depends = get_code_depends(source, filename, provide, ispkg) 
 | 
        for depend, by in depends: 
 | 
            yield depend, by 
 | 
  
 | 
  
 | 
def get_depends(path): 
 | 
    if os.path.isdir(path): 
 | 
        return get_depends_recursive(path) 
 | 
    else: 
 | 
        return get_file_depends(path) 
 | 
  
 | 
  
 | 
def main(): 
 | 
    logging.basicConfig() 
 | 
  
 | 
    parser = argparse.ArgumentParser(description='Determine dependencies and provided packages for python scripts/modules') 
 | 
    parser.add_argument('path', nargs='+', help='full path to content to be processed') 
 | 
    group = parser.add_mutually_exclusive_group() 
 | 
    group.add_argument('-p', '--provides', action='store_true', 
 | 
                       help='given a path, display the provided python modules') 
 | 
    group.add_argument('-d', '--depends', action='store_true', 
 | 
                       help='given a filename, display the imported python modules') 
 | 
  
 | 
    args = parser.parse_args() 
 | 
    if args.provides: 
 | 
        modules = set() 
 | 
        for path in args.path: 
 | 
            for provide, fn in get_provides(path): 
 | 
                modules.add(provide) 
 | 
  
 | 
        for module in sorted(modules): 
 | 
            print(module) 
 | 
    elif args.depends: 
 | 
        for path in args.path: 
 | 
            try: 
 | 
                modules = get_depends(path) 
 | 
            except PythonDepError as exc: 
 | 
                logger.error(str(exc)) 
 | 
                sys.exit(1) 
 | 
  
 | 
            for module, imp_by in modules: 
 | 
                print("{}\t{}".format(module, imp_by)) 
 | 
    else: 
 | 
        parser.print_help() 
 | 
        sys.exit(2) 
 | 
  
 | 
  
 | 
if __name__ == '__main__': 
 | 
    main() 
 |