1 """ 2 Main program for 2to3. 3 """ 4 5 from __future__ import with_statement 6 7 import sys 8 import os 9 import difflib 10 import logging 11 import shutil 12 import optparse 13 14 from . import refactor 15 16 17 def 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 26 class 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): 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 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 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 129 130 def warn(msg): 131 print >> sys.stderr, "WARNING: %s" % (msg,) 132 133 134 def 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 145 parser = optparse.OptionParser(usage="2to3 [options] file|dir ...") 146 parser.add_option("-d", "--doctests_only", action="store_true", 147 help="Fix up doctests only") 148 parser.add_option("-f", "--fix", action="append", default=[], 149 help="Each FIX specifies a transformation; default: all") 150 parser.add_option("-j", "--processes", action="store", default=1, 151 type="int", help="Run 2to3 concurrently") 152 parser.add_option("-x", "--nofix", action="append", default=[], 153 help="Prevent a transformation from being run") 154 parser.add_option("-l", "--list-fixes", action="store_true", 155 help="List available transformations") 156 parser.add_option("-p", "--print-function", action="store_true", 157 help="Modify the grammar so that print() is a function") 158 parser.add_option("-v", "--verbose", action="store_true", 159 help="More verbose logging") 160 parser.add_option("--no-diffs", action="store_true", 161 help="Don't show diffs of the refactoring") 162 parser.add_option("-w", "--write", action="store_true", 163 help="Write back modified files") 164 parser.add_option("-n", "--nobackups", action="store_true", default=False, 165 help="Don't write backups for modified files") 166 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.") 176 177 # Parse command line arguments 178 refactor_stdin = False 179 flags = {} 180 options, args = parser.parse_args(args) 181 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 193 if not options.write and options.no_diffs: 194 warn("not writing files and not printing diffs; that's not very useful") 195 if not options.write and options.nobackups: 196 parser.error("Can't use -n without -w") 197 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: 204 print >> sys.stderr, "At least one file or directory argument required." 205 print >> sys.stderr, "Use --help to show usage." 206 return 2 207 if "-" in args: 208 refactor_stdin = True 209 if options.write: 210 print >> sys.stderr, "Can't write to stdin." 211 return 2 212 if options.print_function: 213 flags["print_function"] = True 214 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) 218 logger = logging.getLogger('lib2to3.main') 219 220 # Initialize the refactoring tool 221 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() 224 if options.fix: 225 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 232 else: 233 requested = avail_fixes.union(explicit) 234 fixer_names = requested.difference(unwanted_fixes) 235 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) 252 253 # Refactor all files and directories passed as arguments 254 if not rt.errors: 255 if refactor_stdin: 256 rt.refactor_stdin() 257 else: 258 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 266 rt.summarize() 267 268 # Return error status (0 if rt.errors is zero) 269 return int(bool(rt.errors)) 270