blob: 66c448eeb9cbfd0d3a77b6f0cced5ee15364727c [file] [log] [blame]
Martin v. Löwis5e37bae2008-03-19 04:43:46 +00001# 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
7import logging
8import itertools
9
10# Get a usable 'set' constructor
11try:
12 set
13except NameError:
14 from sets import Set as set
15
16# Local imports
17from ..patcomp import PatternCompiler
18from .. import pygram
19
20class BaseFix(object):
21
22 """Optional base class for fixers.
23
24 The subclass name must be FixFooBar where FooBar is the result of
25 removing underscores and capitalizing the words of the fix name.
26 For example, the class name for a fixer named 'has_key' should be
27 FixHasKey.
28 """
29
30 PATTERN = None # Most subclasses should override with a string literal
31 pattern = None # Compiled pattern, set by compile_pattern()
32 options = None # Options object passed to initializer
33 filename = None # The filename (set by set_filename)
34 logger = None # A logger (set by set_filename)
35 numbers = itertools.count(1) # For new_name()
36 used_names = set() # A set of all used NAMEs
37 order = "post" # Does the fixer prefer pre- or post-order traversal
38 explicit = False # Is this ignored by refactor.py -f all?
39
40 # Shortcut for access to Python grammar symbols
41 syms = pygram.python_symbols
42
43 def __init__(self, options, log):
44 """Initializer. Subclass may override.
45
46 Args:
47 options: an optparse.Values instance which can be used
48 to inspect the command line options.
49 log: a list to append warnings and other messages to.
50 """
51 self.options = options
52 self.log = log
53 self.compile_pattern()
54
55 def compile_pattern(self):
56 """Compiles self.PATTERN into self.pattern.
57
58 Subclass may override if it doesn't want to use
59 self.{pattern,PATTERN} in .match().
60 """
61 if self.PATTERN is not None:
62 self.pattern = PatternCompiler().compile_pattern(self.PATTERN)
63
64 def set_filename(self, filename):
65 """Set the filename, and a logger derived from it.
66
67 The main refactoring tool should call this.
68 """
69 self.filename = filename
70 self.logger = logging.getLogger(filename)
71
72 def match(self, node):
73 """Returns match for a given parse tree node.
74
75 Should return a true or false object (not necessarily a bool).
76 It may return a non-empty dict of matching sub-nodes as
77 returned by a matching pattern.
78
79 Subclass may override.
80 """
81 results = {"node": node}
82 return self.pattern.match(node, results) and results
83
84 def transform(self, node, results):
85 """Returns the transformation for a given parse tree node.
86
87 Args:
88 node: the root of the parse tree that matched the fixer.
89 results: a dict mapping symbolic names to part of the match.
Martin v. Löwisab41b372008-03-19 05:22:42 +000090
Martin v. Löwis5e37bae2008-03-19 04:43:46 +000091 Returns:
92 None, or a node that is a modified copy of the
93 argument node. The node argument may also be modified in-place to
94 effect the same change.
95
96 Subclass *must* override.
97 """
98 raise NotImplementedError()
99
100 def parenthesize(self, node):
101 """Wrapper around pygram.parenthesize()."""
102 return pygram.parenthesize(node)
103
104 def new_name(self, template="xxx_todo_changeme"):
105 """Return a string suitable for use as an identifier
106
107 The new name is guaranteed not to conflict with other identifiers.
108 """
109 name = template
110 while name in self.used_names:
111 name = template + str(self.numbers.next())
112 self.used_names.add(name)
113 return name
114
115 def log_message(self, message):
116 if self.first_log:
117 self.first_log = False
118 self.log.append("### In file %s ###" % self.filename)
119 self.log.append(message)
120
121 def cannot_convert(self, node, reason=None):
122 """Warn the user that a given chunk of code is not valid Python 3,
123 but that it cannot be converted automatically.
124
125 First argument is the top-level node for the code in question.
126 Optional second argument is why it can't be converted.
127 """
128 lineno = node.get_lineno()
129 for_output = node.clone()
130 for_output.set_prefix("")
131 msg = "Line %d: could not convert: %s"
132 self.log_message(msg % (lineno, for_output))
133 if reason:
134 self.log_message(reason)
135
136 def warning(self, node, reason):
137 """Used for warning the user about possible uncertainty in the
138 translation.
139
140 First argument is the top-level node for the code in question.
141 Optional second argument is why it can't be converted.
142 """
143 lineno = node.get_lineno()
144 self.log_message("Line %d: %s" % (lineno, reason))
145
146 def start_tree(self, tree, filename):
147 """Some fixers need to maintain tree-wide state.
148 This method is called once, at the start of tree fix-up.
Martin v. Löwisab41b372008-03-19 05:22:42 +0000149
Martin v. Löwis5e37bae2008-03-19 04:43:46 +0000150 tree - the root node of the tree to be processed.
151 filename - the name of the file the tree came from.
152 """
153 self.used_names = tree.used_names
154 self.set_filename(filename)
155 self.numbers = itertools.count(1)
156 self.first_log = True
157
158 def finish_tree(self, tree, filename):
159 """Some fixers need to maintain tree-wide state.
160 This method is called once, at the conclusion of tree fix-up.
Martin v. Löwisab41b372008-03-19 05:22:42 +0000161
Martin v. Löwis5e37bae2008-03-19 04:43:46 +0000162 tree - the root node of the tree to be processed.
163 filename - the name of the file the tree came from.
164 """
165 pass