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