1 #! /usr/bin/env python3
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 as 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, "...", end=' ')
93 try:
94 f = open(file)
95 except IOError as msg:
96 errprint("%r: I/O Error: %s" % (file, str(msg)))
97 return
98
99 with f:
100 ff = FutureFinder(f, file)
101 changed = ff.run()
102 if changed:
103 ff.gettherest()
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], end=' ')
113 if line is None:
114 print("-- deleted")
115 else:
116 print("-- change to:")
117 print(line, end=' ')
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 with open(file, "w") as g:
126 ff.write(g)
127 if verbose:
128 print("wrote new", file)
129 else:
130 if verbose:
131 print("unchanged.")
132
133 class ESC[4;38;5;81mFutureFinder:
134
135 def __init__(self, f, fname):
136 self.f = f
137 self.fname = fname
138 self.ateof = 0
139 self.lines = [] # raw file lines
140
141 # List of (start_index, end_index, new_line) triples.
142 self.changed = []
143
144 # Line-getter for tokenize.
145 def getline(self):
146 if self.ateof:
147 return ""
148 line = self.f.readline()
149 if line == "":
150 self.ateof = 1
151 else:
152 self.lines.append(line)
153 return line
154
155 def run(self):
156 STRING = tokenize.STRING
157 NL = tokenize.NL
158 NEWLINE = tokenize.NEWLINE
159 COMMENT = tokenize.COMMENT
160 NAME = tokenize.NAME
161 OP = tokenize.OP
162
163 changed = self.changed
164 get = tokenize.generate_tokens(self.getline).__next__
165 type, token, (srow, scol), (erow, ecol), line = get()
166
167 # Chew up initial comments and blank lines (if any).
168 while type in (COMMENT, NL, NEWLINE):
169 type, token, (srow, scol), (erow, ecol), line = get()
170
171 # Chew up docstring (if any -- and it may be implicitly catenated!).
172 while type is STRING:
173 type, token, (srow, scol), (erow, ecol), line = get()
174
175 # Analyze the future stmts.
176 while 1:
177 # Chew up comments and blank lines (if any).
178 while type in (COMMENT, NL, NEWLINE):
179 type, token, (srow, scol), (erow, ecol), line = get()
180
181 if not (type is NAME and token == "from"):
182 break
183 startline = srow - 1 # tokenize is one-based
184 type, token, (srow, scol), (erow, ecol), line = get()
185
186 if not (type is NAME and token == "__future__"):
187 break
188 type, token, (srow, scol), (erow, ecol), line = get()
189
190 if not (type is NAME and token == "import"):
191 break
192 type, token, (srow, scol), (erow, ecol), line = get()
193
194 # Get the list of features.
195 features = []
196 while type is NAME:
197 features.append(token)
198 type, token, (srow, scol), (erow, ecol), line = get()
199
200 if not (type is OP and token == ','):
201 break
202 type, token, (srow, scol), (erow, ecol), line = get()
203
204 # A trailing comment?
205 comment = None
206 if type is COMMENT:
207 comment = token
208 type, token, (srow, scol), (erow, ecol), line = get()
209
210 if type is not NEWLINE:
211 errprint("Skipping file %r; can't parse line %d:\n%s" %
212 (self.fname, srow, line))
213 return []
214
215 endline = srow - 1
216
217 # Check for obsolete features.
218 okfeatures = []
219 for f in features:
220 object = getattr(__future__, f, None)
221 if object is None:
222 # A feature we don't know about yet -- leave it in.
223 # They'll get a compile-time error when they compile
224 # this program, but that's not our job to sort out.
225 okfeatures.append(f)
226 else:
227 released = object.getMandatoryRelease()
228 if released is None or released <= sys.version_info:
229 # Withdrawn or obsolete.
230 pass
231 else:
232 okfeatures.append(f)
233
234 # Rewrite the line if at least one future-feature is obsolete.
235 if len(okfeatures) < len(features):
236 if len(okfeatures) == 0:
237 line = None
238 else:
239 line = "from __future__ import "
240 line += ', '.join(okfeatures)
241 if comment is not None:
242 line += ' ' + comment
243 line += '\n'
244 changed.append((startline, endline, line))
245
246 # Loop back for more future statements.
247
248 return changed
249
250 def gettherest(self):
251 if self.ateof:
252 self.therest = ''
253 else:
254 self.therest = self.f.read()
255
256 def write(self, f):
257 changed = self.changed
258 assert changed
259 # Prevent calling this again.
260 self.changed = []
261 # Apply changes in reverse order.
262 changed.reverse()
263 for s, e, line in changed:
264 if line is None:
265 # pure deletion
266 del self.lines[s:e+1]
267 else:
268 self.lines[s:e+1] = [line]
269 f.writelines(self.lines)
270 # Copy over the remainder of the file.
271 if self.therest:
272 f.write(self.therest)
273
274 if __name__ == '__main__':
275 main()