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