blob: 94f69126321550d346ff3f58aa89c941b601294b [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
Serhiy Storchaka172bb392019-03-30 08:33:02 +020099 with f:
100 ff = FutureFinder(f, file)
101 changed = ff.run()
102 if changed:
103 ff.gettherest()
Tim Peters3055ad22001-08-13 05:33:53 +0000104 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)
Serhiy Storchaka172bb392019-03-30 08:33:02 +0200125 with open(file, "w") as g:
126 ff.write(g)
Tim Petersb7042382001-08-12 08:41:13 +0000127 if verbose:
Collin Winter6afaeb72007-08-03 17:06:41 +0000128 print("wrote new", file)
Tim Petersb7042382001-08-12 08:41:13 +0000129 else:
130 if verbose:
Collin Winter6afaeb72007-08-03 17:06:41 +0000131 print("unchanged.")
Tim Petersb7042382001-08-12 08:41:13 +0000132
133class FutureFinder:
134
Tim Peters3055ad22001-08-13 05:33:53 +0000135 def __init__(self, f, fname):
136 self.f = f
137 self.fname = fname
138 self.ateof = 0
139 self.lines = [] # raw file lines
Tim Petersb7042382001-08-12 08:41:13 +0000140
141 # List of (start_index, end_index, new_line) triples.
142 self.changed = []
143
144 # Line-getter for tokenize.
145 def getline(self):
Tim Peters3055ad22001-08-13 05:33:53 +0000146 if self.ateof:
147 return ""
148 line = self.f.readline()
149 if line == "":
150 self.ateof = 1
Tim Petersb7042382001-08-12 08:41:13 +0000151 else:
Tim Peters3055ad22001-08-13 05:33:53 +0000152 self.lines.append(line)
Tim Petersb7042382001-08-12 08:41:13 +0000153 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
Tim Petersb7042382001-08-12 08:41:13 +0000163 changed = self.changed
Georg Brandla18af4e2007-04-21 15:47:16 +0000164 get = tokenize.generate_tokens(self.getline).__next__
Tim Petersb7042382001-08-12 08:41:13 +0000165 type, token, (srow, scol), (erow, ecol), line = get()
166
Tim Peters3055ad22001-08-13 05:33:53 +0000167 # 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:
Tim Petersb7042382001-08-12 08:41:13 +0000173 type, token, (srow, scol), (erow, ecol), line = get()
174
175 # Analyze the future stmts.
Tim Peters3055ad22001-08-13 05:33:53 +0000176 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
Tim Petersb7042382001-08-12 08:41:13 +0000183 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:
Tim Peters3055ad22001-08-13 05:33:53 +0000211 errprint("Skipping file %r; can't parse line %d:\n%s" %
212 (self.fname, srow, line))
Tim Petersb7042382001-08-12 08:41:13 +0000213 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
Tim Peters3055ad22001-08-13 05:33:53 +0000234 # Rewrite the line if at least one future-feature is obsolete.
Tim Petersb7042382001-08-12 08:41:13 +0000235 if len(okfeatures) < len(features):
Tim Petersb7042382001-08-12 08:41:13 +0000236 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
Tim Peters3055ad22001-08-13 05:33:53 +0000246 # Loop back for more future statements.
Tim Petersb7042382001-08-12 08:41:13 +0000247
248 return changed
249
Tim Peters3055ad22001-08-13 05:33:53 +0000250 def gettherest(self):
251 if self.ateof:
252 self.therest = ''
253 else:
254 self.therest = self.f.read()
255
Tim Petersb7042382001-08-12 08:41:13 +0000256 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)
Tim Peters3055ad22001-08-13 05:33:53 +0000270 # Copy over the remainder of the file.
271 if self.therest:
272 f.write(self.therest)
Tim Petersb7042382001-08-12 08:41:13 +0000273
274if __name__ == '__main__':
275 main()