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