blob: f2849fd6be3fd93d0f0c48768f09270aea77a209 [file] [log] [blame]
Benjamin Peterson8951b612008-09-03 02:27:16 +00001"""
2Main program for 2to3.
3"""
4
Zachary Ware2acbae82014-10-29 12:24:59 -05005from __future__ import with_statement, print_function
Benjamin Peterson8d26b0b2010-05-07 19:10:11 +00006
Benjamin Peterson8951b612008-09-03 02:27:16 +00007import sys
8import os
Benjamin Peterson3059b002009-07-20 16:42:03 +00009import difflib
Benjamin Peterson8951b612008-09-03 02:27:16 +000010import logging
Benjamin Peterson0b24b3d2008-12-16 03:57:54 +000011import shutil
Benjamin Peterson8951b612008-09-03 02:27:16 +000012import optparse
13
14from . import refactor
15
Benjamin Peterson3059b002009-07-20 16:42:03 +000016
17def diff_texts(a, b, filename):
18 """Return a unified diff of two strings."""
19 a = a.splitlines()
20 b = b.splitlines()
21 return difflib.unified_diff(a, b, filename, filename,
22 "(original)", "(refactored)",
23 lineterm="")
24
25
Benjamin Peterson608d8bc2009-05-05 23:23:31 +000026class StdoutRefactoringTool(refactor.MultiprocessRefactoringTool):
Benjamin Petersond61de7f2008-09-27 22:17:35 +000027 """
Gregory P. Smith58f23ff2012-02-12 15:50:21 -080028 A refactoring tool that can avoid overwriting its input files.
Benjamin Petersond61de7f2008-09-27 22:17:35 +000029 Prints output to stdout.
Gregory P. Smith58f23ff2012-02-12 15:50:21 -080030
31 Output files can optionally be written to a different directory and or
32 have an extra file suffix appended to their name for use in situations
33 where you do not want to replace the input files.
Benjamin Petersond61de7f2008-09-27 22:17:35 +000034 """
35
Gregory P. Smith58f23ff2012-02-12 15:50:21 -080036 def __init__(self, fixers, options, explicit, nobackups, show_diffs,
37 input_base_dir='', output_dir='', append_suffix=''):
38 """
39 Args:
40 fixers: A list of fixers to import.
41 options: A dict with RefactoringTool configuration.
42 explicit: A list of fixers to run even if they are explicit.
43 nobackups: If true no backup '.bak' files will be created for those
44 files that are being refactored.
45 show_diffs: Should diffs of the refactoring be printed to stdout?
46 input_base_dir: The base directory for all input files. This class
47 will strip this path prefix off of filenames before substituting
48 it with output_dir. Only meaningful if output_dir is supplied.
49 All files processed by refactor() must start with this path.
50 output_dir: If supplied, all converted files will be written into
51 this directory tree instead of input_base_dir.
52 append_suffix: If supplied, all files output by this tool will have
53 this appended to their filename. Useful for changing .py to
54 .py3 for example by passing append_suffix='3'.
55 """
Benjamin Peterson206e3072008-10-19 14:07:49 +000056 self.nobackups = nobackups
Benjamin Peterson3059b002009-07-20 16:42:03 +000057 self.show_diffs = show_diffs
Gregory P. Smith58f23ff2012-02-12 15:50:21 -080058 if input_base_dir and not input_base_dir.endswith(os.sep):
59 input_base_dir += os.sep
60 self._input_base_dir = input_base_dir
61 self._output_dir = output_dir
62 self._append_suffix = append_suffix
Benjamin Peterson206e3072008-10-19 14:07:49 +000063 super(StdoutRefactoringTool, self).__init__(fixers, options, explicit)
64
Benjamin Petersond61de7f2008-09-27 22:17:35 +000065 def log_error(self, msg, *args, **kwargs):
66 self.errors.append((msg, args, kwargs))
67 self.logger.error(msg, *args, **kwargs)
68
Benjamin Petersond481e3d2009-05-09 19:42:23 +000069 def write_file(self, new_text, filename, old_text, encoding):
Gregory P. Smith58f23ff2012-02-12 15:50:21 -080070 orig_filename = filename
71 if self._output_dir:
72 if filename.startswith(self._input_base_dir):
73 filename = os.path.join(self._output_dir,
74 filename[len(self._input_base_dir):])
75 else:
76 raise ValueError('filename %s does not start with the '
77 'input_base_dir %s' % (
78 filename, self._input_base_dir))
79 if self._append_suffix:
80 filename += self._append_suffix
81 if orig_filename != filename:
Gregory P. Smithefc66f92012-02-12 15:58:36 -080082 output_dir = os.path.dirname(filename)
Denis Osipove3a523a2018-04-19 04:50:25 +050083 if not os.path.isdir(output_dir) and output_dir:
Gregory P. Smithefc66f92012-02-12 15:58:36 -080084 os.makedirs(output_dir)
85 self.log_message('Writing converted %s to %s.', orig_filename,
86 filename)
Benjamin Peterson206e3072008-10-19 14:07:49 +000087 if not self.nobackups:
88 # Make backup
89 backup = filename + ".bak"
90 if os.path.lexists(backup):
91 try:
92 os.remove(backup)
Pablo Galindo293dd232019-11-19 21:34:03 +000093 except OSError:
Benjamin Peterson206e3072008-10-19 14:07:49 +000094 self.log_message("Can't remove backup %s", backup)
95 try:
96 os.rename(filename, backup)
Pablo Galindo293dd232019-11-19 21:34:03 +000097 except OSError:
Benjamin Peterson206e3072008-10-19 14:07:49 +000098 self.log_message("Can't rename %s to %s", filename, backup)
99 # Actually write the new file
Benjamin Petersond481e3d2009-05-09 19:42:23 +0000100 write = super(StdoutRefactoringTool, self).write_file
101 write(new_text, filename, old_text, encoding)
Benjamin Peterson92035012008-12-27 16:00:54 +0000102 if not self.nobackups:
Benjamin Peterson8bcddca2009-01-03 16:53:14 +0000103 shutil.copymode(backup, filename)
Gregory P. Smith58f23ff2012-02-12 15:50:21 -0800104 if orig_filename != filename:
105 # Preserve the file mode in the new output directory.
106 shutil.copymode(orig_filename, filename)
Benjamin Peterson206e3072008-10-19 14:07:49 +0000107
Benjamin Peterson3059b002009-07-20 16:42:03 +0000108 def print_output(self, old, new, filename, equal):
109 if equal:
110 self.log_message("No changes to %s", filename)
111 else:
112 self.log_message("Refactored %s", filename)
113 if self.show_diffs:
Benjamin Petersonffa94b02009-12-29 00:06:20 +0000114 diff_lines = diff_texts(old, new, filename)
115 try:
Benjamin Peterson8d26b0b2010-05-07 19:10:11 +0000116 if self.output_lock is not None:
117 with self.output_lock:
118 for line in diff_lines:
119 print(line)
120 sys.stdout.flush()
121 else:
122 for line in diff_lines:
123 print(line)
Benjamin Petersonffa94b02009-12-29 00:06:20 +0000124 except UnicodeEncodeError:
125 warn("couldn't encode %s's diff for your terminal" %
126 (filename,))
127 return
Benjamin Peterson3059b002009-07-20 16:42:03 +0000128
129def warn(msg):
Benjamin Peterson8fb00be2009-09-25 20:34:04 +0000130 print("WARNING: %s" % (msg,), file=sys.stderr)
Benjamin Petersond61de7f2008-09-27 22:17:35 +0000131
132
Benjamin Peterson8951b612008-09-03 02:27:16 +0000133def main(fixer_pkg, args=None):
134 """Main program.
135
136 Args:
137 fixer_pkg: the name of a package where the fixers are located.
138 args: optional; a list of command line arguments. If omitted,
139 sys.argv[1:] is used.
140
141 Returns a suggested exit status (0, 1, 2).
142 """
143 # Set up option parser
Benjamin Peterson0b24b3d2008-12-16 03:57:54 +0000144 parser = optparse.OptionParser(usage="2to3 [options] file|dir ...")
Benjamin Peterson8951b612008-09-03 02:27:16 +0000145 parser.add_option("-d", "--doctests_only", action="store_true",
146 help="Fix up doctests only")
147 parser.add_option("-f", "--fix", action="append", default=[],
Benjamin Peterson206e3072008-10-19 14:07:49 +0000148 help="Each FIX specifies a transformation; default: all")
Benjamin Peterson608d8bc2009-05-05 23:23:31 +0000149 parser.add_option("-j", "--processes", action="store", default=1,
150 type="int", help="Run 2to3 concurrently")
Benjamin Peterson206e3072008-10-19 14:07:49 +0000151 parser.add_option("-x", "--nofix", action="append", default=[],
Martin v. Löwise2bb4eb2010-12-03 23:11:07 +0000152 help="Prevent a transformation from being run")
Benjamin Peterson8951b612008-09-03 02:27:16 +0000153 parser.add_option("-l", "--list-fixes", action="store_true",
Benjamin Peterson8d26b0b2010-05-07 19:10:11 +0000154 help="List available transformations")
Benjamin Peterson8951b612008-09-03 02:27:16 +0000155 parser.add_option("-p", "--print-function", action="store_true",
Benjamin Peterson20211002009-11-25 18:34:42 +0000156 help="Modify the grammar so that print() is a function")
Batuhan Taşkaya61b14152020-01-13 01:13:31 +0300157 parser.add_option("-e", "--exec-function", action="store_true",
158 help="Modify the grammar so that exec() is a function")
Benjamin Peterson8951b612008-09-03 02:27:16 +0000159 parser.add_option("-v", "--verbose", action="store_true",
160 help="More verbose logging")
Benjamin Peterson3059b002009-07-20 16:42:03 +0000161 parser.add_option("--no-diffs", action="store_true",
162 help="Don't show diffs of the refactoring")
Benjamin Peterson8951b612008-09-03 02:27:16 +0000163 parser.add_option("-w", "--write", action="store_true",
164 help="Write back modified files")
Benjamin Peterson206e3072008-10-19 14:07:49 +0000165 parser.add_option("-n", "--nobackups", action="store_true", default=False,
Martin v. Löwise2bb4eb2010-12-03 23:11:07 +0000166 help="Don't write backups for modified files")
Gregory P. Smith58f23ff2012-02-12 15:50:21 -0800167 parser.add_option("-o", "--output-dir", action="store", type="str",
168 default="", help="Put output files in this directory "
169 "instead of overwriting the input files. Requires -n.")
170 parser.add_option("-W", "--write-unchanged-files", action="store_true",
171 help="Also write files even if no changes were required"
172 " (useful with --output-dir); implies -w.")
173 parser.add_option("--add-suffix", action="store", type="str", default="",
174 help="Append this string to all output filenames."
175 " Requires -n if non-empty. "
176 "ex: --add-suffix='3' will generate .py3 files.")
Benjamin Peterson8951b612008-09-03 02:27:16 +0000177
178 # Parse command line arguments
179 refactor_stdin = False
Benjamin Peterson20211002009-11-25 18:34:42 +0000180 flags = {}
Benjamin Peterson8951b612008-09-03 02:27:16 +0000181 options, args = parser.parse_args(args)
Gregory P. Smith58f23ff2012-02-12 15:50:21 -0800182 if options.write_unchanged_files:
183 flags["write_unchanged_files"] = True
184 if not options.write:
185 warn("--write-unchanged-files/-W implies -w.")
186 options.write = True
187 # If we allowed these, the original files would be renamed to backup names
188 # but not replaced.
189 if options.output_dir and not options.nobackups:
190 parser.error("Can't use --output-dir/-o without -n.")
191 if options.add_suffix and not options.nobackups:
192 parser.error("Can't use --add-suffix without -n.")
193
Benjamin Peterson3059b002009-07-20 16:42:03 +0000194 if not options.write and options.no_diffs:
195 warn("not writing files and not printing diffs; that's not very useful")
Benjamin Peterson206e3072008-10-19 14:07:49 +0000196 if not options.write and options.nobackups:
197 parser.error("Can't use -n without -w")
Benjamin Peterson8951b612008-09-03 02:27:16 +0000198 if options.list_fixes:
Benjamin Petersonc0a40ab2008-09-11 21:05:25 +0000199 print("Available transformations for the -f/--fix option:")
Benjamin Peterson8951b612008-09-03 02:27:16 +0000200 for fixname in refactor.get_all_fix_names(fixer_pkg):
Benjamin Petersonc0a40ab2008-09-11 21:05:25 +0000201 print(fixname)
Benjamin Peterson8951b612008-09-03 02:27:16 +0000202 if not args:
203 return 0
204 if not args:
Benjamin Petersonc0a40ab2008-09-11 21:05:25 +0000205 print("At least one file or directory argument required.", file=sys.stderr)
206 print("Use --help to show usage.", file=sys.stderr)
Benjamin Peterson8951b612008-09-03 02:27:16 +0000207 return 2
208 if "-" in args:
209 refactor_stdin = True
210 if options.write:
Benjamin Petersonc0a40ab2008-09-11 21:05:25 +0000211 print("Can't write to stdin.", file=sys.stderr)
Benjamin Peterson8951b612008-09-03 02:27:16 +0000212 return 2
Benjamin Peterson20211002009-11-25 18:34:42 +0000213 if options.print_function:
214 flags["print_function"] = True
Benjamin Peterson8951b612008-09-03 02:27:16 +0000215
Batuhan Taşkaya61b14152020-01-13 01:13:31 +0300216 if options.exec_function:
217 flags["exec_function"] = True
218
Benjamin Peterson8951b612008-09-03 02:27:16 +0000219 # Set up logging handler
220 level = logging.DEBUG if options.verbose else logging.INFO
221 logging.basicConfig(format='%(name)s: %(message)s', level=level)
Gregory P. Smith58f23ff2012-02-12 15:50:21 -0800222 logger = logging.getLogger('lib2to3.main')
Benjamin Peterson8951b612008-09-03 02:27:16 +0000223
224 # Initialize the refactoring tool
Benjamin Peterson206e3072008-10-19 14:07:49 +0000225 avail_fixes = set(refactor.get_fixers_from_package(fixer_pkg))
226 unwanted_fixes = set(fixer_pkg + ".fix_" + fix for fix in options.nofix)
227 explicit = set()
Benjamin Peterson8951b612008-09-03 02:27:16 +0000228 if options.fix:
Benjamin Peterson206e3072008-10-19 14:07:49 +0000229 all_present = False
230 for fix in options.fix:
231 if fix == "all":
232 all_present = True
233 else:
234 explicit.add(fixer_pkg + ".fix_" + fix)
235 requested = avail_fixes.union(explicit) if all_present else explicit
Benjamin Peterson8951b612008-09-03 02:27:16 +0000236 else:
Benjamin Peterson206e3072008-10-19 14:07:49 +0000237 requested = avail_fixes.union(explicit)
238 fixer_names = requested.difference(unwanted_fixes)
Gregory P. Smith58f23ff2012-02-12 15:50:21 -0800239 input_base_dir = os.path.commonprefix(args)
240 if (input_base_dir and not input_base_dir.endswith(os.sep)
241 and not os.path.isdir(input_base_dir)):
242 # One or more similar names were passed, their directory is the base.
243 # os.path.commonprefix() is ignorant of path elements, this corrects
244 # for that weird API.
245 input_base_dir = os.path.dirname(input_base_dir)
246 if options.output_dir:
247 input_base_dir = input_base_dir.rstrip(os.sep)
248 logger.info('Output in %r will mirror the input directory %r layout.',
249 options.output_dir, input_base_dir)
250 rt = StdoutRefactoringTool(
251 sorted(fixer_names), flags, sorted(explicit),
252 options.nobackups, not options.no_diffs,
253 input_base_dir=input_base_dir,
254 output_dir=options.output_dir,
255 append_suffix=options.add_suffix)
Benjamin Peterson8951b612008-09-03 02:27:16 +0000256
257 # Refactor all files and directories passed as arguments
258 if not rt.errors:
259 if refactor_stdin:
260 rt.refactor_stdin()
261 else:
Benjamin Peterson608d8bc2009-05-05 23:23:31 +0000262 try:
263 rt.refactor(args, options.write, options.doctests_only,
264 options.processes)
265 except refactor.MultiprocessingUnsupported:
266 assert options.processes > 1
Benjamin Peterson8fb00be2009-09-25 20:34:04 +0000267 print("Sorry, -j isn't supported on this platform.",
268 file=sys.stderr)
Benjamin Peterson608d8bc2009-05-05 23:23:31 +0000269 return 1
Benjamin Peterson8951b612008-09-03 02:27:16 +0000270 rt.summarize()
271
272 # Return error status (0 if rt.errors is zero)
273 return int(bool(rt.errors))