3
"""cleanfuture [-d][-r][-v] path ...
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.
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
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.
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).
23
Limitations: You can do these things, but this tool won't help you then:
25
+ A future statement cannot be mixed with any other statement on the same
26
physical line (separated by semicolon).
28
+ A future statement cannot contain an "as" clause.
30
Example: Assuming you're using Python 2.2, if a file containing
32
from __future__ import nested_scopes, generators
34
is analyzed by cleanfuture, the line is rewritten to
36
from __future__ import generators
38
because nested_scopes is no longer optional in 2.2 but generators is.
51
strings = map(str, args)
52
msg = ' '.join(strings)
59
global verbose, recurse, dryrun
61
opts, args = getopt.getopt(sys.argv[1:], "drv")
62
except getopt.error, msg:
73
errprint("Usage:", __doc__)
79
if os.path.isdir(file) and not os.path.islink(file):
81
print "listing directory", file
82
names = os.listdir(file)
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")):
92
print "checking", file, "...",
96
errprint("%r: I/O Error: %s" % (file, str(msg)))
99
ff = FutureFinder(f, file)
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):
116
print "-- change to:"
120
if os.path.exists(bak):
124
print "renamed", file, "to", bak
129
print "wrote new", file
136
def __init__(self, f, fname):
140
self.lines = [] # raw file lines
142
# List of (start_index, end_index, new_line) triples.
145
# Line-getter for tokenize.
149
line = self.f.readline()
153
self.lines.append(line)
157
STRING = tokenize.STRING
159
NEWLINE = tokenize.NEWLINE
160
COMMENT = tokenize.COMMENT
164
changed = self.changed
165
get = tokenize.generate_tokens(self.getline).next
166
type, token, (srow, scol), (erow, ecol), line = get()
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()
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()
176
# Analyze the future stmts.
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()
182
if not (type is NAME and token == "from"):
184
startline = srow - 1 # tokenize is one-based
185
type, token, (srow, scol), (erow, ecol), line = get()
187
if not (type is NAME and token == "__future__"):
189
type, token, (srow, scol), (erow, ecol), line = get()
191
if not (type is NAME and token == "import"):
193
type, token, (srow, scol), (erow, ecol), line = get()
195
# Get the list of features.
198
features.append(token)
199
type, token, (srow, scol), (erow, ecol), line = get()
201
if not (type is OP and token == ','):
203
type, token, (srow, scol), (erow, ecol), line = get()
205
# A trailing comment?
209
type, token, (srow, scol), (erow, ecol), line = get()
211
if type is not NEWLINE:
212
errprint("Skipping file %r; can't parse line %d:\n%s" %
213
(self.fname, srow, line))
218
# Check for obsolete features.
221
object = getattr(__future__, f, 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.
228
released = object.getMandatoryRelease()
229
if released is None or released <= sys.version_info:
230
# Withdrawn or obsolete.
235
# Rewrite the line if at least one future-feature is obsolete.
236
if len(okfeatures) < len(features):
237
if len(okfeatures) == 0:
240
line = "from __future__ import "
241
line += ', '.join(okfeatures)
242
if comment is not None:
243
line += ' ' + comment
245
changed.append((startline, endline, line))
247
# Loop back for more future statements.
251
def gettherest(self):
255
self.therest = self.f.read()
258
changed = self.changed
260
# Prevent calling this again.
262
# Apply changes in reverse order.
264
for s, e, line in changed:
267
del self.lines[s:e+1]
269
self.lines[s:e+1] = [line]
270
f.writelines(self.lines)
271
# Copy over the remainder of the file.
273
f.write(self.therest)
275
if __name__ == '__main__':