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__':