xref: /OK3568_Linux_fs/yocto/poky/scripts/pythondeps (revision 4882a59341e53eb6f0b4789bf948001014eff981)
1*4882a593Smuzhiyun#!/usr/bin/env python3
2*4882a593Smuzhiyun#
3*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only
4*4882a593Smuzhiyun#
5*4882a593Smuzhiyun# Determine dependencies of python scripts or available python modules in a search path.
6*4882a593Smuzhiyun#
7*4882a593Smuzhiyun# Given the -d argument and a filename/filenames, returns the modules imported by those files.
8*4882a593Smuzhiyun# Given the -d argument and a directory/directories, recurses to find all
9*4882a593Smuzhiyun# python packages and modules, returns the modules imported by these.
10*4882a593Smuzhiyun# Given the -p argument and a path or paths, scans that path for available python modules/packages.
11*4882a593Smuzhiyun
12*4882a593Smuzhiyunimport argparse
13*4882a593Smuzhiyunimport ast
14*4882a593Smuzhiyunimport importlib
15*4882a593Smuzhiyunfrom importlib import machinery
16*4882a593Smuzhiyunimport logging
17*4882a593Smuzhiyunimport os.path
18*4882a593Smuzhiyunimport sys
19*4882a593Smuzhiyun
20*4882a593Smuzhiyun
21*4882a593Smuzhiyunlogger = logging.getLogger('pythondeps')
22*4882a593Smuzhiyun
23*4882a593Smuzhiyunsuffixes = importlib.machinery.all_suffixes()
24*4882a593Smuzhiyun
25*4882a593Smuzhiyunclass PythonDepError(Exception):
26*4882a593Smuzhiyun    pass
27*4882a593Smuzhiyun
28*4882a593Smuzhiyun
29*4882a593Smuzhiyunclass DependError(PythonDepError):
30*4882a593Smuzhiyun    def __init__(self, path, error):
31*4882a593Smuzhiyun        self.path = path
32*4882a593Smuzhiyun        self.error = error
33*4882a593Smuzhiyun        PythonDepError.__init__(self, error)
34*4882a593Smuzhiyun
35*4882a593Smuzhiyun    def __str__(self):
36*4882a593Smuzhiyun        return "Failure determining dependencies of {}: {}".format(self.path, self.error)
37*4882a593Smuzhiyun
38*4882a593Smuzhiyun
39*4882a593Smuzhiyunclass ImportVisitor(ast.NodeVisitor):
40*4882a593Smuzhiyun    def __init__(self):
41*4882a593Smuzhiyun        self.imports = set()
42*4882a593Smuzhiyun        self.importsfrom = []
43*4882a593Smuzhiyun
44*4882a593Smuzhiyun    def visit_Import(self, node):
45*4882a593Smuzhiyun        for alias in node.names:
46*4882a593Smuzhiyun            self.imports.add(alias.name)
47*4882a593Smuzhiyun
48*4882a593Smuzhiyun    def visit_ImportFrom(self, node):
49*4882a593Smuzhiyun        self.importsfrom.append((node.module, [a.name for a in node.names], node.level))
50*4882a593Smuzhiyun
51*4882a593Smuzhiyun
52*4882a593Smuzhiyundef walk_up(path):
53*4882a593Smuzhiyun    while path:
54*4882a593Smuzhiyun        yield path
55*4882a593Smuzhiyun        path, _, _ = path.rpartition(os.sep)
56*4882a593Smuzhiyun
57*4882a593Smuzhiyun
58*4882a593Smuzhiyundef get_provides(path):
59*4882a593Smuzhiyun    path = os.path.realpath(path)
60*4882a593Smuzhiyun
61*4882a593Smuzhiyun    def get_fn_name(fn):
62*4882a593Smuzhiyun        for suffix in suffixes:
63*4882a593Smuzhiyun            if fn.endswith(suffix):
64*4882a593Smuzhiyun                return fn[:-len(suffix)]
65*4882a593Smuzhiyun
66*4882a593Smuzhiyun    isdir = os.path.isdir(path)
67*4882a593Smuzhiyun    if isdir:
68*4882a593Smuzhiyun        pkg_path = path
69*4882a593Smuzhiyun        walk_path = path
70*4882a593Smuzhiyun    else:
71*4882a593Smuzhiyun        pkg_path = get_fn_name(path)
72*4882a593Smuzhiyun        if pkg_path is None:
73*4882a593Smuzhiyun            return
74*4882a593Smuzhiyun        walk_path = os.path.dirname(path)
75*4882a593Smuzhiyun
76*4882a593Smuzhiyun    for curpath in walk_up(walk_path):
77*4882a593Smuzhiyun        if not os.path.exists(os.path.join(curpath, '__init__.py')):
78*4882a593Smuzhiyun            libdir = curpath
79*4882a593Smuzhiyun            break
80*4882a593Smuzhiyun    else:
81*4882a593Smuzhiyun        libdir = ''
82*4882a593Smuzhiyun
83*4882a593Smuzhiyun    package_relpath = pkg_path[len(libdir)+1:]
84*4882a593Smuzhiyun    package = '.'.join(package_relpath.split(os.sep))
85*4882a593Smuzhiyun    if not isdir:
86*4882a593Smuzhiyun        yield package, path
87*4882a593Smuzhiyun    else:
88*4882a593Smuzhiyun        if os.path.exists(os.path.join(path, '__init__.py')):
89*4882a593Smuzhiyun            yield package, path
90*4882a593Smuzhiyun
91*4882a593Smuzhiyun        for dirpath, dirnames, filenames in os.walk(path):
92*4882a593Smuzhiyun            relpath = dirpath[len(path)+1:]
93*4882a593Smuzhiyun            if relpath:
94*4882a593Smuzhiyun                if '__init__.py' not in filenames:
95*4882a593Smuzhiyun                    dirnames[:] = []
96*4882a593Smuzhiyun                    continue
97*4882a593Smuzhiyun                else:
98*4882a593Smuzhiyun                    context = '.'.join(relpath.split(os.sep))
99*4882a593Smuzhiyun                    if package:
100*4882a593Smuzhiyun                        context = package + '.' + context
101*4882a593Smuzhiyun                    yield context, dirpath
102*4882a593Smuzhiyun            else:
103*4882a593Smuzhiyun                context = package
104*4882a593Smuzhiyun
105*4882a593Smuzhiyun            for fn in filenames:
106*4882a593Smuzhiyun                adjusted_fn = get_fn_name(fn)
107*4882a593Smuzhiyun                if not adjusted_fn or adjusted_fn == '__init__':
108*4882a593Smuzhiyun                    continue
109*4882a593Smuzhiyun
110*4882a593Smuzhiyun                fullfn = os.path.join(dirpath, fn)
111*4882a593Smuzhiyun                if context:
112*4882a593Smuzhiyun                    yield context + '.' + adjusted_fn, fullfn
113*4882a593Smuzhiyun                else:
114*4882a593Smuzhiyun                    yield adjusted_fn, fullfn
115*4882a593Smuzhiyun
116*4882a593Smuzhiyun
117*4882a593Smuzhiyundef get_code_depends(code_string, path=None, provide=None, ispkg=False):
118*4882a593Smuzhiyun    try:
119*4882a593Smuzhiyun        code = ast.parse(code_string, path)
120*4882a593Smuzhiyun    except TypeError as exc:
121*4882a593Smuzhiyun        raise DependError(path, exc)
122*4882a593Smuzhiyun    except SyntaxError as exc:
123*4882a593Smuzhiyun        raise DependError(path, exc)
124*4882a593Smuzhiyun
125*4882a593Smuzhiyun    visitor = ImportVisitor()
126*4882a593Smuzhiyun    visitor.visit(code)
127*4882a593Smuzhiyun    for builtin_module in sys.builtin_module_names:
128*4882a593Smuzhiyun        if builtin_module in visitor.imports:
129*4882a593Smuzhiyun            visitor.imports.remove(builtin_module)
130*4882a593Smuzhiyun
131*4882a593Smuzhiyun    if provide:
132*4882a593Smuzhiyun        provide_elements = provide.split('.')
133*4882a593Smuzhiyun        if ispkg:
134*4882a593Smuzhiyun            provide_elements.append("__self__")
135*4882a593Smuzhiyun        context = '.'.join(provide_elements[:-1])
136*4882a593Smuzhiyun        package_path = os.path.dirname(path)
137*4882a593Smuzhiyun    else:
138*4882a593Smuzhiyun        context = None
139*4882a593Smuzhiyun        package_path = None
140*4882a593Smuzhiyun
141*4882a593Smuzhiyun    levelzero_importsfrom = (module for module, names, level in visitor.importsfrom
142*4882a593Smuzhiyun                             if level == 0)
143*4882a593Smuzhiyun    for module in visitor.imports | set(levelzero_importsfrom):
144*4882a593Smuzhiyun        if context and path:
145*4882a593Smuzhiyun            module_basepath = os.path.join(package_path, module.replace('.', '/'))
146*4882a593Smuzhiyun            if os.path.exists(module_basepath):
147*4882a593Smuzhiyun                # Implicit relative import
148*4882a593Smuzhiyun                yield context + '.' + module, path
149*4882a593Smuzhiyun                continue
150*4882a593Smuzhiyun
151*4882a593Smuzhiyun            for suffix in suffixes:
152*4882a593Smuzhiyun                if os.path.exists(module_basepath + suffix):
153*4882a593Smuzhiyun                    # Implicit relative import
154*4882a593Smuzhiyun                    yield context + '.' + module, path
155*4882a593Smuzhiyun                    break
156*4882a593Smuzhiyun            else:
157*4882a593Smuzhiyun                yield module, path
158*4882a593Smuzhiyun        else:
159*4882a593Smuzhiyun            yield module, path
160*4882a593Smuzhiyun
161*4882a593Smuzhiyun    for module, names, level in visitor.importsfrom:
162*4882a593Smuzhiyun        if level == 0:
163*4882a593Smuzhiyun            continue
164*4882a593Smuzhiyun        elif not provide:
165*4882a593Smuzhiyun            raise DependError("Error: ImportFrom non-zero level outside of a package: {0}".format((module, names, level)), path)
166*4882a593Smuzhiyun        elif level > len(provide_elements):
167*4882a593Smuzhiyun            raise DependError("Error: ImportFrom level exceeds package depth: {0}".format((module, names, level)), path)
168*4882a593Smuzhiyun        else:
169*4882a593Smuzhiyun            context = '.'.join(provide_elements[:-level])
170*4882a593Smuzhiyun            if module:
171*4882a593Smuzhiyun                if context:
172*4882a593Smuzhiyun                    yield context + '.' + module, path
173*4882a593Smuzhiyun                else:
174*4882a593Smuzhiyun                    yield module, path
175*4882a593Smuzhiyun
176*4882a593Smuzhiyun
177*4882a593Smuzhiyundef get_file_depends(path):
178*4882a593Smuzhiyun    try:
179*4882a593Smuzhiyun        code_string = open(path, 'r').read()
180*4882a593Smuzhiyun    except (OSError, IOError) as exc:
181*4882a593Smuzhiyun        raise DependError(path, exc)
182*4882a593Smuzhiyun
183*4882a593Smuzhiyun    return get_code_depends(code_string, path)
184*4882a593Smuzhiyun
185*4882a593Smuzhiyun
186*4882a593Smuzhiyundef get_depends_recursive(directory):
187*4882a593Smuzhiyun    directory = os.path.realpath(directory)
188*4882a593Smuzhiyun
189*4882a593Smuzhiyun    provides = dict((v, k) for k, v in get_provides(directory))
190*4882a593Smuzhiyun    for filename, provide in provides.items():
191*4882a593Smuzhiyun        if os.path.isdir(filename):
192*4882a593Smuzhiyun            filename = os.path.join(filename, '__init__.py')
193*4882a593Smuzhiyun            ispkg = True
194*4882a593Smuzhiyun        elif not filename.endswith('.py'):
195*4882a593Smuzhiyun            continue
196*4882a593Smuzhiyun        else:
197*4882a593Smuzhiyun            ispkg = False
198*4882a593Smuzhiyun
199*4882a593Smuzhiyun        with open(filename, 'r') as f:
200*4882a593Smuzhiyun            source = f.read()
201*4882a593Smuzhiyun
202*4882a593Smuzhiyun        depends = get_code_depends(source, filename, provide, ispkg)
203*4882a593Smuzhiyun        for depend, by in depends:
204*4882a593Smuzhiyun            yield depend, by
205*4882a593Smuzhiyun
206*4882a593Smuzhiyun
207*4882a593Smuzhiyundef get_depends(path):
208*4882a593Smuzhiyun    if os.path.isdir(path):
209*4882a593Smuzhiyun        return get_depends_recursive(path)
210*4882a593Smuzhiyun    else:
211*4882a593Smuzhiyun        return get_file_depends(path)
212*4882a593Smuzhiyun
213*4882a593Smuzhiyun
214*4882a593Smuzhiyundef main():
215*4882a593Smuzhiyun    logging.basicConfig()
216*4882a593Smuzhiyun
217*4882a593Smuzhiyun    parser = argparse.ArgumentParser(description='Determine dependencies and provided packages for python scripts/modules')
218*4882a593Smuzhiyun    parser.add_argument('path', nargs='+', help='full path to content to be processed')
219*4882a593Smuzhiyun    group = parser.add_mutually_exclusive_group()
220*4882a593Smuzhiyun    group.add_argument('-p', '--provides', action='store_true',
221*4882a593Smuzhiyun                       help='given a path, display the provided python modules')
222*4882a593Smuzhiyun    group.add_argument('-d', '--depends', action='store_true',
223*4882a593Smuzhiyun                       help='given a filename, display the imported python modules')
224*4882a593Smuzhiyun
225*4882a593Smuzhiyun    args = parser.parse_args()
226*4882a593Smuzhiyun    if args.provides:
227*4882a593Smuzhiyun        modules = set()
228*4882a593Smuzhiyun        for path in args.path:
229*4882a593Smuzhiyun            for provide, fn in get_provides(path):
230*4882a593Smuzhiyun                modules.add(provide)
231*4882a593Smuzhiyun
232*4882a593Smuzhiyun        for module in sorted(modules):
233*4882a593Smuzhiyun            print(module)
234*4882a593Smuzhiyun    elif args.depends:
235*4882a593Smuzhiyun        for path in args.path:
236*4882a593Smuzhiyun            try:
237*4882a593Smuzhiyun                modules = get_depends(path)
238*4882a593Smuzhiyun            except PythonDepError as exc:
239*4882a593Smuzhiyun                logger.error(str(exc))
240*4882a593Smuzhiyun                sys.exit(1)
241*4882a593Smuzhiyun
242*4882a593Smuzhiyun            for module, imp_by in modules:
243*4882a593Smuzhiyun                print("{}\t{}".format(module, imp_by))
244*4882a593Smuzhiyun    else:
245*4882a593Smuzhiyun        parser.print_help()
246*4882a593Smuzhiyun        sys.exit(2)
247*4882a593Smuzhiyun
248*4882a593Smuzhiyun
249*4882a593Smuzhiyunif __name__ == '__main__':
250*4882a593Smuzhiyun    main()
251