main.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. """
  2. Main program for 2to3.
  3. """
  4. from __future__ import with_statement, print_function
  5. import sys
  6. import os
  7. import difflib
  8. import logging
  9. import shutil
  10. import optparse
  11. from . import refactor
  12. def diff_texts(a, b, filename):
  13. """Return a unified diff of two strings."""
  14. a = a.splitlines()
  15. b = b.splitlines()
  16. return difflib.unified_diff(a, b, filename, filename,
  17. "(original)", "(refactored)",
  18. lineterm="")
  19. class StdoutRefactoringTool(refactor.MultiprocessRefactoringTool):
  20. """
  21. A refactoring tool that can avoid overwriting its input files.
  22. Prints output to stdout.
  23. Output files can optionally be written to a different directory and or
  24. have an extra file suffix appended to their name for use in situations
  25. where you do not want to replace the input files.
  26. """
  27. def __init__(self, fixers, options, explicit, nobackups, show_diffs,
  28. input_base_dir='', output_dir='', append_suffix=''):
  29. """
  30. Args:
  31. fixers: A list of fixers to import.
  32. options: A dict with RefactoringTool configuration.
  33. explicit: A list of fixers to run even if they are explicit.
  34. nobackups: If true no backup '.bak' files will be created for those
  35. files that are being refactored.
  36. show_diffs: Should diffs of the refactoring be printed to stdout?
  37. input_base_dir: The base directory for all input files. This class
  38. will strip this path prefix off of filenames before substituting
  39. it with output_dir. Only meaningful if output_dir is supplied.
  40. All files processed by refactor() must start with this path.
  41. output_dir: If supplied, all converted files will be written into
  42. this directory tree instead of input_base_dir.
  43. append_suffix: If supplied, all files output by this tool will have
  44. this appended to their filename. Useful for changing .py to
  45. .py3 for example by passing append_suffix='3'.
  46. """
  47. self.nobackups = nobackups
  48. self.show_diffs = show_diffs
  49. if input_base_dir and not input_base_dir.endswith(os.sep):
  50. input_base_dir += os.sep
  51. self._input_base_dir = input_base_dir
  52. self._output_dir = output_dir
  53. self._append_suffix = append_suffix
  54. super(StdoutRefactoringTool, self).__init__(fixers, options, explicit)
  55. def log_error(self, msg, *args, **kwargs):
  56. self.errors.append((msg, args, kwargs))
  57. self.logger.error(msg, *args, **kwargs)
  58. def write_file(self, new_text, filename, old_text, encoding):
  59. orig_filename = filename
  60. if self._output_dir:
  61. if filename.startswith(self._input_base_dir):
  62. filename = os.path.join(self._output_dir,
  63. filename[len(self._input_base_dir):])
  64. else:
  65. raise ValueError('filename %s does not start with the '
  66. 'input_base_dir %s' % (
  67. filename, self._input_base_dir))
  68. if self._append_suffix:
  69. filename += self._append_suffix
  70. if orig_filename != filename:
  71. output_dir = os.path.dirname(filename)
  72. if not os.path.isdir(output_dir) and output_dir:
  73. os.makedirs(output_dir)
  74. self.log_message('Writing converted %s to %s.', orig_filename,
  75. filename)
  76. if not self.nobackups:
  77. # Make backup
  78. backup = filename + ".bak"
  79. if os.path.lexists(backup):
  80. try:
  81. os.remove(backup)
  82. except OSError:
  83. self.log_message("Can't remove backup %s", backup)
  84. try:
  85. os.rename(filename, backup)
  86. except OSError:
  87. self.log_message("Can't rename %s to %s", filename, backup)
  88. # Actually write the new file
  89. write = super(StdoutRefactoringTool, self).write_file
  90. write(new_text, filename, old_text, encoding)
  91. if not self.nobackups:
  92. shutil.copymode(backup, filename)
  93. if orig_filename != filename:
  94. # Preserve the file mode in the new output directory.
  95. shutil.copymode(orig_filename, filename)
  96. def print_output(self, old, new, filename, equal):
  97. if equal:
  98. self.log_message("No changes to %s", filename)
  99. else:
  100. self.log_message("Refactored %s", filename)
  101. if self.show_diffs:
  102. diff_lines = diff_texts(old, new, filename)
  103. try:
  104. if self.output_lock is not None:
  105. with self.output_lock:
  106. for line in diff_lines:
  107. print(line)
  108. sys.stdout.flush()
  109. else:
  110. for line in diff_lines:
  111. print(line)
  112. except UnicodeEncodeError:
  113. warn("couldn't encode %s's diff for your terminal" %
  114. (filename,))
  115. return
  116. def warn(msg):
  117. print("WARNING: %s" % (msg,), file=sys.stderr)
  118. def main(fixer_pkg, args=None):
  119. """Main program.
  120. Args:
  121. fixer_pkg: the name of a package where the fixers are located.
  122. args: optional; a list of command line arguments. If omitted,
  123. sys.argv[1:] is used.
  124. Returns a suggested exit status (0, 1, 2).
  125. """
  126. # Set up option parser
  127. parser = optparse.OptionParser(usage="2to3 [options] file|dir ...")
  128. parser.add_option("-d", "--doctests_only", action="store_true",
  129. help="Fix up doctests only")
  130. parser.add_option("-f", "--fix", action="append", default=[],
  131. help="Each FIX specifies a transformation; default: all")
  132. parser.add_option("-j", "--processes", action="store", default=1,
  133. type="int", help="Run 2to3 concurrently")
  134. parser.add_option("-x", "--nofix", action="append", default=[],
  135. help="Prevent a transformation from being run")
  136. parser.add_option("-l", "--list-fixes", action="store_true",
  137. help="List available transformations")
  138. parser.add_option("-p", "--print-function", action="store_true",
  139. help="Modify the grammar so that print() is a function")
  140. parser.add_option("-e", "--exec-function", action="store_true",
  141. help="Modify the grammar so that exec() is a function")
  142. parser.add_option("-v", "--verbose", action="store_true",
  143. help="More verbose logging")
  144. parser.add_option("--no-diffs", action="store_true",
  145. help="Don't show diffs of the refactoring")
  146. parser.add_option("-w", "--write", action="store_true",
  147. help="Write back modified files")
  148. parser.add_option("-n", "--nobackups", action="store_true", default=False,
  149. help="Don't write backups for modified files")
  150. parser.add_option("-o", "--output-dir", action="store", type="str",
  151. default="", help="Put output files in this directory "
  152. "instead of overwriting the input files. Requires -n.")
  153. parser.add_option("-W", "--write-unchanged-files", action="store_true",
  154. help="Also write files even if no changes were required"
  155. " (useful with --output-dir); implies -w.")
  156. parser.add_option("--add-suffix", action="store", type="str", default="",
  157. help="Append this string to all output filenames."
  158. " Requires -n if non-empty. "
  159. "ex: --add-suffix='3' will generate .py3 files.")
  160. # Parse command line arguments
  161. refactor_stdin = False
  162. flags = {}
  163. options, args = parser.parse_args(args)
  164. if options.write_unchanged_files:
  165. flags["write_unchanged_files"] = True
  166. if not options.write:
  167. warn("--write-unchanged-files/-W implies -w.")
  168. options.write = True
  169. # If we allowed these, the original files would be renamed to backup names
  170. # but not replaced.
  171. if options.output_dir and not options.nobackups:
  172. parser.error("Can't use --output-dir/-o without -n.")
  173. if options.add_suffix and not options.nobackups:
  174. parser.error("Can't use --add-suffix without -n.")
  175. if not options.write and options.no_diffs:
  176. warn("not writing files and not printing diffs; that's not very useful")
  177. if not options.write and options.nobackups:
  178. parser.error("Can't use -n without -w")
  179. if options.list_fixes:
  180. print("Available transformations for the -f/--fix option:")
  181. for fixname in refactor.get_all_fix_names(fixer_pkg):
  182. print(fixname)
  183. if not args:
  184. return 0
  185. if not args:
  186. print("At least one file or directory argument required.", file=sys.stderr)
  187. print("Use --help to show usage.", file=sys.stderr)
  188. return 2
  189. if "-" in args:
  190. refactor_stdin = True
  191. if options.write:
  192. print("Can't write to stdin.", file=sys.stderr)
  193. return 2
  194. if options.print_function:
  195. flags["print_function"] = True
  196. if options.exec_function:
  197. flags["exec_function"] = True
  198. # Set up logging handler
  199. level = logging.DEBUG if options.verbose else logging.INFO
  200. logging.basicConfig(format='%(name)s: %(message)s', level=level)
  201. logger = logging.getLogger('lib2to3.main')
  202. # Initialize the refactoring tool
  203. avail_fixes = set(refactor.get_fixers_from_package(fixer_pkg))
  204. unwanted_fixes = set(fixer_pkg + ".fix_" + fix for fix in options.nofix)
  205. explicit = set()
  206. if options.fix:
  207. all_present = False
  208. for fix in options.fix:
  209. if fix == "all":
  210. all_present = True
  211. else:
  212. explicit.add(fixer_pkg + ".fix_" + fix)
  213. requested = avail_fixes.union(explicit) if all_present else explicit
  214. else:
  215. requested = avail_fixes.union(explicit)
  216. fixer_names = requested.difference(unwanted_fixes)
  217. input_base_dir = os.path.commonprefix(args)
  218. if (input_base_dir and not input_base_dir.endswith(os.sep)
  219. and not os.path.isdir(input_base_dir)):
  220. # One or more similar names were passed, their directory is the base.
  221. # os.path.commonprefix() is ignorant of path elements, this corrects
  222. # for that weird API.
  223. input_base_dir = os.path.dirname(input_base_dir)
  224. if options.output_dir:
  225. input_base_dir = input_base_dir.rstrip(os.sep)
  226. logger.info('Output in %r will mirror the input directory %r layout.',
  227. options.output_dir, input_base_dir)
  228. rt = StdoutRefactoringTool(
  229. sorted(fixer_names), flags, sorted(explicit),
  230. options.nobackups, not options.no_diffs,
  231. input_base_dir=input_base_dir,
  232. output_dir=options.output_dir,
  233. append_suffix=options.add_suffix)
  234. # Refactor all files and directories passed as arguments
  235. if not rt.errors:
  236. if refactor_stdin:
  237. rt.refactor_stdin()
  238. else:
  239. try:
  240. rt.refactor(args, options.write, options.doctests_only,
  241. options.processes)
  242. except refactor.MultiprocessingUnsupported:
  243. assert options.processes > 1
  244. print("Sorry, -j isn't supported on this platform.",
  245. file=sys.stderr)
  246. return 1
  247. rt.summarize()
  248. # Return error status (0 if rt.errors is zero)
  249. return int(bool(rt.errors))