|
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 options = None # Options object passed to initializer |
|
28 filename = None # The filename (set by set_filename) |
|
29 logger = None # A logger (set by set_filename) |
|
30 numbers = itertools.count(1) # For new_name() |
|
31 used_names = set() # A set of all used NAMEs |
|
32 order = "post" # Does the fixer prefer pre- or post-order traversal |
|
33 explicit = False # Is this ignored by refactor.py -f all? |
|
34 run_order = 5 # Fixers will be sorted by run order before execution |
|
35 # Lower numbers will be run first. |
|
36 |
|
37 # Shortcut for access to Python grammar symbols |
|
38 syms = pygram.python_symbols |
|
39 |
|
40 def __init__(self, options, log): |
|
41 """Initializer. Subclass may override. |
|
42 |
|
43 Args: |
|
44 options: an dict containing the options passed to RefactoringTool |
|
45 that could be used to customize the fixer through the command line. |
|
46 log: a list to append warnings and other messages to. |
|
47 """ |
|
48 self.options = options |
|
49 self.log = log |
|
50 self.compile_pattern() |
|
51 |
|
52 def compile_pattern(self): |
|
53 """Compiles self.PATTERN into self.pattern. |
|
54 |
|
55 Subclass may override if it doesn't want to use |
|
56 self.{pattern,PATTERN} in .match(). |
|
57 """ |
|
58 if self.PATTERN is not None: |
|
59 self.pattern = PatternCompiler().compile_pattern(self.PATTERN) |
|
60 |
|
61 def set_filename(self, filename): |
|
62 """Set the filename, and a logger derived from it. |
|
63 |
|
64 The main refactoring tool should call this. |
|
65 """ |
|
66 self.filename = filename |
|
67 self.logger = logging.getLogger(filename) |
|
68 |
|
69 def match(self, node): |
|
70 """Returns match for a given parse tree node. |
|
71 |
|
72 Should return a true or false object (not necessarily a bool). |
|
73 It may return a non-empty dict of matching sub-nodes as |
|
74 returned by a matching pattern. |
|
75 |
|
76 Subclass may override. |
|
77 """ |
|
78 results = {"node": node} |
|
79 return self.pattern.match(node, results) and results |
|
80 |
|
81 def transform(self, node, results): |
|
82 """Returns the transformation for a given parse tree node. |
|
83 |
|
84 Args: |
|
85 node: the root of the parse tree that matched the fixer. |
|
86 results: a dict mapping symbolic names to part of the match. |
|
87 |
|
88 Returns: |
|
89 None, or a node that is a modified copy of the |
|
90 argument node. The node argument may also be modified in-place to |
|
91 effect the same change. |
|
92 |
|
93 Subclass *must* override. |
|
94 """ |
|
95 raise NotImplementedError() |
|
96 |
|
97 def parenthesize(self, node): |
|
98 """Wrapper around pygram.parenthesize().""" |
|
99 return pygram.parenthesize(node) |
|
100 |
|
101 def new_name(self, template="xxx_todo_changeme"): |
|
102 """Return a string suitable for use as an identifier |
|
103 |
|
104 The new name is guaranteed not to conflict with other identifiers. |
|
105 """ |
|
106 name = template |
|
107 while name in self.used_names: |
|
108 name = template + str(self.numbers.next()) |
|
109 self.used_names.add(name) |
|
110 return name |
|
111 |
|
112 def log_message(self, message): |
|
113 if self.first_log: |
|
114 self.first_log = False |
|
115 self.log.append("### In file %s ###" % self.filename) |
|
116 self.log.append(message) |
|
117 |
|
118 def cannot_convert(self, node, reason=None): |
|
119 """Warn the user that a given chunk of code is not valid Python 3, |
|
120 but that it cannot be converted automatically. |
|
121 |
|
122 First argument is the top-level node for the code in question. |
|
123 Optional second argument is why it can't be converted. |
|
124 """ |
|
125 lineno = node.get_lineno() |
|
126 for_output = node.clone() |
|
127 for_output.set_prefix("") |
|
128 msg = "Line %d: could not convert: %s" |
|
129 self.log_message(msg % (lineno, for_output)) |
|
130 if reason: |
|
131 self.log_message(reason) |
|
132 |
|
133 def warning(self, node, reason): |
|
134 """Used for warning the user about possible uncertainty in the |
|
135 translation. |
|
136 |
|
137 First argument is the top-level node for the code in question. |
|
138 Optional second argument is why it can't be converted. |
|
139 """ |
|
140 lineno = node.get_lineno() |
|
141 self.log_message("Line %d: %s" % (lineno, reason)) |
|
142 |
|
143 def start_tree(self, tree, filename): |
|
144 """Some fixers need to maintain tree-wide state. |
|
145 This method is called once, at the start of tree fix-up. |
|
146 |
|
147 tree - the root node of the tree to be processed. |
|
148 filename - the name of the file the tree came from. |
|
149 """ |
|
150 self.used_names = tree.used_names |
|
151 self.set_filename(filename) |
|
152 self.numbers = itertools.count(1) |
|
153 self.first_log = True |
|
154 |
|
155 def finish_tree(self, tree, filename): |
|
156 """Some fixers need to maintain tree-wide state. |
|
157 This method is called once, at the conclusion of tree fix-up. |
|
158 |
|
159 tree - the root node of the tree to be processed. |
|
160 filename - the name of the file the tree came from. |
|
161 """ |
|
162 pass |
|
163 |
|
164 |
|
165 class ConditionalFix(BaseFix): |
|
166 """ Base class for fixers which not execute if an import is found. """ |
|
167 |
|
168 # This is the name of the import which, if found, will cause the test to be skipped |
|
169 skip_on = None |
|
170 |
|
171 def start_tree(self, *args): |
|
172 super(ConditionalFix, self).start_tree(*args) |
|
173 self._should_skip = None |
|
174 |
|
175 def should_skip(self, node): |
|
176 if self._should_skip is not None: |
|
177 return self._should_skip |
|
178 pkg = self.skip_on.split(".") |
|
179 name = pkg[-1] |
|
180 pkg = ".".join(pkg[:-1]) |
|
181 self._should_skip = does_tree_import(pkg, name, node) |
|
182 return self._should_skip |