1"""
2Main program for 2to3.
3"""
4
5from __future__ import with_statement
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):
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
130def warn(msg):
131    print >> sys.stderr, "WARNING: %s" % (msg,)
132
133
134def 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