1""" 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)) 269