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