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:
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))
274