Home | History | Annotate | Download | only in lib2to3
      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