| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 1 | # encoding: utf-8 |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 2 | import atexit |
| 3 | import zipfile |
| 4 | |
| Tor Norbye | 809cb3e | 2014-01-27 09:36:41 -0800 | [diff] [blame] | 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 | |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 10 | debug_mode = False |
| 11 | |
| 12 | |
| Tor Norbye | 8668e1b | 2013-12-20 09:14:04 -0800 | [diff] [blame] | 13 | def redo_module(module_name, outfile, module_file_name, doing_builtins): |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 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 |
| Tor Norbye | 8668e1b | 2013-12-20 09:14:04 -0800 | [diff] [blame] | 17 | mod = sys.modules.get(module_name) |
| 18 | mod_path = module_name.split('.') |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 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 |
| Tor Norbye | 8668e1b | 2013-12-20 09:14:04 -0800 | [diff] [blame] | 29 | report("Failed to find CLR module " + module_name) |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 30 | break |
| 31 | if mod: |
| 32 | action("restoring") |
| 33 | r = ModuleRedeclarator(mod, outfile, module_file_name, doing_builtins=doing_builtins) |
| Tor Norbye | 8668e1b | 2013-12-20 09:14:04 -0800 | [diff] [blame] | 34 | r.redo(module_name, ".".join(mod_path[:-1]) in MODULES_INSPECT_DIR) |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 35 | action("flushing") |
| 36 | r.flush() |
| 37 | else: |
| Tor Norbye | 8668e1b | 2013-12-20 09:14:04 -0800 | [diff] [blame] | 38 | report("Failed to find imported module in sys.modules " + module_name) |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 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 | |
| Tor Norbye | 8668e1b | 2013-12-20 09:14:04 -0800 | [diff] [blame] | 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 | |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 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 |
| Tor Norbye | 8668e1b | 2013-12-20 09:14:04 -0800 | [diff] [blame] | 126 | for root, files in walk_python_path(path): |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 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 | |
| Tor Norbye | 8668e1b | 2013-12-20 09:14:04 -0800 | [diff] [blame] | 161 | for root, files in walk_python_path(path): |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 162 | for name in files: |
| 163 | if name.endswith('.py'): |
| 164 | file_path = os.path.join(root, name) |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 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") |
| Tor Norbye | 809cb3e | 2014-01-27 09:36:41 -0800 | [diff] [blame] | 238 | |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 239 | try: |
| Tor Norbye | 809cb3e | 2014-01-27 09:36:41 -0800 | [diff] [blame] | 240 | fname = build_output_name(subdir, name) |
| 241 | action("opening %r", fname) |
| 242 | old_modules = list(sys.modules.keys()) |
| 243 | imported_module_names = [] |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 244 | |
| Tor Norbye | 809cb3e | 2014-01-27 09:36:41 -0800 | [diff] [blame] | 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 |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 251 | |
| Tor Norbye | 809cb3e | 2014-01-27 09:36:41 -0800 | [diff] [blame] | 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 |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 258 | |
| Tor Norbye | 809cb3e | 2014-01-27 09:36:41 -0800 | [diff] [blame] | 259 | action("importing") |
| 260 | __import__(name) # sys.modules will fill up with what we want |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 261 | |
| Tor Norbye | 809cb3e | 2014-01-27 09:36:41 -0800 | [diff] [blame] | 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] |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 266 | |
| Tor Norbye | 809cb3e | 2014-01-27 09:36:41 -0800 | [diff] [blame] | 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 |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 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()) |