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