blob: 7ef5d5463d4548eeb40a0dba24e5e8084fa87352 [file] [log] [blame]
Tim Petersb7042382001-08-12 08:41:13 +00001#! /usr/bin/env python
2
3"""cleanfuture [-d][-r][-v] path ...
4
5-d Dry run. Analyze, but don't make any changes to, files.
6-r Recurse. Search for all .py files in subdirectories too.
7-v Verbose. Print informative msgs.
8
9Search Python (.py) files for future statements, and remove the features
10from such statements that are already mandatory in the version of Python
11you're using.
12
13Pass one or more file and/or directory paths. When a directory path, all
14.py files within the directory will be examined, and, if the -r option is
15given, likewise recursively for subdirectories.
16
17Overwrites files in place, renaming the originals with a .bak extension. If
18cleanfuture finds nothing to change, the file is left alone. If cleanfuture
19does change a file, the changed file is a fixed-point (i.e., running
20cleanfuture on the resulting .py file won't change it again, at least not
21until you try it again with a m later Python release).
22
23Limitations: You can do these things, but this tool won't help you then:
24
25+ A future statement cannot be mixed with any other statement on the same
26 physical line (separated by semicolon).
27
28+ A future statement cannot contain an "as" clause.
29
30Example: Assuming you're using Python 2.2, if a file containing
31
32from __future__ import nested_scopes, generators
33
34is analyzed by cleanfuture, the line is rewritten to
35
36from __future__ import generators
37
38because nested_scopes is no longer optional in 2.2 but generators is.
39"""
40
41import __future__
42import tokenize
43import os
44import sys
45
46dryrun = 0
47recurse = 0
48verbose = 0
49
50def errprint(*args):
51 strings = map(str, args)
52 sys.stderr.write(' '.join(strings))
53 sys.stderr.write("\n")
54
55def main():
56 import getopt
57 global verbose, recurse, dryrun
58 try:
59 opts, args = getopt.getopt(sys.argv[1:], "drv")
60 except getopt.error, msg:
61 errprint(msg)
62 return
63 for o, a in opts:
64 if o == '-d':
65 dryrun += 1
66 elif o == '-r':
67 recurse += 1
68 elif o == '-v':
69 verbose += 1
70 if not args:
71 errprint("Usage:", __doc__)
72 return
73 for arg in args:
74 check(arg)
75
76def check(file):
77 if os.path.isdir(file) and not os.path.islink(file):
78 if verbose:
79 print "listing directory", file
80 names = os.listdir(file)
81 for name in names:
82 fullname = os.path.join(file, name)
83 if ((recurse and os.path.isdir(fullname) and
84 not os.path.islink(fullname))
85 or name.lower().endswith(".py")):
86 check(fullname)
87 return
88
89 if verbose:
90 print "checking", file, "...",
91 try:
92 f = open(file)
93 except IOError, msg:
94 errprint("%r: I/O Error: %s" % (file, str(msg)))
95 return
96
97 ff = FutureFinder(f)
98 f.close()
99 changed = ff.run()
100 if changed:
101 if verbose:
102 print "changed."
103 if dryrun:
104 print "But this is a dry run, so leaving it alone."
105 for s, e, line in changed:
106 print "%r lines %d-%d" % (file, s+1, e+1)
107 for i in range(s, e+1):
108 print ff.lines[i],
109 if line is None:
110 print "-- deleted"
111 else:
112 print "-- change to:"
113 print line,
114 if not dryrun:
115 bak = file + ".bak"
116 if os.path.exists(bak):
117 os.remove(bak)
118 os.rename(file, bak)
119 if verbose:
120 print "renamed", file, "to", bak
121 f = open(file, "w")
122 ff.write(f)
123 f.close()
124 if verbose:
125 print "wrote new", file
126 else:
127 if verbose:
128 print "unchanged."
129
130class FutureFinder:
131
132 def __init__(self, f):
133 # Raw file lines.
134 self.lines = f.readlines()
135 self.index = 0 # index into self.lines of next line
136
137 # List of (start_index, end_index, new_line) triples.
138 self.changed = []
139
140 # Line-getter for tokenize.
141 def getline(self):
142 if self.index >= len(self.lines):
143 line = ""
144 else:
145 line = self.lines[self.index]
146 self.index += 1
147 return line
148
149 def run(self):
150 STRING = tokenize.STRING
151 NL = tokenize.NL
152 NEWLINE = tokenize.NEWLINE
153 COMMENT = tokenize.COMMENT
154 NAME = tokenize.NAME
155 OP = tokenize.OP
156
157 saw_string = 0
158 changed = self.changed
159 get = tokenize.generate_tokens(self.getline).next
160 type, token, (srow, scol), (erow, ecol), line = get()
161
162 # Chew up initial comments, blank lines, and docstring (if any).
163 while type in (COMMENT, NL, NEWLINE, STRING):
164 if type is STRING:
165 if saw_string:
166 return changed
167 saw_string = 1
168 type, token, (srow, scol), (erow, ecol), line = get()
169
170 # Analyze the future stmts.
171 while type is NAME and token == "from":
172 startline = srow - 1 # tokenize is one-based
173 type, token, (srow, scol), (erow, ecol), line = get()
174
175 if not (type is NAME and token == "__future__"):
176 break
177 type, token, (srow, scol), (erow, ecol), line = get()
178
179 if not (type is NAME and token == "import"):
180 break
181 type, token, (srow, scol), (erow, ecol), line = get()
182
183 # Get the list of features.
184 features = []
185 while type is NAME:
186 features.append(token)
187 type, token, (srow, scol), (erow, ecol), line = get()
188
189 if not (type is OP and token == ','):
190 break
191 type, token, (srow, scol), (erow, ecol), line = get()
192
193 # A trailing comment?
194 comment = None
195 if type is COMMENT:
196 comment = token
197 type, token, (srow, scol), (erow, ecol), line = get()
198
199 if type is not NEWLINE:
200 errprint("Skipping file; can't parse line:\n", line)
201 return []
202
203 endline = srow - 1
204
205 # Check for obsolete features.
206 okfeatures = []
207 for f in features:
208 object = getattr(__future__, f, None)
209 if object is None:
210 # A feature we don't know about yet -- leave it in.
211 # They'll get a compile-time error when they compile
212 # this program, but that's not our job to sort out.
213 okfeatures.append(f)
214 else:
215 released = object.getMandatoryRelease()
216 if released is None or released <= sys.version_info:
217 # Withdrawn or obsolete.
218 pass
219 else:
220 okfeatures.append(f)
221
222 if len(okfeatures) < len(features):
223 # At least one future-feature is obsolete.
224 if len(okfeatures) == 0:
225 line = None
226 else:
227 line = "from __future__ import "
228 line += ', '.join(okfeatures)
229 if comment is not None:
230 line += ' ' + comment
231 line += '\n'
232 changed.append((startline, endline, line))
233
234 # Chew up comments and blank lines (if any).
235 while type in (COMMENT, NL, NEWLINE):
236 type, token, (srow, scol), (erow, ecol), line = get()
237
238 return changed
239
240 def write(self, f):
241 changed = self.changed
242 assert changed
243 # Prevent calling this again.
244 self.changed = []
245 # Apply changes in reverse order.
246 changed.reverse()
247 for s, e, line in changed:
248 if line is None:
249 # pure deletion
250 del self.lines[s:e+1]
251 else:
252 self.lines[s:e+1] = [line]
253 f.writelines(self.lines)
254
255if __name__ == '__main__':
256 main()