blob: b48ab60dd65becdecab973f84c6e4c70ca5a51af [file] [log] [blame]
Benjamin Peterson90f5ba52010-03-11 22:53:45 +00001#! /usr/bin/env python3
Tim Petersb7042382001-08-12 08:41:13 +00002
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
Tim Petersebb71332001-08-15 06:07:42 +000021until you try it again with a later Python release).
Tim Petersb7042382001-08-12 08:41:13 +000022
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)
Tim Peters3055ad22001-08-13 05:33:53 +000052 msg = ' '.join(strings)
53 if msg[-1:] != '\n':
54 msg += '\n'
55 sys.stderr.write(msg)
Tim Petersb7042382001-08-12 08:41:13 +000056
57def main():
58 import getopt
59 global verbose, recurse, dryrun
60 try:
61 opts, args = getopt.getopt(sys.argv[1:], "drv")
Guido van Rossumb940e112007-01-10 16:19:56 +000062 except getopt.error as msg:
Tim Petersb7042382001-08-12 08:41:13 +000063 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
78def check(file):
79 if os.path.isdir(file) and not os.path.islink(file):
80 if verbose:
Collin Winter6afaeb72007-08-03 17:06:41 +000081 print("listing directory", file)
Tim Petersb7042382001-08-12 08:41:13 +000082 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:
Collin Winter6afaeb72007-08-03 17:06:41 +000092 print("checking", file, "...", end=' ')
Tim Petersb7042382001-08-12 08:41:13 +000093 try:
94 f = open(file)
Guido van Rossumb940e112007-01-10 16:19:56 +000095 except IOError as msg:
Tim Petersb7042382001-08-12 08:41:13 +000096 errprint("%r: I/O Error: %s" % (file, str(msg)))
97 return
98
Tim Peters3055ad22001-08-13 05:33:53 +000099 ff = FutureFinder(f, file)
Tim Petersb7042382001-08-12 08:41:13 +0000100 changed = ff.run()
101 if changed:
Tim Peters3055ad22001-08-13 05:33:53 +0000102 ff.gettherest()
103 f.close()
104 if changed:
Tim Petersb7042382001-08-12 08:41:13 +0000105 if verbose:
Collin Winter6afaeb72007-08-03 17:06:41 +0000106 print("changed.")
Tim Petersb7042382001-08-12 08:41:13 +0000107 if dryrun:
Collin Winter6afaeb72007-08-03 17:06:41 +0000108 print("But this is a dry run, so leaving it alone.")
Tim Petersb7042382001-08-12 08:41:13 +0000109 for s, e, line in changed:
Collin Winter6afaeb72007-08-03 17:06:41 +0000110 print("%r lines %d-%d" % (file, s+1, e+1))
Tim Petersb7042382001-08-12 08:41:13 +0000111 for i in range(s, e+1):
Collin Winter6afaeb72007-08-03 17:06:41 +0000112 print(ff.lines[i], end=' ')
Tim Petersb7042382001-08-12 08:41:13 +0000113 if line is None:
Collin Winter6afaeb72007-08-03 17:06:41 +0000114 print("-- deleted")
Tim Petersb7042382001-08-12 08:41:13 +0000115 else:
Collin Winter6afaeb72007-08-03 17:06:41 +0000116 print("-- change to:")
117 print(line, end=' ')
Tim Petersb7042382001-08-12 08:41:13 +0000118 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:
Collin Winter6afaeb72007-08-03 17:06:41 +0000124 print("renamed", file, "to", bak)
Tim Peters3055ad22001-08-13 05:33:53 +0000125 g = open(file, "w")
126 ff.write(g)
127 g.close()
Tim Petersb7042382001-08-12 08:41:13 +0000128 if verbose:
Collin Winter6afaeb72007-08-03 17:06:41 +0000129 print("wrote new", file)
Tim Petersb7042382001-08-12 08:41:13 +0000130 else:
131 if verbose:
Collin Winter6afaeb72007-08-03 17:06:41 +0000132 print("unchanged.")
Tim Petersb7042382001-08-12 08:41:13 +0000133
134class FutureFinder:
135
Tim Peters3055ad22001-08-13 05:33:53 +0000136 def __init__(self, f, fname):
137 self.f = f
138 self.fname = fname
139 self.ateof = 0
140 self.lines = [] # raw file lines
Tim Petersb7042382001-08-12 08:41:13 +0000141
142 # List of (start_index, end_index, new_line) triples.
143 self.changed = []
144
145 # Line-getter for tokenize.
146 def getline(self):
Tim Peters3055ad22001-08-13 05:33:53 +0000147 if self.ateof:
148 return ""
149 line = self.f.readline()
150 if line == "":
151 self.ateof = 1
Tim Petersb7042382001-08-12 08:41:13 +0000152 else:
Tim Peters3055ad22001-08-13 05:33:53 +0000153 self.lines.append(line)
Tim Petersb7042382001-08-12 08:41:13 +0000154 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
Tim Petersb7042382001-08-12 08:41:13 +0000164 changed = self.changed
Georg Brandla18af4e2007-04-21 15:47:16 +0000165 get = tokenize.generate_tokens(self.getline).__next__
Tim Petersb7042382001-08-12 08:41:13 +0000166 type, token, (srow, scol), (erow, ecol), line = get()
167
Tim Peters3055ad22001-08-13 05:33:53 +0000168 # 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:
Tim Petersb7042382001-08-12 08:41:13 +0000174 type, token, (srow, scol), (erow, ecol), line = get()
175
176 # Analyze the future stmts.
Tim Peters3055ad22001-08-13 05:33:53 +0000177 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
Tim Petersb7042382001-08-12 08:41:13 +0000184 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:
Tim Peters3055ad22001-08-13 05:33:53 +0000212 errprint("Skipping file %r; can't parse line %d:\n%s" %
213 (self.fname, srow, line))
Tim Petersb7042382001-08-12 08:41:13 +0000214 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
Tim Peters3055ad22001-08-13 05:33:53 +0000235 # Rewrite the line if at least one future-feature is obsolete.
Tim Petersb7042382001-08-12 08:41:13 +0000236 if len(okfeatures) < len(features):
Tim Petersb7042382001-08-12 08:41:13 +0000237 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
Tim Peters3055ad22001-08-13 05:33:53 +0000247 # Loop back for more future statements.
Tim Petersb7042382001-08-12 08:41:13 +0000248
249 return changed
250
Tim Peters3055ad22001-08-13 05:33:53 +0000251 def gettherest(self):
252 if self.ateof:
253 self.therest = ''
254 else:
255 self.therest = self.f.read()
256
Tim Petersb7042382001-08-12 08:41:13 +0000257 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)
Tim Peters3055ad22001-08-13 05:33:53 +0000271 # Copy over the remainder of the file.
272 if self.therest:
273 f.write(self.therest)
Tim Petersb7042382001-08-12 08:41:13 +0000274
275if __name__ == '__main__':
276 main()