blob: d6b708848ede1ac6c2d6dea5ebaca619d80fc170 [file] [log] [blame]
xf.li86118912025-03-19 20:07:27 -07001"""
2Main program for 2to3.
3"""
4
5from __future__ import with_statement, print_function
6
7import sys
8import os
9import difflib
10import logging
11import shutil
12import optparse
13
14from . import refactor
15
16
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
26class StdoutRefactoringTool(refactor.MultiprocessRefactoringTool):
27 """
28 A refactoring tool that can avoid overwriting its input files.
29 Prints output to stdout.
30
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.
34 """
35
36 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 """
56 self.nobackups = nobackups
57 self.show_diffs = show_diffs
58 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
63 super(StdoutRefactoringTool, self).__init__(fixers, options, explicit)
64
65 def log_error(self, msg, *args, **kwargs):
66 self.errors.append((msg, args, kwargs))
67 self.logger.error(msg, *args, **kwargs)
68
69 def write_file(self, new_text, filename, old_text, encoding):
70 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:
82 output_dir = os.path.dirname(filename)
83 if not os.path.isdir(output_dir) and output_dir:
84 os.makedirs(output_dir)
85 self.log_message('Writing converted %s to %s.', orig_filename,
86 filename)
87 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 OSError as err:
94 self.log_message("Can't remove backup %s", backup)
95 try:
96 os.rename(filename, backup)
97 except OSError as err:
98 self.log_message("Can't rename %s to %s", filename, backup)
99 # Actually write the new file
100 write = super(StdoutRefactoringTool, self).write_file
101 write(new_text, filename, old_text, encoding)
102 if not self.nobackups:
103 shutil.copymode(backup, filename)
104 if orig_filename != filename:
105 # Preserve the file mode in the new output directory.
106 shutil.copymode(orig_filename, filename)
107
108 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:
114 diff_lines = diff_texts(old, new, filename)
115 try:
116 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)
124 except UnicodeEncodeError:
125 warn("couldn't encode %s's diff for your terminal" %
126 (filename,))
127 return
128
129def warn(msg):
130 print("WARNING: %s" % (msg,), file=sys.stderr)
131
132
133def 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
144 parser = optparse.OptionParser(usage="2to3 [options] file|dir ...")
145 parser.add_option("-d", "--doctests_only", action="store_true",
146 help="Fix up doctests only")
147 parser.add_option("-f", "--fix", action="append", default=[],
148 help="Each FIX specifies a transformation; default: all")
149 parser.add_option("-j", "--processes", action="store", default=1,
150 type="int", help="Run 2to3 concurrently")
151 parser.add_option("-x", "--nofix", action="append", default=[],
152 help="Prevent a transformation from being run")
153 parser.add_option("-l", "--list-fixes", action="store_true",
154 help="List available transformations")
155 parser.add_option("-p", "--print-function", action="store_true",
156 help="Modify the grammar so that print() is a function")
157 parser.add_option("-v", "--verbose", action="store_true",
158 help="More verbose logging")
159 parser.add_option("--no-diffs", action="store_true",
160 help="Don't show diffs of the refactoring")
161 parser.add_option("-w", "--write", action="store_true",
162 help="Write back modified files")
163 parser.add_option("-n", "--nobackups", action="store_true", default=False,
164 help="Don't write backups for modified files")
165 parser.add_option("-o", "--output-dir", action="store", type="str",
166 default="", help="Put output files in this directory "
167 "instead of overwriting the input files. Requires -n.")
168 parser.add_option("-W", "--write-unchanged-files", action="store_true",
169 help="Also write files even if no changes were required"
170 " (useful with --output-dir); implies -w.")
171 parser.add_option("--add-suffix", action="store", type="str", default="",
172 help="Append this string to all output filenames."
173 " Requires -n if non-empty. "
174 "ex: --add-suffix='3' will generate .py3 files.")
175
176 # Parse command line arguments
177 refactor_stdin = False
178 flags = {}
179 options, args = parser.parse_args(args)
180 if options.write_unchanged_files:
181 flags["write_unchanged_files"] = True
182 if not options.write:
183 warn("--write-unchanged-files/-W implies -w.")
184 options.write = True
185 # If we allowed these, the original files would be renamed to backup names
186 # but not replaced.
187 if options.output_dir and not options.nobackups:
188 parser.error("Can't use --output-dir/-o without -n.")
189 if options.add_suffix and not options.nobackups:
190 parser.error("Can't use --add-suffix without -n.")
191
192 if not options.write and options.no_diffs:
193 warn("not writing files and not printing diffs; that's not very useful")
194 if not options.write and options.nobackups:
195 parser.error("Can't use -n without -w")
196 if options.list_fixes:
197 print("Available transformations for the -f/--fix option:")
198 for fixname in refactor.get_all_fix_names(fixer_pkg):
199 print(fixname)
200 if not args:
201 return 0
202 if not args:
203 print("At least one file or directory argument required.", file=sys.stderr)
204 print("Use --help to show usage.", file=sys.stderr)
205 return 2
206 if "-" in args:
207 refactor_stdin = True
208 if options.write:
209 print("Can't write to stdin.", file=sys.stderr)
210 return 2
211 if options.print_function:
212 flags["print_function"] = True
213
214 # Set up logging handler
215 level = logging.DEBUG if options.verbose else logging.INFO
216 logging.basicConfig(format='%(name)s: %(message)s', level=level)
217 logger = logging.getLogger('lib2to3.main')
218
219 # Initialize the refactoring tool
220 avail_fixes = set(refactor.get_fixers_from_package(fixer_pkg))
221 unwanted_fixes = set(fixer_pkg + ".fix_" + fix for fix in options.nofix)
222 explicit = set()
223 if options.fix:
224 all_present = False
225 for fix in options.fix:
226 if fix == "all":
227 all_present = True
228 else:
229 explicit.add(fixer_pkg + ".fix_" + fix)
230 requested = avail_fixes.union(explicit) if all_present else explicit
231 else:
232 requested = avail_fixes.union(explicit)
233 fixer_names = requested.difference(unwanted_fixes)
234 input_base_dir = os.path.commonprefix(args)
235 if (input_base_dir and not input_base_dir.endswith(os.sep)
236 and not os.path.isdir(input_base_dir)):
237 # One or more similar names were passed, their directory is the base.
238 # os.path.commonprefix() is ignorant of path elements, this corrects
239 # for that weird API.
240 input_base_dir = os.path.dirname(input_base_dir)
241 if options.output_dir:
242 input_base_dir = input_base_dir.rstrip(os.sep)
243 logger.info('Output in %r will mirror the input directory %r layout.',
244 options.output_dir, input_base_dir)
245 rt = StdoutRefactoringTool(
246 sorted(fixer_names), flags, sorted(explicit),
247 options.nobackups, not options.no_diffs,
248 input_base_dir=input_base_dir,
249 output_dir=options.output_dir,
250 append_suffix=options.add_suffix)
251
252 # Refactor all files and directories passed as arguments
253 if not rt.errors:
254 if refactor_stdin:
255 rt.refactor_stdin()
256 else:
257 try:
258 rt.refactor(args, options.write, options.doctests_only,
259 options.processes)
260 except refactor.MultiprocessingUnsupported:
261 assert options.processes > 1
262 print("Sorry, -j isn't supported on this platform.",
263 file=sys.stderr)
264 return 1
265 rt.summarize()
266
267 # Return error status (0 if rt.errors is zero)
268 return int(bool(rt.errors))