__init__.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. # -*- coding: utf-8 -*-
  2. """
  3. past.translation
  4. ==================
  5. The ``past.translation`` package provides an import hook for Python 3 which
  6. transparently runs ``futurize`` fixers over Python 2 code on import to convert
  7. print statements into functions, etc.
  8. It is intended to assist users in migrating to Python 3.x even if some
  9. dependencies still only support Python 2.x.
  10. Usage
  11. -----
  12. Once your Py2 package is installed in the usual module search path, the import
  13. hook is invoked as follows:
  14. >>> from past.translation import autotranslate
  15. >>> autotranslate('mypackagename')
  16. Or:
  17. >>> autotranslate(['mypackage1', 'mypackage2'])
  18. You can unregister the hook using::
  19. >>> from past.translation import remove_hooks
  20. >>> remove_hooks()
  21. Author: Ed Schofield.
  22. Inspired by and based on ``uprefix`` by Vinay M. Sajip.
  23. """
  24. import sys
  25. # imp was deprecated in python 3.6
  26. if sys.version_info >= (3, 6):
  27. import importlib as imp
  28. else:
  29. import imp
  30. import logging
  31. import os
  32. import copy
  33. from lib2to3.pgen2.parse import ParseError
  34. from lib2to3.refactor import RefactoringTool
  35. from libfuturize import fixes
  36. try:
  37. from importlib.machinery import (
  38. PathFinder,
  39. SourceFileLoader,
  40. )
  41. except ImportError:
  42. PathFinder = None
  43. SourceFileLoader = object
  44. if sys.version_info[:2] < (3, 4):
  45. import imp
  46. logger = logging.getLogger(__name__)
  47. logger.setLevel(logging.DEBUG)
  48. myfixes = (list(fixes.libfuturize_fix_names_stage1) +
  49. list(fixes.lib2to3_fix_names_stage1) +
  50. list(fixes.libfuturize_fix_names_stage2) +
  51. list(fixes.lib2to3_fix_names_stage2))
  52. # We detect whether the code is Py2 or Py3 by applying certain lib2to3 fixers
  53. # to it. If the diff is empty, it's Python 3 code.
  54. py2_detect_fixers = [
  55. # From stage 1:
  56. 'lib2to3.fixes.fix_apply',
  57. # 'lib2to3.fixes.fix_dict', # TODO: add support for utils.viewitems() etc. and move to stage2
  58. 'lib2to3.fixes.fix_except',
  59. 'lib2to3.fixes.fix_execfile',
  60. 'lib2to3.fixes.fix_exitfunc',
  61. 'lib2to3.fixes.fix_funcattrs',
  62. 'lib2to3.fixes.fix_filter',
  63. 'lib2to3.fixes.fix_has_key',
  64. 'lib2to3.fixes.fix_idioms',
  65. 'lib2to3.fixes.fix_import', # makes any implicit relative imports explicit. (Use with ``from __future__ import absolute_import)
  66. 'lib2to3.fixes.fix_intern',
  67. 'lib2to3.fixes.fix_isinstance',
  68. 'lib2to3.fixes.fix_methodattrs',
  69. 'lib2to3.fixes.fix_ne',
  70. 'lib2to3.fixes.fix_numliterals', # turns 1L into 1, 0755 into 0o755
  71. 'lib2to3.fixes.fix_paren',
  72. 'lib2to3.fixes.fix_print',
  73. 'lib2to3.fixes.fix_raise', # uses incompatible with_traceback() method on exceptions
  74. 'lib2to3.fixes.fix_renames',
  75. 'lib2to3.fixes.fix_reduce',
  76. # 'lib2to3.fixes.fix_set_literal', # this is unnecessary and breaks Py2.6 support
  77. 'lib2to3.fixes.fix_repr',
  78. 'lib2to3.fixes.fix_standarderror',
  79. 'lib2to3.fixes.fix_sys_exc',
  80. 'lib2to3.fixes.fix_throw',
  81. 'lib2to3.fixes.fix_tuple_params',
  82. 'lib2to3.fixes.fix_types',
  83. 'lib2to3.fixes.fix_ws_comma',
  84. 'lib2to3.fixes.fix_xreadlines',
  85. # From stage 2:
  86. 'lib2to3.fixes.fix_basestring',
  87. # 'lib2to3.fixes.fix_buffer', # perhaps not safe. Test this.
  88. # 'lib2to3.fixes.fix_callable', # not needed in Py3.2+
  89. # 'lib2to3.fixes.fix_dict', # TODO: add support for utils.viewitems() etc.
  90. 'lib2to3.fixes.fix_exec',
  91. # 'lib2to3.fixes.fix_future', # we don't want to remove __future__ imports
  92. 'lib2to3.fixes.fix_getcwdu',
  93. # 'lib2to3.fixes.fix_imports', # called by libfuturize.fixes.fix_future_standard_library
  94. # 'lib2to3.fixes.fix_imports2', # we don't handle this yet (dbm)
  95. # 'lib2to3.fixes.fix_input',
  96. # 'lib2to3.fixes.fix_itertools',
  97. # 'lib2to3.fixes.fix_itertools_imports',
  98. 'lib2to3.fixes.fix_long',
  99. # 'lib2to3.fixes.fix_map',
  100. # 'lib2to3.fixes.fix_metaclass', # causes SyntaxError in Py2! Use the one from ``six`` instead
  101. 'lib2to3.fixes.fix_next',
  102. 'lib2to3.fixes.fix_nonzero', # TODO: add a decorator for mapping __bool__ to __nonzero__
  103. # 'lib2to3.fixes.fix_operator', # we will need support for this by e.g. extending the Py2 operator module to provide those functions in Py3
  104. 'lib2to3.fixes.fix_raw_input',
  105. # 'lib2to3.fixes.fix_unicode', # strips off the u'' prefix, which removes a potentially helpful source of information for disambiguating unicode/byte strings
  106. # 'lib2to3.fixes.fix_urllib',
  107. 'lib2to3.fixes.fix_xrange',
  108. # 'lib2to3.fixes.fix_zip',
  109. ]
  110. class RTs:
  111. """
  112. A namespace for the refactoring tools. This avoids creating these at
  113. the module level, which slows down the module import. (See issue #117).
  114. There are two possible grammars: with or without the print statement.
  115. Hence we have two possible refactoring tool implementations.
  116. """
  117. _rt = None
  118. _rtp = None
  119. _rt_py2_detect = None
  120. _rtp_py2_detect = None
  121. @staticmethod
  122. def setup():
  123. """
  124. Call this before using the refactoring tools to create them on demand
  125. if needed.
  126. """
  127. if None in [RTs._rt, RTs._rtp]:
  128. RTs._rt = RefactoringTool(myfixes)
  129. RTs._rtp = RefactoringTool(myfixes, {'print_function': True})
  130. @staticmethod
  131. def setup_detect_python2():
  132. """
  133. Call this before using the refactoring tools to create them on demand
  134. if needed.
  135. """
  136. if None in [RTs._rt_py2_detect, RTs._rtp_py2_detect]:
  137. RTs._rt_py2_detect = RefactoringTool(py2_detect_fixers)
  138. RTs._rtp_py2_detect = RefactoringTool(py2_detect_fixers,
  139. {'print_function': True})
  140. # We need to find a prefix for the standard library, as we don't want to
  141. # process any files there (they will already be Python 3).
  142. #
  143. # The following method is used by Sanjay Vinip in uprefix. This fails for
  144. # ``conda`` environments:
  145. # # In a non-pythonv virtualenv, sys.real_prefix points to the installed Python.
  146. # # In a pythonv venv, sys.base_prefix points to the installed Python.
  147. # # Outside a virtual environment, sys.prefix points to the installed Python.
  148. # if hasattr(sys, 'real_prefix'):
  149. # _syslibprefix = sys.real_prefix
  150. # else:
  151. # _syslibprefix = getattr(sys, 'base_prefix', sys.prefix)
  152. # Instead, we use the portion of the path common to both the stdlib modules
  153. # ``math`` and ``urllib``.
  154. def splitall(path):
  155. """
  156. Split a path into all components. From Python Cookbook.
  157. """
  158. allparts = []
  159. while True:
  160. parts = os.path.split(path)
  161. if parts[0] == path: # sentinel for absolute paths
  162. allparts.insert(0, parts[0])
  163. break
  164. elif parts[1] == path: # sentinel for relative paths
  165. allparts.insert(0, parts[1])
  166. break
  167. else:
  168. path = parts[0]
  169. allparts.insert(0, parts[1])
  170. return allparts
  171. def common_substring(s1, s2):
  172. """
  173. Returns the longest common substring to the two strings, starting from the
  174. left.
  175. """
  176. chunks = []
  177. path1 = splitall(s1)
  178. path2 = splitall(s2)
  179. for (dir1, dir2) in zip(path1, path2):
  180. if dir1 != dir2:
  181. break
  182. chunks.append(dir1)
  183. return os.path.join(*chunks)
  184. # _stdlibprefix = common_substring(math.__file__, urllib.__file__)
  185. def detect_python2(source, pathname):
  186. """
  187. Returns a bool indicating whether we think the code is Py2
  188. """
  189. RTs.setup_detect_python2()
  190. try:
  191. tree = RTs._rt_py2_detect.refactor_string(source, pathname)
  192. except ParseError as e:
  193. if e.msg != 'bad input' or e.value != '=':
  194. raise
  195. tree = RTs._rtp.refactor_string(source, pathname)
  196. if source != str(tree)[:-1]: # remove added newline
  197. # The above fixers made changes, so we conclude it's Python 2 code
  198. logger.debug('Detected Python 2 code: {0}'.format(pathname))
  199. return True
  200. else:
  201. logger.debug('Detected Python 3 code: {0}'.format(pathname))
  202. return False
  203. def transform(source, pathname):
  204. # This implementation uses lib2to3,
  205. # you can override and use something else
  206. # if that's better for you
  207. # lib2to3 likes a newline at the end
  208. RTs.setup()
  209. source += '\n'
  210. try:
  211. tree = RTs._rt.refactor_string(source, pathname)
  212. except ParseError as e:
  213. if e.msg != 'bad input' or e.value != '=':
  214. raise
  215. tree = RTs._rtp.refactor_string(source, pathname)
  216. # could optimise a bit for only doing str(tree) if
  217. # getattr(tree, 'was_changed', False) returns True
  218. return str(tree)[:-1] # remove added newline
  219. class PastSourceFileLoader(SourceFileLoader):
  220. exclude_paths = []
  221. include_paths = []
  222. def _convert_needed(self):
  223. fullname = self.name
  224. if any(fullname.startswith(path) for path in self.exclude_paths):
  225. convert = False
  226. elif any(fullname.startswith(path) for path in self.include_paths):
  227. convert = True
  228. else:
  229. convert = False
  230. return convert
  231. def _exec_transformed_module(self, module):
  232. source = self.get_source(self.name)
  233. pathname = self.path
  234. if detect_python2(source, pathname):
  235. source = transform(source, pathname)
  236. code = compile(source, pathname, "exec")
  237. exec(code, module.__dict__)
  238. # For Python 3.3
  239. def load_module(self, fullname):
  240. logger.debug("Running load_module for %s", fullname)
  241. if fullname in sys.modules:
  242. mod = sys.modules[fullname]
  243. else:
  244. if self._convert_needed():
  245. logger.debug("Autoconverting %s", fullname)
  246. mod = imp.new_module(fullname)
  247. sys.modules[fullname] = mod
  248. # required by PEP 302
  249. mod.__file__ = self.path
  250. mod.__loader__ = self
  251. if self.is_package(fullname):
  252. mod.__path__ = []
  253. mod.__package__ = fullname
  254. else:
  255. mod.__package__ = fullname.rpartition('.')[0]
  256. self._exec_transformed_module(mod)
  257. else:
  258. mod = super().load_module(fullname)
  259. return mod
  260. # For Python >=3.4
  261. def exec_module(self, module):
  262. logger.debug("Running exec_module for %s", module)
  263. if self._convert_needed():
  264. logger.debug("Autoconverting %s", self.name)
  265. self._exec_transformed_module(module)
  266. else:
  267. super().exec_module(module)
  268. class Py2Fixer(object):
  269. """
  270. An import hook class that uses lib2to3 for source-to-source translation of
  271. Py2 code to Py3.
  272. """
  273. # See the comments on :class:future.standard_library.RenameImport.
  274. # We add this attribute here so remove_hooks() and install_hooks() can
  275. # unambiguously detect whether the import hook is installed:
  276. PY2FIXER = True
  277. def __init__(self):
  278. self.found = None
  279. self.base_exclude_paths = ['future', 'past']
  280. self.exclude_paths = copy.copy(self.base_exclude_paths)
  281. self.include_paths = []
  282. def include(self, paths):
  283. """
  284. Pass in a sequence of module names such as 'plotrique.plotting' that,
  285. if present at the leftmost side of the full package name, would
  286. specify the module to be transformed from Py2 to Py3.
  287. """
  288. self.include_paths += paths
  289. def exclude(self, paths):
  290. """
  291. Pass in a sequence of strings such as 'mymodule' that, if
  292. present at the leftmost side of the full package name, would cause
  293. the module not to undergo any source transformation.
  294. """
  295. self.exclude_paths += paths
  296. # For Python 3.3
  297. def find_module(self, fullname, path=None):
  298. logger.debug("Running find_module: (%s, %s)", fullname, path)
  299. loader = PathFinder.find_module(fullname, path)
  300. if not loader:
  301. logger.debug("Py2Fixer could not find %s", fullname)
  302. return None
  303. loader.__class__ = PastSourceFileLoader
  304. loader.exclude_paths = self.exclude_paths
  305. loader.include_paths = self.include_paths
  306. return loader
  307. # For Python >=3.4
  308. def find_spec(self, fullname, path=None, target=None):
  309. logger.debug("Running find_spec: (%s, %s, %s)", fullname, path, target)
  310. spec = PathFinder.find_spec(fullname, path, target)
  311. if not spec:
  312. logger.debug("Py2Fixer could not find %s", fullname)
  313. return None
  314. spec.loader.__class__ = PastSourceFileLoader
  315. spec.loader.exclude_paths = self.exclude_paths
  316. spec.loader.include_paths = self.include_paths
  317. return spec
  318. _hook = Py2Fixer()
  319. def install_hooks(include_paths=(), exclude_paths=()):
  320. if isinstance(include_paths, str):
  321. include_paths = (include_paths,)
  322. if isinstance(exclude_paths, str):
  323. exclude_paths = (exclude_paths,)
  324. assert len(include_paths) + len(exclude_paths) > 0, 'Pass at least one argument'
  325. _hook.include(include_paths)
  326. _hook.exclude(exclude_paths)
  327. # _hook.debug = debug
  328. enable = sys.version_info[0] >= 3 # enabled for all 3.x+
  329. if enable and _hook not in sys.meta_path:
  330. sys.meta_path.insert(0, _hook) # insert at beginning. This could be made a parameter
  331. # We could return the hook when there are ways of configuring it
  332. #return _hook
  333. def remove_hooks():
  334. if _hook in sys.meta_path:
  335. sys.meta_path.remove(_hook)
  336. def detect_hooks():
  337. """
  338. Returns True if the import hooks are installed, False if not.
  339. """
  340. return _hook in sys.meta_path
  341. # present = any([hasattr(hook, 'PY2FIXER') for hook in sys.meta_path])
  342. # return present
  343. class hooks(object):
  344. """
  345. Acts as a context manager. Use like this:
  346. >>> from past import translation
  347. >>> with translation.hooks():
  348. ... import mypy2module
  349. >>> import requests # py2/3 compatible anyway
  350. >>> # etc.
  351. """
  352. def __enter__(self):
  353. self.hooks_were_installed = detect_hooks()
  354. install_hooks()
  355. return self
  356. def __exit__(self, *args):
  357. if not self.hooks_were_installed:
  358. remove_hooks()
  359. class suspend_hooks(object):
  360. """
  361. Acts as a context manager. Use like this:
  362. >>> from past import translation
  363. >>> translation.install_hooks()
  364. >>> import http.client
  365. >>> # ...
  366. >>> with translation.suspend_hooks():
  367. >>> import requests # or others that support Py2/3
  368. If the hooks were disabled before the context, they are not installed when
  369. the context is left.
  370. """
  371. def __enter__(self):
  372. self.hooks_were_installed = detect_hooks()
  373. remove_hooks()
  374. return self
  375. def __exit__(self, *args):
  376. if self.hooks_were_installed:
  377. install_hooks()
  378. # alias
  379. autotranslate = install_hooks