blob: f2849fd6be3fd93d0f0c48768f09270aea77a209 [file] [log] [blame]
Olivier Deprezf4ef2d02021-04-20 13:36:24 +02001"""
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:
94 self.log_message("Can't remove backup %s", backup)
95 try:
96 os.rename(filename, backup)
97 except OSError:
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("-e", "--exec-function", action="store_true",
158 help="Modify the grammar so that exec() is a function")
159 parser.add_option("-v", "--verbose", action="store_true",
160 help="More verbose logging")
161 parser.add_option("--no-diffs", action="store_true",
162 help="Don't show diffs of the refactoring")
163 parser.add_option("-w", "--write", action="store_true",
164 help="Write back modified files")
165 parser.add_option("-n", "--nobackups", action="store_true", default=False,
166 help="Don't write backups for modified files")
167 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.")
177
178 # Parse command line arguments
179 refactor_stdin = False
180 flags = {}
181 options, args = parser.parse_args(args)
182 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
194 if not options.write and options.no_diffs:
195 warn("not writing files and not printing diffs; that's not very useful")
196 if not options.write and options.nobackups:
197 parser.error("Can't use -n without -w")
198 if options.list_fixes:
199 print("Available transformations for the -f/--fix option:")
200 for fixname in refactor.get_all_fix_names(fixer_pkg):
201 print(fixname)
202 if not args:
203 return 0
204 if not args:
205 print("At least one file or directory argument required.", file=sys.stderr)
206 print("Use --help to show usage.", file=sys.stderr)
207 return 2
208 if "-" in args:
209 refactor_stdin = True
210 if options.write:
211 print("Can't write to stdin.", file=sys.stderr)
212 return 2
213 if options.print_function:
214 flags["print_function"] = True
215
216 if options.exec_function:
217 flags["exec_function"] = True
218
219 # Set up logging handler
220 level = logging.DEBUG if options.verbose else logging.INFO
221 logging.basicConfig(format='%(name)s: %(message)s', level=level)
222 logger = logging.getLogger('lib2to3.main')
223
224 # Initialize the refactoring tool
225 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()
228 if options.fix:
229 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
236 else:
237 requested = avail_fixes.union(explicit)
238 fixer_names = requested.difference(unwanted_fixes)
239 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)
256
257 # Refactor all files and directories passed as arguments
258 if not rt.errors:
259 if refactor_stdin:
260 rt.refactor_stdin()
261 else:
262 try:
263 rt.refactor(args, options.write, options.doctests_only,
264 options.processes)
265 except refactor.MultiprocessingUnsupported:
266 assert options.processes > 1
267 print("Sorry, -j isn't supported on this platform.",
268 file=sys.stderr)
269 return 1
270 rt.summarize()
271
272 # Return error status (0 if rt.errors is zero)
273 return int(bool(rt.errors))