OLD | NEW |
(Empty) | |
| 1 # -*- coding: utf-8 -*- |
| 2 """ |
| 3 past.translation |
| 4 ================== |
| 5 |
| 6 The ``past.translation`` package provides an import hook for Python 3 which |
| 7 transparently runs ``futurize`` fixers over Python 2 code on import to convert |
| 8 print statements into functions, etc. |
| 9 |
| 10 It is intended to assist users in migrating to Python 3.x even if some |
| 11 dependencies still only support Python 2.x. |
| 12 |
| 13 Usage |
| 14 ----- |
| 15 |
| 16 Once your Py2 package is installed in the usual module search path, the import |
| 17 hook is invoked as follows: |
| 18 |
| 19 >>> from past import autotranslate |
| 20 >>> autotranslate('mypackagename') |
| 21 |
| 22 Or: |
| 23 |
| 24 >>> autotranslate(['mypackage1', 'mypackage2']) |
| 25 |
| 26 You can unregister the hook using:: |
| 27 |
| 28 >>> from past.translation import remove_hooks |
| 29 >>> remove_hooks() |
| 30 |
| 31 Author: Ed Schofield. |
| 32 Inspired by and based on ``uprefix`` by Vinay M. Sajip. |
| 33 """ |
| 34 |
| 35 import imp |
| 36 import logging |
| 37 import marshal |
| 38 import os |
| 39 import sys |
| 40 import copy |
| 41 from lib2to3.pgen2.parse import ParseError |
| 42 from lib2to3.refactor import RefactoringTool |
| 43 |
| 44 from libfuturize import fixes |
| 45 |
| 46 |
| 47 logger = logging.getLogger(__name__) |
| 48 logger.setLevel(logging.DEBUG) |
| 49 |
| 50 myfixes = (list(fixes.libfuturize_fix_names_stage1) + |
| 51 list(fixes.lib2to3_fix_names_stage1) + |
| 52 list(fixes.libfuturize_fix_names_stage2) + |
| 53 list(fixes.lib2to3_fix_names_stage2)) |
| 54 |
| 55 |
| 56 # We detect whether the code is Py2 or Py3 by applying certain lib2to3 fixers |
| 57 # to it. If the diff is empty, it's Python 3 code. |
| 58 |
| 59 py2_detect_fixers = [ |
| 60 # From stage 1: |
| 61 'lib2to3.fixes.fix_apply', |
| 62 # 'lib2to3.fixes.fix_dict', # TODO: add support for utils.viewitems()
etc. and move to stage2 |
| 63 'lib2to3.fixes.fix_except', |
| 64 'lib2to3.fixes.fix_execfile', |
| 65 'lib2to3.fixes.fix_exitfunc', |
| 66 'lib2to3.fixes.fix_funcattrs', |
| 67 'lib2to3.fixes.fix_filter', |
| 68 'lib2to3.fixes.fix_has_key', |
| 69 'lib2to3.fixes.fix_idioms', |
| 70 'lib2to3.fixes.fix_import', # makes any implicit relative imports explici
t. (Use with ``from __future__ import absolute_import) |
| 71 'lib2to3.fixes.fix_intern', |
| 72 'lib2to3.fixes.fix_isinstance', |
| 73 'lib2to3.fixes.fix_methodattrs', |
| 74 'lib2to3.fixes.fix_ne', |
| 75 'lib2to3.fixes.fix_numliterals', # turns 1L into 1, 0755 into 0o755 |
| 76 'lib2to3.fixes.fix_paren', |
| 77 'lib2to3.fixes.fix_print', |
| 78 'lib2to3.fixes.fix_raise', # uses incompatible with_traceback() method on
exceptions |
| 79 'lib2to3.fixes.fix_renames', |
| 80 'lib2to3.fixes.fix_reduce', |
| 81 # 'lib2to3.fixes.fix_set_literal', # this is unnecessary and breaks Py2.6 s
upport |
| 82 'lib2to3.fixes.fix_repr', |
| 83 'lib2to3.fixes.fix_standarderror', |
| 84 'lib2to3.fixes.fix_sys_exc', |
| 85 'lib2to3.fixes.fix_throw', |
| 86 'lib2to3.fixes.fix_tuple_params', |
| 87 'lib2to3.fixes.fix_types', |
| 88 'lib2to3.fixes.fix_ws_comma', |
| 89 'lib2to3.fixes.fix_xreadlines', |
| 90 |
| 91 # From stage 2: |
| 92 'lib2to3.fixes.fix_basestring', |
| 93 # 'lib2to3.fixes.fix_buffer', # perhaps not safe. Test this. |
| 94 # 'lib2to3.fixes.fix_callable', # not needed in Py3.2+ |
| 95 # 'lib2to3.fixes.fix_dict', # TODO: add support for utils.viewitems()
etc. |
| 96 'lib2to3.fixes.fix_exec', |
| 97 # 'lib2to3.fixes.fix_future', # we don't want to remove __future__ import
s |
| 98 'lib2to3.fixes.fix_getcwdu', |
| 99 # 'lib2to3.fixes.fix_imports', # called by libfuturize.fixes.fix_future_st
andard_library |
| 100 # 'lib2to3.fixes.fix_imports2', # we don't handle this yet (dbm) |
| 101 # 'lib2to3.fixes.fix_input', |
| 102 # 'lib2to3.fixes.fix_itertools', |
| 103 # 'lib2to3.fixes.fix_itertools_imports', |
| 104 'lib2to3.fixes.fix_long', |
| 105 # 'lib2to3.fixes.fix_map', |
| 106 # 'lib2to3.fixes.fix_metaclass', # causes SyntaxError in Py2! Use the one fr
om ``six`` instead |
| 107 'lib2to3.fixes.fix_next', |
| 108 'lib2to3.fixes.fix_nonzero', # TODO: add a decorator for mapping __bool_
_ to __nonzero__ |
| 109 # 'lib2to3.fixes.fix_operator', # we will need support for this by e.g. e
xtending the Py2 operator module to provide those functions in Py3 |
| 110 'lib2to3.fixes.fix_raw_input', |
| 111 # 'lib2to3.fixes.fix_unicode', # strips off the u'' prefix, which removes
a potentially helpful source of information for disambiguating unicode/byte stri
ngs |
| 112 # 'lib2to3.fixes.fix_urllib', |
| 113 'lib2to3.fixes.fix_xrange', |
| 114 # 'lib2to3.fixes.fix_zip', |
| 115 ] |
| 116 |
| 117 |
| 118 class RTs: |
| 119 """ |
| 120 A namespace for the refactoring tools. This avoids creating these at |
| 121 the module level, which slows down the module import. (See issue #117). |
| 122 |
| 123 There are two possible grammars: with or without the print statement. |
| 124 Hence we have two possible refactoring tool implementations. |
| 125 """ |
| 126 _rt = None |
| 127 _rtp = None |
| 128 _rt_py2_detect = None |
| 129 _rtp_py2_detect = None |
| 130 |
| 131 @staticmethod |
| 132 def setup(): |
| 133 """ |
| 134 Call this before using the refactoring tools to create them on demand |
| 135 if needed. |
| 136 """ |
| 137 if None in [RTs._rt, RTs._rtp]: |
| 138 RTs._rt = RefactoringTool(myfixes) |
| 139 RTs._rtp = RefactoringTool(myfixes, {'print_function': True}) |
| 140 |
| 141 |
| 142 @staticmethod |
| 143 def setup_detect_python2(): |
| 144 """ |
| 145 Call this before using the refactoring tools to create them on demand |
| 146 if needed. |
| 147 """ |
| 148 if None in [RTs._rt_py2_detect, RTs._rtp_py2_detect]: |
| 149 RTs._rt_py2_detect = RefactoringTool(py2_detect_fixers) |
| 150 RTs._rtp_py2_detect = RefactoringTool(py2_detect_fixers, |
| 151 {'print_function': True}) |
| 152 |
| 153 |
| 154 # We need to find a prefix for the standard library, as we don't want to |
| 155 # process any files there (they will already be Python 3). |
| 156 # |
| 157 # The following method is used by Sanjay Vinip in uprefix. This fails for |
| 158 # ``conda`` environments: |
| 159 # # In a non-pythonv virtualenv, sys.real_prefix points to the installed Pyt
hon. |
| 160 # # In a pythonv venv, sys.base_prefix points to the installed Python. |
| 161 # # Outside a virtual environment, sys.prefix points to the installed Python
. |
| 162 |
| 163 # if hasattr(sys, 'real_prefix'): |
| 164 # _syslibprefix = sys.real_prefix |
| 165 # else: |
| 166 # _syslibprefix = getattr(sys, 'base_prefix', sys.prefix) |
| 167 |
| 168 # Instead, we use the portion of the path common to both the stdlib modules |
| 169 # ``math`` and ``urllib``. |
| 170 |
| 171 def splitall(path): |
| 172 """ |
| 173 Split a path into all components. From Python Cookbook. |
| 174 """ |
| 175 allparts = [] |
| 176 while True: |
| 177 parts = os.path.split(path) |
| 178 if parts[0] == path: # sentinel for absolute paths |
| 179 allparts.insert(0, parts[0]) |
| 180 break |
| 181 elif parts[1] == path: # sentinel for relative paths |
| 182 allparts.insert(0, parts[1]) |
| 183 break |
| 184 else: |
| 185 path = parts[0] |
| 186 allparts.insert(0, parts[1]) |
| 187 return allparts |
| 188 |
| 189 |
| 190 def common_substring(s1, s2): |
| 191 """ |
| 192 Returns the longest common substring to the two strings, starting from the |
| 193 left. |
| 194 """ |
| 195 chunks = [] |
| 196 path1 = splitall(s1) |
| 197 path2 = splitall(s2) |
| 198 for (dir1, dir2) in zip(path1, path2): |
| 199 if dir1 != dir2: |
| 200 break |
| 201 chunks.append(dir1) |
| 202 return os.path.join(*chunks) |
| 203 |
| 204 # _stdlibprefix = common_substring(math.__file__, urllib.__file__) |
| 205 |
| 206 |
| 207 def detect_python2(source, pathname): |
| 208 """ |
| 209 Returns a bool indicating whether we think the code is Py2 |
| 210 """ |
| 211 RTs.setup_detect_python2() |
| 212 try: |
| 213 tree = RTs._rt_py2_detect.refactor_string(source, pathname) |
| 214 except ParseError as e: |
| 215 if e.msg != 'bad input' or e.value != '=': |
| 216 raise |
| 217 tree = RTs._rtp.refactor_string(source, pathname) |
| 218 |
| 219 if source != str(tree)[:-1]: # remove added newline |
| 220 # The above fixers made changes, so we conclude it's Python 2 code |
| 221 logger.debug('Detected Python 2 code: {0}'.format(pathname)) |
| 222 with open('/tmp/original_code.py', 'w') as f: |
| 223 f.write('### Original code (detected as py2): %s\n%s' % |
| 224 (pathname, source)) |
| 225 with open('/tmp/py2_detection_code.py', 'w') as f: |
| 226 f.write('### Code after running py3 detection (from %s)\n%s' % |
| 227 (pathname, str(tree)[:-1])) |
| 228 return True |
| 229 else: |
| 230 logger.debug('Detected Python 3 code: {0}'.format(pathname)) |
| 231 with open('/tmp/original_code.py', 'w') as f: |
| 232 f.write('### Original code (detected as py3): %s\n%s' % |
| 233 (pathname, source)) |
| 234 try: |
| 235 os.remove('/tmp/futurize_code.py') |
| 236 except OSError: |
| 237 pass |
| 238 return False |
| 239 |
| 240 |
| 241 class Py2Fixer(object): |
| 242 """ |
| 243 An import hook class that uses lib2to3 for source-to-source translation of |
| 244 Py2 code to Py3. |
| 245 """ |
| 246 |
| 247 # See the comments on :class:future.standard_library.RenameImport. |
| 248 # We add this attribute here so remove_hooks() and install_hooks() can |
| 249 # unambiguously detect whether the import hook is installed: |
| 250 PY2FIXER = True |
| 251 |
| 252 def __init__(self): |
| 253 self.found = None |
| 254 self.base_exclude_paths = ['future', 'past'] |
| 255 self.exclude_paths = copy.copy(self.base_exclude_paths) |
| 256 self.include_paths = [] |
| 257 |
| 258 def include(self, paths): |
| 259 """ |
| 260 Pass in a sequence of module names such as 'plotrique.plotting' that, |
| 261 if present at the leftmost side of the full package name, would |
| 262 specify the module to be transformed from Py2 to Py3. |
| 263 """ |
| 264 self.include_paths += paths |
| 265 |
| 266 def exclude(self, paths): |
| 267 """ |
| 268 Pass in a sequence of strings such as 'mymodule' that, if |
| 269 present at the leftmost side of the full package name, would cause |
| 270 the module not to undergo any source transformation. |
| 271 """ |
| 272 self.exclude_paths += paths |
| 273 |
| 274 def find_module(self, fullname, path=None): |
| 275 logger.debug('Running find_module: {0}...'.format(fullname)) |
| 276 if '.' in fullname: |
| 277 parent, child = fullname.rsplit('.', 1) |
| 278 if path is None: |
| 279 loader = self.find_module(parent, path) |
| 280 mod = loader.load_module(parent) |
| 281 path = mod.__path__ |
| 282 fullname = child |
| 283 |
| 284 # Perhaps we should try using the new importlib functionality in Python |
| 285 # 3.3: something like this? |
| 286 # thing = importlib.machinery.PathFinder.find_module(fullname, path) |
| 287 try: |
| 288 self.found = imp.find_module(fullname, path) |
| 289 except Exception as e: |
| 290 logger.debug('Py2Fixer could not find {0}') |
| 291 logger.debug('Exception was: {0})'.format(fullname, e)) |
| 292 return None |
| 293 self.kind = self.found[-1][-1] |
| 294 if self.kind == imp.PKG_DIRECTORY: |
| 295 self.pathname = os.path.join(self.found[1], '__init__.py') |
| 296 elif self.kind == imp.PY_SOURCE: |
| 297 self.pathname = self.found[1] |
| 298 return self |
| 299 |
| 300 def transform(self, source): |
| 301 # This implementation uses lib2to3, |
| 302 # you can override and use something else |
| 303 # if that's better for you |
| 304 |
| 305 # lib2to3 likes a newline at the end |
| 306 RTs.setup() |
| 307 source += '\n' |
| 308 try: |
| 309 tree = RTs._rt.refactor_string(source, self.pathname) |
| 310 except ParseError as e: |
| 311 if e.msg != 'bad input' or e.value != '=': |
| 312 raise |
| 313 tree = RTs._rtp.refactor_string(source, self.pathname) |
| 314 # could optimise a bit for only doing str(tree) if |
| 315 # getattr(tree, 'was_changed', False) returns True |
| 316 return str(tree)[:-1] # remove added newline |
| 317 |
| 318 def load_module(self, fullname): |
| 319 logger.debug('Running load_module for {0}...'.format(fullname)) |
| 320 if fullname in sys.modules: |
| 321 mod = sys.modules[fullname] |
| 322 else: |
| 323 if self.kind in (imp.PY_COMPILED, imp.C_EXTENSION, imp.C_BUILTIN, |
| 324 imp.PY_FROZEN): |
| 325 convert = False |
| 326 # elif (self.pathname.startswith(_stdlibprefix) |
| 327 # and 'site-packages' not in self.pathname): |
| 328 # # We assume it's a stdlib package in this case. Is this too br
ittle? |
| 329 # # Please file a bug report at https://github.com/PythonCharmer
s/python-future |
| 330 # # if so. |
| 331 # convert = False |
| 332 # in theory, other paths could be configured to be excluded here too |
| 333 elif any([fullname.startswith(path) for path in self.exclude_paths])
: |
| 334 convert = False |
| 335 elif any([fullname.startswith(path) for path in self.include_paths])
: |
| 336 convert = True |
| 337 else: |
| 338 convert = False |
| 339 if not convert: |
| 340 logger.debug('Excluded {0} from translation'.format(fullname)) |
| 341 mod = imp.load_module(fullname, *self.found) |
| 342 else: |
| 343 logger.debug('Autoconverting {0} ...'.format(fullname)) |
| 344 mod = imp.new_module(fullname) |
| 345 sys.modules[fullname] = mod |
| 346 |
| 347 # required by PEP 302 |
| 348 mod.__file__ = self.pathname |
| 349 mod.__name__ = fullname |
| 350 mod.__loader__ = self |
| 351 |
| 352 # This: |
| 353 # mod.__package__ = '.'.join(fullname.split('.')[:-1]) |
| 354 # seems to result in "SystemError: Parent module '' not loaded, |
| 355 # cannot perform relative import" for a package's __init__.py |
| 356 # file. We use the approach below. Another option to try is the |
| 357 # minimal load_module pattern from the PEP 302 text instead. |
| 358 |
| 359 # Is the test in the next line more or less robust than the |
| 360 # following one? Presumably less ... |
| 361 # ispkg = self.pathname.endswith('__init__.py') |
| 362 |
| 363 if self.kind == imp.PKG_DIRECTORY: |
| 364 mod.__path__ = [ os.path.dirname(self.pathname) ] |
| 365 mod.__package__ = fullname |
| 366 else: |
| 367 #else, regular module |
| 368 mod.__path__ = [] |
| 369 mod.__package__ = fullname.rpartition('.')[0] |
| 370 |
| 371 try: |
| 372 cachename = imp.cache_from_source(self.pathname) |
| 373 if not os.path.exists(cachename): |
| 374 update_cache = True |
| 375 else: |
| 376 sourcetime = os.stat(self.pathname).st_mtime |
| 377 cachetime = os.stat(cachename).st_mtime |
| 378 update_cache = cachetime < sourcetime |
| 379 # # Force update_cache to work around a problem with it bein
g treated as Py3 code??? |
| 380 # update_cache = True |
| 381 if not update_cache: |
| 382 with open(cachename, 'rb') as f: |
| 383 data = f.read() |
| 384 try: |
| 385 code = marshal.loads(data) |
| 386 except Exception: |
| 387 # pyc could be corrupt. Regenerate it |
| 388 update_cache = True |
| 389 if update_cache: |
| 390 if self.found[0]: |
| 391 source = self.found[0].read() |
| 392 elif self.kind == imp.PKG_DIRECTORY: |
| 393 with open(self.pathname) as f: |
| 394 source = f.read() |
| 395 |
| 396 if detect_python2(source, self.pathname): |
| 397 source = self.transform(source) |
| 398 with open('/tmp/futurized_code.py', 'w') as f: |
| 399 f.write('### Futurized code (from %s)\n%s' % |
| 400 (self.pathname, source)) |
| 401 |
| 402 code = compile(source, self.pathname, 'exec') |
| 403 |
| 404 dirname = os.path.dirname(cachename) |
| 405 if not os.path.exists(dirname): |
| 406 os.makedirs(dirname) |
| 407 try: |
| 408 with open(cachename, 'wb') as f: |
| 409 data = marshal.dumps(code) |
| 410 f.write(data) |
| 411 except Exception: # could be write-protected |
| 412 pass |
| 413 exec(code, mod.__dict__) |
| 414 except Exception as e: |
| 415 # must remove module from sys.modules |
| 416 del sys.modules[fullname] |
| 417 raise # keep it simple |
| 418 |
| 419 if self.found[0]: |
| 420 self.found[0].close() |
| 421 return mod |
| 422 |
| 423 _hook = Py2Fixer() |
| 424 |
| 425 |
| 426 def install_hooks(include_paths=(), exclude_paths=()): |
| 427 if isinstance(include_paths, str): |
| 428 include_paths = (include_paths,) |
| 429 if isinstance(exclude_paths, str): |
| 430 exclude_paths = (exclude_paths,) |
| 431 assert len(include_paths) + len(exclude_paths) > 0, 'Pass at least one argum
ent' |
| 432 _hook.include(include_paths) |
| 433 _hook.exclude(exclude_paths) |
| 434 # _hook.debug = debug |
| 435 enable = sys.version_info[0] >= 3 # enabled for all 3.x |
| 436 if enable and _hook not in sys.meta_path: |
| 437 sys.meta_path.insert(0, _hook) # insert at beginning. This could be mad
e a parameter |
| 438 |
| 439 # We could return the hook when there are ways of configuring it |
| 440 #return _hook |
| 441 |
| 442 |
| 443 def remove_hooks(): |
| 444 if _hook in sys.meta_path: |
| 445 sys.meta_path.remove(_hook) |
| 446 |
| 447 |
| 448 def detect_hooks(): |
| 449 """ |
| 450 Returns True if the import hooks are installed, False if not. |
| 451 """ |
| 452 return _hook in sys.meta_path |
| 453 # present = any([hasattr(hook, 'PY2FIXER') for hook in sys.meta_path]) |
| 454 # return present |
| 455 |
| 456 |
| 457 class hooks(object): |
| 458 """ |
| 459 Acts as a context manager. Use like this: |
| 460 |
| 461 >>> from past import translation |
| 462 >>> with translation.hooks(): |
| 463 ... import mypy2module |
| 464 >>> import requests # py2/3 compatible anyway |
| 465 >>> # etc. |
| 466 """ |
| 467 def __enter__(self): |
| 468 self.hooks_were_installed = detect_hooks() |
| 469 install_hooks() |
| 470 return self |
| 471 |
| 472 def __exit__(self, *args): |
| 473 if not self.hooks_were_installed: |
| 474 remove_hooks() |
| 475 |
| 476 |
| 477 class suspend_hooks(object): |
| 478 """ |
| 479 Acts as a context manager. Use like this: |
| 480 |
| 481 >>> from past import translation |
| 482 >>> translation.install_hooks() |
| 483 >>> import http.client |
| 484 >>> # ... |
| 485 >>> with translation.suspend_hooks(): |
| 486 >>> import requests # or others that support Py2/3 |
| 487 |
| 488 If the hooks were disabled before the context, they are not installed when |
| 489 the context is left. |
| 490 """ |
| 491 def __enter__(self): |
| 492 self.hooks_were_installed = detect_hooks() |
| 493 remove_hooks() |
| 494 return self |
| 495 def __exit__(self, *args): |
| 496 if self.hooks_were_installed: |
| 497 install_hooks() |
| 498 |
OLD | NEW |