blob: 5be1df88a1aafb794c5d25063cf9fd8f610f20fb [file] [log] [blame]
Tor Norbye3a2425a2013-11-04 10:16:08 -08001# encoding: utf-8
Tor Norbye3a2425a2013-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 Norbye3a2425a2013-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 Norbye3a2425a2013-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 Norbye3a2425a2013-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 Norbye3a2425a2013-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 Norbye3a2425a2013-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 Norbye3a2425a2013-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 Norbye3a2425a2013-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 Norbye3a2425a2013-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 Norbye8668e1b2013-12-20 09:14:04 -0800164 for root, files in walk_python_path(path):
Tor Norbye3a2425a2013-11-04 10:16:08 -0800165 for name in files:
166 if name.endswith('.py'):
167 file_path = os.path.join(root, name)
Tor Norbye3a2425a2013-11-04 10:16:08 -0800168 say("%s\t%s\t%d", os.path.normpath(file_path), path, os.path.getsize(file_path))
169 say('END')
170 sys.stdout.flush()
171 except:
172 import traceback
173
174 traceback.print_exc()
175 sys.exit(1)
176
177
178#noinspection PyBroadException
179def zip_sources(zip_path):
180 if not os.path.exists(zip_path):
181 os.makedirs(zip_path)
182
183 zip_filename = os.path.normpath(os.path.sep.join([zip_path, "skeletons.zip"]))
184
185 try:
186 zip = zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED)
187 except:
188 zip = zipfile.ZipFile(zip_filename, 'w')
189
190 try:
191 try:
192 while True:
193 line = sys.stdin.readline()
194 line = line.strip()
195
196 if line == '-':
197 break
198
199 if line:
200 # This line will break the split:
201 # /.../dist-packages/setuptools/script template (dev).py setuptools/script template (dev).py
202 split_items = line.split()
203 if len(split_items) > 2:
204 match_two_files = re.match(r'^(.+\.py)\s+(.+\.py)$', line)
205 if not match_two_files:
206 report("Error(zip_sources): invalid line '%s'" % line)
207 continue
208 split_items = match_two_files.group(1, 2)
209 (path, arcpath) = split_items
210 zip.write(path, arcpath)
211 else:
212 # busy waiting for input from PyCharm...
213 time.sleep(0.10)
214 say('OK: ' + zip_filename)
215 sys.stdout.flush()
216 except:
217 import traceback
218
219 traceback.print_exc()
220 say('Error creating archive.')
221
222 sys.exit(1)
223 finally:
224 zip.close()
225
226
227# command-line interface
228#noinspection PyBroadException
229def process_one(name, mod_file_name, doing_builtins, subdir):
230 """
231 Processes a single module named name defined in file_name (autodetect if not given).
232 Returns True on success.
233 """
234 if has_regular_python_ext(name):
235 report("Ignored a regular Python file %r", name)
236 return True
237 if not quiet:
238 say(name)
239 sys.stdout.flush()
240 action("doing nothing")
Tor Norbye809cb3e2014-01-27 09:36:41 -0800241
Tor Norbye3a2425a2013-11-04 10:16:08 -0800242 try:
Tor Norbye809cb3e2014-01-27 09:36:41 -0800243 fname = build_output_name(subdir, name)
244 action("opening %r", fname)
245 old_modules = list(sys.modules.keys())
246 imported_module_names = []
Tor Norbye3a2425a2013-11-04 10:16:08 -0800247
Tor Norbye809cb3e2014-01-27 09:36:41 -0800248 class MyFinder:
249 #noinspection PyMethodMayBeStatic
250 def find_module(self, fullname, path=None):
251 if fullname != name:
252 imported_module_names.append(fullname)
253 return None
Tor Norbye3a2425a2013-11-04 10:16:08 -0800254
Tor Norbye809cb3e2014-01-27 09:36:41 -0800255 my_finder = None
256 if hasattr(sys, 'meta_path'):
257 my_finder = MyFinder()
258 sys.meta_path.append(my_finder)
259 else:
260 imported_module_names = None
Tor Norbye3a2425a2013-11-04 10:16:08 -0800261
Tor Norbye809cb3e2014-01-27 09:36:41 -0800262 action("importing")
263 __import__(name) # sys.modules will fill up with what we want
Tor Norbye3a2425a2013-11-04 10:16:08 -0800264
Tor Norbye809cb3e2014-01-27 09:36:41 -0800265 if my_finder:
266 sys.meta_path.remove(my_finder)
267 if imported_module_names is None:
268 imported_module_names = [m for m in sys.modules.keys() if m not in old_modules]
Tor Norbye3a2425a2013-11-04 10:16:08 -0800269
Tor Norbye809cb3e2014-01-27 09:36:41 -0800270 redo_module(name, fname, mod_file_name, doing_builtins)
271 # The C library may have called Py_InitModule() multiple times to define several modules (gtk._gtk and gtk.gdk);
272 # restore all of them
273 path = name.split(".")
274 redo_imports = not ".".join(path[:-1]) in MODULES_INSPECT_DIR
275 if imported_module_names and redo_imports:
276 for m in sys.modules.keys():
277 if m.startswith("pycharm_generator_utils"): continue
278 action("looking at possible submodule %r", m)
279 # if module has __file__ defined, it has Python source code and doesn't need a skeleton
280 if m not in old_modules and m not in imported_module_names and m != name and not hasattr(
281 sys.modules[m], '__file__'):
282 if not quiet:
283 say(m)
284 sys.stdout.flush()
285 fname = build_output_name(subdir, m)
286 action("opening %r", fname)
287 try:
288 redo_module(m, fname, mod_file_name, doing_builtins)
289 finally:
290 action("closing %r", fname)
291 except:
292 exctype, value = sys.exc_info()[:2]
293 msg = "Failed to process %r while %s: %s"
294 args = name, CURRENT_ACTION, str(value)
295 report(msg, *args)
296 if debug_mode:
297 if sys.platform == 'cli':
298 import traceback
299 traceback.print_exc(file=sys.stderr)
300 raise
301 return False
Tor Norbye3a2425a2013-11-04 10:16:08 -0800302 return True
303
304
305def get_help_text():
306 return (
307 #01234567890123456789012345678901234567890123456789012345678901234567890123456789
308 'Generates interface skeletons for python modules.' '\n'
309 'Usage: ' '\n'
310 ' generator [options] [module_name [file_name]]' '\n'
311 ' generator [options] -L ' '\n'
312 'module_name is fully qualified, and file_name is where the module is defined.' '\n'
313 'E.g. foo.bar /usr/lib/python/foo_bar.so' '\n'
314 'For built-in modules file_name is not provided.' '\n'
315 'Output files will be named as modules plus ".py" suffix.' '\n'
316 'Normally every name processed will be printed and stdout flushed.' '\n'
317 'directory_list is one string separated by OS-specific path separtors.' '\n'
318 '\n'
319 'Options are:' '\n'
320 ' -h -- prints this help message.' '\n'
321 ' -d dir -- output dir, must be writable. If not given, current dir is used.' '\n'
322 ' -b -- use names from sys.builtin_module_names' '\n'
323 ' -q -- quiet, do not print anything on stdout. Errors still go to stderr.' '\n'
324 ' -x -- die on exceptions with a stacktrace; only for debugging.' '\n'
325 ' -v -- be verbose, print lots of debug output to stderr' '\n'
326 ' -c modules -- import CLR assemblies with specified names' '\n'
327 ' -p -- run CLR profiler ' '\n'
328 ' -s path_list -- add paths to sys.path before run; path_list lists directories' '\n'
329 ' separated by path separator char, e.g. "c:\\foo;d:\\bar;c:\\with space"' '\n'
330 ' -L -- print version and then a list of binary module files found ' '\n'
331 ' on sys.path and in directories in directory_list;' '\n'
332 ' lines are "qualified.module.name /full/path/to/module_file.{pyd,dll,so}"' '\n'
333 ' -S -- lists all python sources found in sys.path and in directories in directory_list\n'
334 ' -z archive_name -- zip files to archive_name. Accepts files to be archived from stdin in format <filepath> <name in archive>'
335 )
336
337
338if __name__ == "__main__":
339 from getopt import getopt
340
341 helptext = get_help_text()
342 opts, args = getopt(sys.argv[1:], "d:hbqxvc:ps:LSz")
343 opts = dict(opts)
344
345 quiet = '-q' in opts
346 _is_verbose = '-v' in opts
347 subdir = opts.get('-d', '')
348
349 if not opts or '-h' in opts:
350 say(helptext)
351 sys.exit(0)
352
353 if '-L' not in opts and '-b' not in opts and '-S' not in opts and not args:
354 report("Neither -L nor -b nor -S nor any module name given")
355 sys.exit(1)
356
357 if "-x" in opts:
358 debug_mode = True
359
360 # patch sys.path?
361 extra_path = opts.get('-s', None)
362 if extra_path:
363 source_dirs = extra_path.split(os.path.pathsep)
364 for p in source_dirs:
365 if p and p not in sys.path:
366 sys.path.append(p) # we need this to make things in additional dirs importable
367 note("Altered sys.path: %r", sys.path)
368
369 # find binaries?
370 if "-L" in opts:
371 if len(args) > 0:
372 report("Expected no args with -L, got %d args", len(args))
373 sys.exit(1)
374 say(VERSION)
375 results = list(list_binaries(sys.path))
376 results.sort()
377 for name, path, size, last_modified in results:
378 say("%s\t%s\t%d\t%d", name, path, size, last_modified)
379 sys.exit(0)
380
381 if "-S" in opts:
382 if len(args) > 0:
383 report("Expected no args with -S, got %d args", len(args))
384 sys.exit(1)
385 say(VERSION)
386 list_sources(sys.path)
387 sys.exit(0)
388
389 if "-z" in opts:
390 if len(args) != 1:
391 report("Expected 1 arg with -S, got %d args", len(args))
392 sys.exit(1)
393 zip_sources(args[0])
394 sys.exit(0)
395
396 # build skeleton(s)
397
398 timer = Timer()
399 # determine names
400 if '-b' in opts:
401 if args:
402 report("No names should be specified with -b")
403 sys.exit(1)
404 names = list(sys.builtin_module_names)
405 if not BUILTIN_MOD_NAME in names:
406 names.append(BUILTIN_MOD_NAME)
407 if '__main__' in names:
408 names.remove('__main__') # we don't want ourselves processed
409 ok = True
410 for name in names:
411 ok = process_one(name, None, True, subdir) and ok
412 if not ok:
413 sys.exit(1)
414
415 else:
416 if len(args) > 2:
417 report("Only module_name or module_name and file_name should be specified; got %d args", len(args))
418 sys.exit(1)
419 name = args[0]
420 if len(args) == 2:
421 mod_file_name = args[1]
422 else:
423 mod_file_name = None
424
425 if sys.platform == 'cli':
426 #noinspection PyUnresolvedReferences
427 import clr
428
429 refs = opts.get('-c', '')
430 if refs:
431 for ref in refs.split(';'): clr.AddReferenceByPartialName(ref)
432
433 if '-p' in opts:
434 atexit.register(print_profile)
435
Tor Norbye92584642014-04-17 08:39:25 -0700436 # We take module name from import statement
437 name = get_namespace_by_name(name)
438
Tor Norbye3a2425a2013-11-04 10:16:08 -0800439 if not process_one(name, mod_file_name, False, subdir):
440 sys.exit(1)
441
442 say("Generation completed in %d ms", timer.elapsed())