blob: ad0625e52730f54578fb0160eb8c38700c7ec1c6 [file] [log] [blame]
Benjamin Petersoneb55fd82008-09-03 00:21:32 +00001"""
2Main program for 2to3.
3"""
4
Benjamin Peterson5dfad9d2010-05-07 18:58:23 +00005from __future__ import with_statement
6
Benjamin Petersoneb55fd82008-09-03 00:21:32 +00007import sys
8import os
Benjamin Peterson840077c2009-07-20 15:33:09 +00009import difflib
Benjamin Petersoneb55fd82008-09-03 00:21:32 +000010import logging
Benjamin Peterson43caaa02008-12-16 03:35:28 +000011import shutil
Benjamin Petersoneb55fd82008-09-03 00:21:32 +000012import optparse
13
14from . import refactor
15
Benjamin Peterson840077c2009-07-20 15:33:09 +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 Petersoneaeb4c62009-05-05 23:13:58 +000026class StdoutRefactoringTool(refactor.MultiprocessRefactoringTool):
Benjamin Peterson08be2912008-09-27 21:09:10 +000027 """
Gregory P. Smith12426992012-02-12 15:51:21 -080028 A refactoring tool that can avoid overwriting its input files.
Benjamin Peterson08be2912008-09-27 21:09:10 +000029 Prints output to stdout.
Gregory P. Smith12426992012-02-12 15:51: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 Peterson08be2912008-09-27 21:09:10 +000034 """
35
Gregory P. Smith12426992012-02-12 15:51: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 Peterson6ae94ee2008-10-15 23:10:28 +000056 self.nobackups = nobackups
Benjamin Peterson840077c2009-07-20 15:33:09 +000057 self.show_diffs = show_diffs
Gregory P. Smith12426992012-02-12 15:51: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 Peterson6ae94ee2008-10-15 23:10:28 +000063 super(StdoutRefactoringTool, self).__init__(fixers, options, explicit)
64
Benjamin Peterson08be2912008-09-27 21:09:10 +000065 def log_error(self, msg, *args, **kwargs):
66 self.errors.append((msg, args, kwargs))
67 self.logger.error(msg, *args, **kwargs)
68
Benjamin Peterson84ad84e2009-05-09 01:01:14 +000069 def write_file(self, new_text, filename, old_text, encoding):
Gregory P. Smith12426992012-02-12 15:51: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. Smith32b63712012-02-12 15:59:35 -080082 output_dir = os.path.dirname(filename)
83 if not os.path.isdir(output_dir):
84 os.makedirs(output_dir)
85 self.log_message('Writing converted %s to %s.', orig_filename,
86 filename)
Benjamin Peterson6ae94ee2008-10-15 23:10:28 +000087 if not self.nobackups:
88 # Make backup
89 backup = filename + ".bak"
90 if os.path.lexists(backup):
91 try:
92 os.remove(backup)
93 except os.error, err:
94 self.log_message("Can't remove backup %s", backup)
95 try:
96 os.rename(filename, backup)
97 except os.error, err:
98 self.log_message("Can't rename %s to %s", filename, backup)
99 # Actually write the new file
Benjamin Peterson84ad84e2009-05-09 01:01:14 +0000100 write = super(StdoutRefactoringTool, self).write_file
101 write(new_text, filename, old_text, encoding)
Benjamin Peterson03943d92008-12-21 01:29:32 +0000102 if not self.nobackups:
Benjamin Peterson37fc8232009-01-03 16:34:02 +0000103 shutil.copymode(backup, filename)
Gregory P. Smith12426992012-02-12 15:51:21 -0800104 if orig_filename != filename:
105 # Preserve the file mode in the new output directory.
106 shutil.copymode(orig_filename, filename)
Benjamin Peterson6ae94ee2008-10-15 23:10:28 +0000107
Benjamin Peterson840077c2009-07-20 15:33:09 +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 Petersonabb42742009-12-28 23:50:41 +0000114 diff_lines = diff_texts(old, new, filename)
115 try:
Benjamin Peterson5dfad9d2010-05-07 18:58:23 +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 Petersonabb42742009-12-28 23:50:41 +0000124 except UnicodeEncodeError:
125 warn("couldn't encode %s's diff for your terminal" %
126 (filename,))
127 return
Benjamin Peterson840077c2009-07-20 15:33:09 +0000128
129
130def warn(msg):
131 print >> sys.stderr, "WARNING: %s" % (msg,)
Benjamin Peterson08be2912008-09-27 21:09:10 +0000132
133
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000134def main(fixer_pkg, args=None):
135 """Main program.
136
137 Args:
138 fixer_pkg: the name of a package where the fixers are located.
139 args: optional; a list of command line arguments. If omitted,
140 sys.argv[1:] is used.
141
142 Returns a suggested exit status (0, 1, 2).
143 """
144 # Set up option parser
Benjamin Peterson43caaa02008-12-16 03:35:28 +0000145 parser = optparse.OptionParser(usage="2to3 [options] file|dir ...")
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000146 parser.add_option("-d", "--doctests_only", action="store_true",
147 help="Fix up doctests only")
148 parser.add_option("-f", "--fix", action="append", default=[],
Benjamin Peterson6ae94ee2008-10-15 23:10:28 +0000149 help="Each FIX specifies a transformation; default: all")
Benjamin Petersoneaeb4c62009-05-05 23:13:58 +0000150 parser.add_option("-j", "--processes", action="store", default=1,
151 type="int", help="Run 2to3 concurrently")
Benjamin Peterson6ae94ee2008-10-15 23:10:28 +0000152 parser.add_option("-x", "--nofix", action="append", default=[],
Benjamin Peterson848cfa22010-12-04 02:18:58 +0000153 help="Prevent a transformation from being run")
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000154 parser.add_option("-l", "--list-fixes", action="store_true",
Benjamin Peterson5dfad9d2010-05-07 18:58:23 +0000155 help="List available transformations")
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000156 parser.add_option("-p", "--print-function", action="store_true",
Benjamin Peterson42d26d92009-11-25 18:16:46 +0000157 help="Modify the grammar so that print() is a function")
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000158 parser.add_option("-v", "--verbose", action="store_true",
159 help="More verbose logging")
Benjamin Peterson840077c2009-07-20 15:33:09 +0000160 parser.add_option("--no-diffs", action="store_true",
161 help="Don't show diffs of the refactoring")
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000162 parser.add_option("-w", "--write", action="store_true",
163 help="Write back modified files")
Benjamin Peterson6ae94ee2008-10-15 23:10:28 +0000164 parser.add_option("-n", "--nobackups", action="store_true", default=False,
Benjamin Peterson848cfa22010-12-04 02:18:58 +0000165 help="Don't write backups for modified files")
Gregory P. Smith12426992012-02-12 15:51:21 -0800166 parser.add_option("-o", "--output-dir", action="store", type="str",
167 default="", help="Put output files in this directory "
168 "instead of overwriting the input files. Requires -n.")
169 parser.add_option("-W", "--write-unchanged-files", action="store_true",
170 help="Also write files even if no changes were required"
171 " (useful with --output-dir); implies -w.")
172 parser.add_option("--add-suffix", action="store", type="str", default="",
173 help="Append this string to all output filenames."
174 " Requires -n if non-empty. "
175 "ex: --add-suffix='3' will generate .py3 files.")
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000176
177 # Parse command line arguments
178 refactor_stdin = False
Benjamin Peterson42d26d92009-11-25 18:16:46 +0000179 flags = {}
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000180 options, args = parser.parse_args(args)
Gregory P. Smith12426992012-02-12 15:51:21 -0800181 if options.write_unchanged_files:
182 flags["write_unchanged_files"] = True
183 if not options.write:
184 warn("--write-unchanged-files/-W implies -w.")
185 options.write = True
186 # If we allowed these, the original files would be renamed to backup names
187 # but not replaced.
188 if options.output_dir and not options.nobackups:
189 parser.error("Can't use --output-dir/-o without -n.")
190 if options.add_suffix and not options.nobackups:
191 parser.error("Can't use --add-suffix without -n.")
192
Benjamin Peterson840077c2009-07-20 15:33:09 +0000193 if not options.write and options.no_diffs:
194 warn("not writing files and not printing diffs; that's not very useful")
Benjamin Peterson6ae94ee2008-10-15 23:10:28 +0000195 if not options.write and options.nobackups:
196 parser.error("Can't use -n without -w")
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000197 if options.list_fixes:
198 print "Available transformations for the -f/--fix option:"
199 for fixname in refactor.get_all_fix_names(fixer_pkg):
200 print fixname
201 if not args:
202 return 0
203 if not args:
Benjamin Peterson840077c2009-07-20 15:33:09 +0000204 print >> sys.stderr, "At least one file or directory argument required."
205 print >> sys.stderr, "Use --help to show usage."
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000206 return 2
207 if "-" in args:
208 refactor_stdin = True
209 if options.write:
Benjamin Peterson840077c2009-07-20 15:33:09 +0000210 print >> sys.stderr, "Can't write to stdin."
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000211 return 2
Benjamin Peterson42d26d92009-11-25 18:16:46 +0000212 if options.print_function:
213 flags["print_function"] = True
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000214
215 # Set up logging handler
216 level = logging.DEBUG if options.verbose else logging.INFO
217 logging.basicConfig(format='%(name)s: %(message)s', level=level)
Gregory P. Smith12426992012-02-12 15:51:21 -0800218 logger = logging.getLogger('lib2to3.main')
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000219
220 # Initialize the refactoring tool
Benjamin Peterson6ae94ee2008-10-15 23:10:28 +0000221 avail_fixes = set(refactor.get_fixers_from_package(fixer_pkg))
222 unwanted_fixes = set(fixer_pkg + ".fix_" + fix for fix in options.nofix)
223 explicit = set()
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000224 if options.fix:
Benjamin Peterson6ae94ee2008-10-15 23:10:28 +0000225 all_present = False
226 for fix in options.fix:
227 if fix == "all":
228 all_present = True
229 else:
230 explicit.add(fixer_pkg + ".fix_" + fix)
231 requested = avail_fixes.union(explicit) if all_present else explicit
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000232 else:
Benjamin Peterson6ae94ee2008-10-15 23:10:28 +0000233 requested = avail_fixes.union(explicit)
234 fixer_names = requested.difference(unwanted_fixes)
Gregory P. Smith12426992012-02-12 15:51:21 -0800235 input_base_dir = os.path.commonprefix(args)
236 if (input_base_dir and not input_base_dir.endswith(os.sep)
237 and not os.path.isdir(input_base_dir)):
238 # One or more similar names were passed, their directory is the base.
239 # os.path.commonprefix() is ignorant of path elements, this corrects
240 # for that weird API.
241 input_base_dir = os.path.dirname(input_base_dir)
242 if options.output_dir:
243 input_base_dir = input_base_dir.rstrip(os.sep)
244 logger.info('Output in %r will mirror the input directory %r layout.',
245 options.output_dir, input_base_dir)
246 rt = StdoutRefactoringTool(
247 sorted(fixer_names), flags, sorted(explicit),
248 options.nobackups, not options.no_diffs,
249 input_base_dir=input_base_dir,
250 output_dir=options.output_dir,
251 append_suffix=options.add_suffix)
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000252
253 # Refactor all files and directories passed as arguments
254 if not rt.errors:
255 if refactor_stdin:
256 rt.refactor_stdin()
257 else:
Benjamin Petersoneaeb4c62009-05-05 23:13:58 +0000258 try:
259 rt.refactor(args, options.write, options.doctests_only,
260 options.processes)
261 except refactor.MultiprocessingUnsupported:
262 assert options.processes > 1
263 print >> sys.stderr, "Sorry, -j isn't " \
264 "supported on this platform."
265 return 1
Benjamin Petersoneb55fd82008-09-03 00:21:32 +0000266 rt.summarize()
267
268 # Return error status (0 if rt.errors is zero)
269 return int(bool(rt.errors))