• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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