Home | History | Annotate | Download | only in lib2to3
      1 # Copyright 2006 Google, Inc. All Rights Reserved.
      2 # Licensed to PSF under a Contributor Agreement.
      3 
      4 """Base class for fixers (optional, but recommended)."""
      5 
      6 # Python imports
      7 import logging
      8 import itertools
      9 
     10 # Local imports
     11 from .patcomp import PatternCompiler
     12 from . import pygram
     13 from .fixer_util import does_tree_import
     14 
     15 class BaseFix(object):
     16 
     17     """Optional base class for fixers.
     18 
     19     The subclass name must be FixFooBar where FooBar is the result of
     20     removing underscores and capitalizing the words of the fix name.
     21     For example, the class name for a fixer named 'has_key' should be
     22     FixHasKey.
     23     """
     24 
     25     PATTERN = None  # Most subclasses should override with a string literal
     26     pattern = None  # Compiled pattern, set by compile_pattern()
     27     pattern_tree = None # Tree representation of the pattern
     28     options = None  # Options object passed to initializer
     29     filename = None # The filename (set by set_filename)
     30     logger = None   # A logger (set by set_filename)
     31     numbers = itertools.count(1) # For new_name()
     32     used_names = set() # A set of all used NAMEs
     33     order = "post" # Does the fixer prefer pre- or post-order traversal
     34     explicit = False # Is this ignored by refactor.py -f all?
     35     run_order = 5   # Fixers will be sorted by run order before execution
     36                     # Lower numbers will be run first.
     37     _accept_type = None # [Advanced and not public] This tells RefactoringTool
     38                         # which node type to accept when there's not a pattern.
     39 
     40     keep_line_order = False # For the bottom matcher: match with the
     41                             # original line order
     42     BM_compatible = False # Compatibility with the bottom matching
     43                           # module; every fixer should set this
     44                           # manually
     45 
     46     # Shortcut for access to Python grammar symbols
     47     syms = pygram.python_symbols
     48 
     49     def __init__(self, options, log):
     50         """Initializer.  Subclass may override.
     51 
     52         Args:
     53             options: an dict containing the options passed to RefactoringTool
     54             that could be used to customize the fixer through the command line.
     55             log: a list to append warnings and other messages to.
     56         """
     57         self.options = options
     58         self.log = log
     59         self.compile_pattern()
     60 
     61     def compile_pattern(self):
     62         """Compiles self.PATTERN into self.pattern.
     63 
     64         Subclass may override if it doesn't want to use
     65         self.{pattern,PATTERN} in .match().
     66         """
     67         if self.PATTERN is not None:
     68             PC = PatternCompiler()
     69             self.pattern, self.pattern_tree = PC.compile_pattern(self.PATTERN,
     70                                                                  with_tree=True)
     71 
     72     def set_filename(self, filename):
     73         """Set the filename, and a logger derived from it.
     74 
     75         The main refactoring tool should call this.
     76         """
     77         self.filename = filename
     78         self.logger = logging.getLogger(filename)
     79 
     80     def match(self, node):
     81         """Returns match for a given parse tree node.
     82 
     83         Should return a true or false object (not necessarily a bool).
     84         It may return a non-empty dict of matching sub-nodes as
     85         returned by a matching pattern.
     86 
     87         Subclass may override.
     88         """
     89         results = {"node": node}
     90         return self.pattern.match(node, results) and results
     91 
     92     def transform(self, node, results):
     93         """Returns the transformation for a given parse tree node.
     94 
     95         Args:
     96           node: the root of the parse tree that matched the fixer.
     97           results: a dict mapping symbolic names to part of the match.
     98 
     99         Returns:
    100           None, or a node that is a modified copy of the
    101           argument node.  The node argument may also be modified in-place to
    102           effect the same change.
    103 
    104         Subclass *must* override.
    105         """
    106         raise NotImplementedError()
    107 
    108     def new_name(self, template=u"xxx_todo_changeme"):
    109         """Return a string suitable for use as an identifier
    110 
    111         The new name is guaranteed not to conflict with other identifiers.
    112         """
    113         name = template
    114         while name in self.used_names:
    115             name = template + unicode(self.numbers.next())
    116         self.used_names.add(name)
    117         return name
    118 
    119     def log_message(self, message):
    120         if self.first_log:
    121             self.first_log = False
    122             self.log.append("### In file %s ###" % self.filename)
    123         self.log.append(message)
    124 
    125     def cannot_convert(self, node, reason=None):
    126         """Warn the user that a given chunk of code is not valid Python 3,
    127         but that it cannot be converted automatically.
    128 
    129         First argument is the top-level node for the code in question.
    130         Optional second argument is why it can't be converted.
    131         """
    132         lineno = node.get_lineno()
    133         for_output = node.clone()
    134         for_output.prefix = u""
    135         msg = "Line %d: could not convert: %s"
    136         self.log_message(msg % (lineno, for_output))
    137         if reason:
    138             self.log_message(reason)
    139 
    140     def warning(self, node, reason):
    141         """Used for warning the user about possible uncertainty in the
    142         translation.
    143 
    144         First argument is the top-level node for the code in question.
    145         Optional second argument is why it can't be converted.
    146         """
    147         lineno = node.get_lineno()
    148         self.log_message("Line %d: %s" % (lineno, reason))
    149 
    150     def start_tree(self, tree, filename):
    151         """Some fixers need to maintain tree-wide state.
    152         This method is called once, at the start of tree fix-up.
    153 
    154         tree - the root node of the tree to be processed.
    155         filename - the name of the file the tree came from.
    156         """
    157         self.used_names = tree.used_names
    158         self.set_filename(filename)
    159         self.numbers = itertools.count(1)
    160         self.first_log = True
    161 
    162     def finish_tree(self, tree, filename):
    163         """Some fixers need to maintain tree-wide state.
    164         This method is called once, at the conclusion of tree fix-up.
    165 
    166         tree - the root node of the tree to be processed.
    167         filename - the name of the file the tree came from.
    168         """
    169         pass
    170 
    171 
    172 class ConditionalFix(BaseFix):
    173     """ Base class for fixers which not execute if an import is found. """
    174 
    175     # This is the name of the import which, if found, will cause the test to be skipped
    176     skip_on = None
    177 
    178     def start_tree(self, *args):
    179         super(ConditionalFix, self).start_tree(*args)
    180         self._should_skip = None
    181 
    182     def should_skip(self, node):
    183         if self._should_skip is not None:
    184             return self._should_skip
    185         pkg = self.skip_on.split(".")
    186         name = pkg[-1]
    187         pkg = ".".join(pkg[:-1])
    188         self._should_skip = does_tree_import(pkg, name, node)
    189         return self._should_skip
    190