| #!/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() |