performance for generator: Split big generated modules (like _Gtk, PyQt) into smaller...
[idea/community.git] / python / helpers / generator3.py
1 # encoding: utf-8
2 import atexit
3 import zipfile
4
5 from pycharm_generator_utils.module_redeclarator import *
6 from pycharm_generator_utils.util_methods import *
7 from pycharm_generator_utils.constants import *
8
9
10 debug_mode = False
11
12
13 def redo_module(module_name, outfile, module_file_name, doing_builtins):
14     # gobject does 'del _gobject' in its __init__.py, so the chained attribute lookup code
15     # fails to find 'gobject._gobject'. thus we need to pull the module directly out of
16     # sys.modules
17     mod = sys.modules.get(module_name)
18     mod_path = module_name.split('.')
19     if not mod and sys.platform == 'cli':
20         # "import System.Collections" in IronPython 2.7 doesn't actually put System.Collections in sys.modules
21         # instead, sys.modules['System'] get set to a Microsoft.Scripting.Actions.NamespaceTracker and Collections can be
22         # accessed as its attribute
23         mod = sys.modules[mod_path[0]]
24         for component in mod_path[1:]:
25             try:
26                 mod = getattr(mod, component)
27             except AttributeError:
28                 mod = None
29                 report("Failed to find CLR module " + module_name)
30                 break
31     if mod:
32         action("restoring")
33         r = ModuleRedeclarator(mod, outfile, module_file_name, doing_builtins=doing_builtins)
34         r.redo(module_name, ".".join(mod_path[:-1]) in MODULES_INSPECT_DIR)
35         action("flushing")
36         r.flush()
37     else:
38         report("Failed to find imported module in sys.modules " + module_name)
39
40 # find_binaries functionality
41 def cut_binary_lib_suffix(path, f):
42     """
43     @param path where f lives
44     @param f file name of a possible binary lib file (no path)
45     @return f without a binary suffix (that is, an importable name) if path+f is indeed a binary lib, or None.
46     Note: if for .pyc or .pyo file a .py is found, None is returned.
47     """
48     if not f.endswith(".pyc") and not f.endswith(".typelib") and not f.endswith(".pyo") and not f.endswith(".so") and not f.endswith(".pyd"):
49         return None
50     ret = None
51     match = BIN_MODULE_FNAME_PAT.match(f)
52     if match:
53         ret = match.group(1)
54         modlen = len('module')
55         retlen = len(ret)
56         if ret.endswith('module') and retlen > modlen and f.endswith('.so'):   # what for?
57             ret = ret[:(retlen - modlen)]
58     if f.endswith('.pyc') or f.endswith('.pyo'):
59         fullname = os.path.join(path, f[:-1]) # check for __pycache__ is made outside
60         if os.path.exists(fullname):
61             ret = None
62     pat_match = TYPELIB_MODULE_FNAME_PAT.match(f)
63     if pat_match:
64         ret = "gi.repository." + pat_match.group(1)
65     return ret
66
67
68 def is_posix_skipped_module(path, f):
69     if os.name == 'posix':
70         name = os.path.join(path, f)
71         for mod in POSIX_SKIP_MODULES:
72             if name.endswith(mod):
73                 return True
74     return False
75
76
77 def is_mac_skipped_module(path, f):
78     fullname = os.path.join(path, f)
79     m = MAC_STDLIB_PATTERN.match(fullname)
80     if not m: return 0
81     relpath = m.group(2)
82     for module in MAC_SKIP_MODULES:
83         if relpath.startswith(module): return 1
84     return 0
85
86
87 def is_skipped_module(path, f):
88     return is_mac_skipped_module(path, f) or is_posix_skipped_module(path, f[:f.rindex('.')]) or 'pynestkernel' in f
89
90
91 def is_module(d, root):
92     return (os.path.exists(os.path.join(root, d, "__init__.py")) or
93             os.path.exists(os.path.join(root, d, "__init__.pyc")) or
94             os.path.exists(os.path.join(root, d, "__init__.pyo")))
95
96
97 def walk_python_path(path):
98     for root, dirs, files in os.walk(path):
99         if root.endswith('__pycache__'):
100             continue
101         dirs_copy = list(dirs)
102         for d in dirs_copy:
103             if d.endswith('__pycache__') or not is_module(d, root):
104                 dirs.remove(d)
105         # some files show up but are actually non-existent symlinks
106         yield root, [f for f in files if os.path.exists(os.path.join(root, f))]
107
108
109 def list_binaries(paths):
110     """
111     Finds binaries in the given list of paths.
112     Understands nested paths, as sys.paths have it (both "a/b" and "a/b/c").
113     Tries to be case-insensitive, but case-preserving.
114     @param paths: list of paths.
115     @return: dict[module_name, full_path]
116     """
117     SEP = os.path.sep
118     res = {} # {name.upper(): (name, full_path)} # b/c windows is case-oblivious
119     if not paths:
120         return {}
121     if IS_JAVA: # jython can't have binary modules
122         return {}
123     paths = sorted_no_case(paths)
124     for path in paths:
125         if path == os.path.dirname(sys.argv[0]): continue
126         for root, files in walk_python_path(path):
127             cutpoint = path.rfind(SEP)
128             if cutpoint > 0:
129                 preprefix = path[(cutpoint + len(SEP)):] + '.'
130             else:
131                 preprefix = ''
132             prefix = root[(len(path) + len(SEP)):].replace(SEP, '.')
133             if prefix:
134                 prefix += '.'
135             note("root: %s path: %s prefix: %s preprefix: %s", root, path, prefix, preprefix)
136             for f in files:
137                 name = cut_binary_lib_suffix(root, f)
138                 if name and not is_skipped_module(root, f):
139                     note("cutout: %s", name)
140                     if preprefix:
141                         note("prefixes: %s %s", prefix, preprefix)
142                         pre_name = (preprefix + prefix + name).upper()
143                         if pre_name in res:
144                             res.pop(pre_name) # there might be a dupe, if paths got both a/b and a/b/c
145                         note("done with %s", name)
146                     the_name = prefix + name
147                     file_path = os.path.join(root, f)
148
149                     res[the_name.upper()] = (the_name, file_path, os.path.getsize(file_path), int(os.stat(file_path).st_mtime))
150     return list(res.values())
151
152
153 def list_sources(paths):
154     #noinspection PyBroadException
155     try:
156         for path in paths:
157             if path == os.path.dirname(sys.argv[0]): continue
158
159             path = os.path.normpath(path)
160
161             for root, files in walk_python_path(path):
162                 for name in files:
163                     if name.endswith('.py'):
164                         file_path = os.path.join(root, name)
165                         say("%s\t%s\t%d", os.path.normpath(file_path), path, os.path.getsize(file_path))
166         say('END')
167         sys.stdout.flush()
168     except:
169         import traceback
170
171         traceback.print_exc()
172         sys.exit(1)
173
174
175 #noinspection PyBroadException
176 def zip_sources(zip_path):
177     if not os.path.exists(zip_path):
178         os.makedirs(zip_path)
179
180     zip_filename = os.path.normpath(os.path.sep.join([zip_path, "skeletons.zip"]))
181
182     try:
183         zip = zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED)
184     except:
185         zip = zipfile.ZipFile(zip_filename, 'w')
186
187     try:
188         try:
189             while True:
190                 line = sys.stdin.readline()
191                 line = line.strip()
192
193                 if line == '-':
194                     break
195
196                 if line:
197                     # This line will break the split:
198                     # /.../dist-packages/setuptools/script template (dev).py setuptools/script template (dev).py
199                     split_items = line.split()
200                     if len(split_items) > 2:
201                         match_two_files = re.match(r'^(.+\.py)\s+(.+\.py)$', line)
202                         if not match_two_files:
203                             report("Error(zip_sources): invalid line '%s'" % line)
204                             continue
205                         split_items = match_two_files.group(1, 2)
206                     (path, arcpath) = split_items
207                     zip.write(path, arcpath)
208                 else:
209                     # busy waiting for input from PyCharm...
210                     time.sleep(0.10)
211             say('OK: ' + zip_filename)
212             sys.stdout.flush()
213         except:
214             import traceback
215
216             traceback.print_exc()
217             say('Error creating archive.')
218
219             sys.exit(1)
220     finally:
221         zip.close()
222
223
224 # command-line interface
225 #noinspection PyBroadException
226 def process_one(name, mod_file_name, doing_builtins, subdir):
227     """
228     Processes a single module named name defined in file_name (autodetect if not given).
229     Returns True on success.
230     """
231     if has_regular_python_ext(name):
232         report("Ignored a regular Python file %r", name)
233         return True
234     if not quiet:
235         say(name)
236         sys.stdout.flush()
237     action("doing nothing")
238
239     try:
240         fname = build_output_name(subdir, name)
241         action("opening %r", fname)
242         old_modules = list(sys.modules.keys())
243         imported_module_names = []
244
245         class MyFinder:
246             #noinspection PyMethodMayBeStatic
247             def find_module(self, fullname, path=None):
248                 if fullname != name:
249                     imported_module_names.append(fullname)
250                 return None
251
252         my_finder = None
253         if hasattr(sys, 'meta_path'):
254             my_finder = MyFinder()
255             sys.meta_path.append(my_finder)
256         else:
257             imported_module_names = None
258
259         action("importing")
260         __import__(name) # sys.modules will fill up with what we want
261
262         if my_finder:
263             sys.meta_path.remove(my_finder)
264         if imported_module_names is None:
265             imported_module_names = [m for m in sys.modules.keys() if m not in old_modules]
266
267         redo_module(name, fname, mod_file_name, doing_builtins)
268         # The C library may have called Py_InitModule() multiple times to define several modules (gtk._gtk and gtk.gdk);
269         # restore all of them
270         path = name.split(".")
271         redo_imports = not ".".join(path[:-1]) in MODULES_INSPECT_DIR
272         if imported_module_names and redo_imports:
273             for m in sys.modules.keys():
274                 if m.startswith("pycharm_generator_utils"): continue
275                 action("looking at possible submodule %r", m)
276                 # if module has __file__ defined, it has Python source code and doesn't need a skeleton
277                 if m not in old_modules and m not in imported_module_names and m != name and not hasattr(
278                         sys.modules[m], '__file__'):
279                     if not quiet:
280                         say(m)
281                         sys.stdout.flush()
282                     fname = build_output_name(subdir, m)
283                     action("opening %r", fname)
284                     try:
285                         redo_module(m, fname, mod_file_name, doing_builtins)
286                     finally:
287                         action("closing %r", fname)
288     except:
289         exctype, value = sys.exc_info()[:2]
290         msg = "Failed to process %r while %s: %s"
291         args = name, CURRENT_ACTION, str(value)
292         report(msg, *args)
293         if debug_mode:
294             if sys.platform == 'cli':
295                 import traceback
296                 traceback.print_exc(file=sys.stderr)
297             raise
298         return False
299     return True
300
301
302 def get_help_text():
303     return (
304         #01234567890123456789012345678901234567890123456789012345678901234567890123456789
305         'Generates interface skeletons for python modules.' '\n'
306         'Usage: ' '\n'
307         '  generator [options] [module_name [file_name]]' '\n'
308         '  generator [options] -L ' '\n'
309         'module_name is fully qualified, and file_name is where the module is defined.' '\n'
310         'E.g. foo.bar /usr/lib/python/foo_bar.so' '\n'
311         'For built-in modules file_name is not provided.' '\n'
312         'Output files will be named as modules plus ".py" suffix.' '\n'
313         'Normally every name processed will be printed and stdout flushed.' '\n'
314         'directory_list is one string separated by OS-specific path separtors.' '\n'
315         '\n'
316         'Options are:' '\n'
317         ' -h -- prints this help message.' '\n'
318         ' -d dir -- output dir, must be writable. If not given, current dir is used.' '\n'
319         ' -b -- use names from sys.builtin_module_names' '\n'
320         ' -q -- quiet, do not print anything on stdout. Errors still go to stderr.' '\n'
321         ' -x -- die on exceptions with a stacktrace; only for debugging.' '\n'
322         ' -v -- be verbose, print lots of debug output to stderr' '\n'
323         ' -c modules -- import CLR assemblies with specified names' '\n'
324         ' -p -- run CLR profiler ' '\n'
325         ' -s path_list -- add paths to sys.path before run; path_list lists directories' '\n'
326         '    separated by path separator char, e.g. "c:\\foo;d:\\bar;c:\\with space"' '\n'
327         ' -L -- print version and then a list of binary module files found ' '\n'
328         '    on sys.path and in directories in directory_list;' '\n'
329         '    lines are "qualified.module.name /full/path/to/module_file.{pyd,dll,so}"' '\n'
330         ' -S -- lists all python sources found in sys.path and in directories in directory_list\n'
331         ' -z archive_name -- zip files to archive_name. Accepts files to be archived from stdin in format <filepath> <name in archive>'
332     )
333
334
335 if __name__ == "__main__":
336     from getopt import getopt
337
338     helptext = get_help_text()
339     opts, args = getopt(sys.argv[1:], "d:hbqxvc:ps:LSz")
340     opts = dict(opts)
341
342     quiet = '-q' in opts
343     _is_verbose = '-v' in opts
344     subdir = opts.get('-d', '')
345
346     if not opts or '-h' in opts:
347         say(helptext)
348         sys.exit(0)
349
350     if '-L' not in opts and '-b' not in opts and '-S' not in opts and not args:
351         report("Neither -L nor -b nor -S nor any module name given")
352         sys.exit(1)
353
354     if "-x" in opts:
355         debug_mode = True
356
357     # patch sys.path?
358     extra_path = opts.get('-s', None)
359     if extra_path:
360         source_dirs = extra_path.split(os.path.pathsep)
361         for p in source_dirs:
362             if p and p not in sys.path:
363                 sys.path.append(p) # we need this to make things in additional dirs importable
364         note("Altered sys.path: %r", sys.path)
365
366     # find binaries?
367     if "-L" in opts:
368         if len(args) > 0:
369             report("Expected no args with -L, got %d args", len(args))
370             sys.exit(1)
371         say(VERSION)
372         results = list(list_binaries(sys.path))
373         results.sort()
374         for name, path, size, last_modified in results:
375             say("%s\t%s\t%d\t%d", name, path, size, last_modified)
376         sys.exit(0)
377
378     if "-S" in opts:
379         if len(args) > 0:
380             report("Expected no args with -S, got %d args", len(args))
381             sys.exit(1)
382         say(VERSION)
383         list_sources(sys.path)
384         sys.exit(0)
385
386     if "-z" in opts:
387         if len(args) != 1:
388             report("Expected 1 arg with -S, got %d args", len(args))
389             sys.exit(1)
390         zip_sources(args[0])
391         sys.exit(0)
392
393     # build skeleton(s)
394
395     timer = Timer()
396     # determine names
397     if '-b' in opts:
398         if args:
399             report("No names should be specified with -b")
400             sys.exit(1)
401         names = list(sys.builtin_module_names)
402         if not BUILTIN_MOD_NAME in names:
403             names.append(BUILTIN_MOD_NAME)
404         if '__main__' in names:
405             names.remove('__main__') # we don't want ourselves processed
406         ok = True
407         for name in names:
408             ok = process_one(name, None, True, subdir) and ok
409         if not ok:
410             sys.exit(1)
411
412     else:
413         if len(args) > 2:
414             report("Only module_name or module_name and file_name should be specified; got %d args", len(args))
415             sys.exit(1)
416         name = args[0]
417         if len(args) == 2:
418             mod_file_name = args[1]
419         else:
420             mod_file_name = None
421
422         if sys.platform == 'cli':
423             #noinspection PyUnresolvedReferences
424             import clr
425
426             refs = opts.get('-c', '')
427             if refs:
428                 for ref in refs.split(';'): clr.AddReferenceByPartialName(ref)
429
430             if '-p' in opts:
431                 atexit.register(print_profile)
432
433         if not process_one(name, mod_file_name, False, subdir):
434             sys.exit(1)
435
436     say("Generation completed in %d ms", timer.elapsed())