(root)/
Python-3.12.0/
Lib/
lib2to3/
main.py
       1  """
       2  Main program for 2to3.
       3  """
       4  
       5  from __future__ import with_statement, print_function
       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 ESC[4;38;5;81mStdoutRefactoringTool(ESC[4;38;5;149mrefactorESC[4;38;5;149m.ESC[4;38;5;149mMultiprocessRefactoringTool):
      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  
     129  def warn(msg):
     130      print("WARNING: %s" % (msg,), file=sys.stderr)
     131  
     132  
     133  def 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))