blob: 382503a0604a160340ddcd0b3066ce8c95acf3d2 [file] [log] [blame]
Guido van Rossum822218b2001-09-01 21:55:58 +00001#! /usr/bin/env python
2
3"""fixdiv - tool to fix division operators.
4
5To use this tool, first run `python -Dwarn yourscript.py 2>warnings'.
6This runs the script `yourscript.py' while writing warning messages
7about all uses of the classic division operator to the file
Guido van Rossum13c51ec2001-09-02 04:49:36 +00008`warnings'. The warnings look like this:
9
10 <file>:<line>: DeprecationWarning: classic <type> division
11
12The warnings are written to stderr, so you must use `2>' for the I/O
13redirect. I know of no way to redirect stderr on Windows in a DOS
14box, so you will have to modify the script to set sys.stderr to some
15kind of log file if you want to do this on Windows.
16
17The warnings are not limited to the script; modules imported by the
18script may also trigger warnings. In fact a useful technique is to
19write a test script specifically intended to exercise all code in a
20particular module or set of modules.
Guido van Rossum822218b2001-09-01 21:55:58 +000021
22Then run `python fixdiv.py warnings'. This first reads the warnings,
23looking for classic division warnings, and sorts them by file name and
24line number. Then, for each file that received at least one warning,
25it parses the file and tries to match the warnings up to the division
26operators found in the source code. If it is successful, it writes a
27recommendation to stdout in the form of a context diff. If it is not
Guido van Rossum13c51ec2001-09-02 04:49:36 +000028successful, it writes observations to stdout instead.
29
30There are several possible recommendations and observations:
31
32- A / operator was found that can remain unchanged. This is the
33 recommendation when only float and/or complex arguments were seen.
34
35- A / operator was found that should be changed to //. This is the
36 recommendation when only int and/or long arguments were seen.
37
38- A / operator was found for which int or long as well as float or
39 complex arguments were seen. This is highly unlikely; if it occurs,
40 you may have to restructure the code to keep the classic semantics,
41 or maybe you don't care about the classic semantics.
42
43- A / operator was found for which no warnings were seen. This could
44 be code that was never executed, or code that was only executed with
45 with user-defined objects as arguments. You will have to
46 investigate further. Note that // can be overloaded separately from
47 /, using __floordiv__. True division can also be separately
48 overloaded, using __truediv__. Classic division should be the same
49 as either of those. (XXX should I add a warning for division on
50 user-defined objects, to disambiguate this case from code that was
51 never executed?)
52
53- A warning was seen for a line not containing a / operator. This is
54 an anomaly that shouldn't happen; the most likely cause is a change
55 to the file between the time the test script was run to collect
56 warnings and the time fixdiv was run.
57
58- More than one / operator was found on one line, or in a statement
59 split across multiple lines. Because the warnings framework doesn't
60 (and can't) show the offset within the line, and the code generator
61 doesn't always give the correct line number for operations in a
62 multi-line statement, it's not clear whether both were executed. In
63 practice, they usually are, so the default action is make the same
64 recommendation for all / operators, based on the above criteria.
65
66Notes:
67
68- The augmented assignment operator /= is handled the same way as the
69 / operator.
70
71- This tool never looks at the // operator; no warnings are ever
72 generated for use of this operator.
73
74- This tool never looks at the / operator when a future division
75 statement is in effect; no warnings are generated in this case, and
76 because the tool only looks at files for which at least one classic
77 division warning was seen, it will never look at files containing a
78 future division statement.
79
80- Warnings may be issued for code not read from a file, but executed
81 using an exec statement or the eval() function. These will have
82 <string> in the filename position. The fixdiv script will attempt
83 and fail to open a file named "<string>", and issue a warning about
84 this failure. You're on your own to deal with this. You could make
85 all recommended changes and add a future division statement to all
86 affected files, and then re-run the test script; it should not issue
87 any warnings. If there are any, and you have a hard time tracking
88 down where they are generated, you can use the -Werror option to
89 force an error instead of a first warning, generating a traceback.
90
91- The tool should be run from the same directory as that from which
92 the original script was run, otherwise it won't be able to open
93 files given by relative pathnames.
Guido van Rossum822218b2001-09-01 21:55:58 +000094"""
95
96import sys
97import getopt
98import re
99import tokenize
100from pprint import pprint
101
102def main():
103 try:
104 opts, args = getopt.getopt(sys.argv[1:], "h")
105 except getopt.error, msg:
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000106 usage(msg)
107 return 2
Guido van Rossum822218b2001-09-01 21:55:58 +0000108 for o, a in opts:
109 if o == "-h":
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000110 print __doc__
111 return
Guido van Rossum822218b2001-09-01 21:55:58 +0000112 if not args:
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000113 usage("at least one file argument is required")
114 return 2
Guido van Rossum822218b2001-09-01 21:55:58 +0000115 if args[1:]:
116 sys.stderr.write("%s: extra file arguments ignored\n", sys.argv[0])
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000117 warnings = readwarnings(args[0])
118 if warnings is None:
119 return 1
120 files = warnings.keys()
121 if not files:
122 print "No classic division warnings read from", args[0]
123 return
124 files.sort()
125 exit = None
126 for file in files:
127 x = process(file, warnings[file])
128 exit = exit or x
129 return exit
Guido van Rossum822218b2001-09-01 21:55:58 +0000130
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000131def usage(msg):
132 sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
Guido van Rossum822218b2001-09-01 21:55:58 +0000133 sys.stderr.write("Usage: %s warnings\n" % sys.argv[0])
134 sys.stderr.write("Try `%s -h' for more information.\n" % sys.argv[0])
Guido van Rossum822218b2001-09-01 21:55:58 +0000135
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000136PATTERN = ("^(.+?):(\d+): DeprecationWarning: "
137 "classic (int|long|float|complex) division$")
Guido van Rossum822218b2001-09-01 21:55:58 +0000138
139def readwarnings(warningsfile):
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000140 prog = re.compile(PATTERN)
Guido van Rossum822218b2001-09-01 21:55:58 +0000141 try:
142 f = open(warningsfile)
143 except IOError, msg:
144 sys.stderr.write("can't open: %s\n" % msg)
145 return
146 warnings = {}
147 while 1:
148 line = f.readline()
149 if not line:
150 break
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000151 m = prog.match(line)
Guido van Rossum822218b2001-09-01 21:55:58 +0000152 if not m:
153 if line.find("division") >= 0:
154 sys.stderr.write("Warning: ignored input " + line)
155 continue
156 file, lineno, what = m.groups()
157 list = warnings.get(file)
158 if list is None:
159 warnings[file] = list = []
160 list.append((int(lineno), intern(what)))
161 f.close()
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000162 return warnings
Guido van Rossum822218b2001-09-01 21:55:58 +0000163
164def process(file, list):
165 print "-"*70
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000166 assert list # if this fails, readwarnings() is broken
Guido van Rossum822218b2001-09-01 21:55:58 +0000167 try:
168 fp = open(file)
169 except IOError, msg:
170 sys.stderr.write("can't open: %s\n" % msg)
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000171 return 1
172 print "Index:", file
Guido van Rossum822218b2001-09-01 21:55:58 +0000173 f = FileContext(fp)
174 list.sort()
175 index = 0 # list[:index] has been processed, list[index:] is still to do
Guido van Rossum822218b2001-09-01 21:55:58 +0000176 g = tokenize.generate_tokens(f.readline)
177 while 1:
178 startlineno, endlineno, slashes = lineinfo = scanline(g)
179 if startlineno is None:
180 break
181 assert startlineno <= endlineno is not None
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000182 orphans = []
Guido van Rossum822218b2001-09-01 21:55:58 +0000183 while index < len(list) and list[index][0] < startlineno:
184 orphans.append(list[index])
185 index += 1
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000186 if orphans:
187 reportphantomwarnings(orphans, f)
Guido van Rossum822218b2001-09-01 21:55:58 +0000188 warnings = []
189 while index < len(list) and list[index][0] <= endlineno:
190 warnings.append(list[index])
191 index += 1
192 if not slashes and not warnings:
193 pass
194 elif slashes and not warnings:
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000195 report(slashes, "No conclusive evidence")
Guido van Rossum822218b2001-09-01 21:55:58 +0000196 elif warnings and not slashes:
197 reportphantomwarnings(warnings, f)
198 else:
199 if len(slashes) > 1:
200 report(slashes, "More than one / operator")
201 else:
202 (row, col), line = slashes[0]
203 line = chop(line)
204 if line[col:col+1] != "/":
205 print "*** Can't find the / operator in line %d:" % row
206 print "*", line
207 continue
208 intlong = []
209 floatcomplex = []
210 bad = []
211 for lineno, what in warnings:
212 if what in ("int", "long"):
213 intlong.append(what)
214 elif what in ("float", "complex"):
215 floatcomplex.append(what)
216 else:
217 bad.append(what)
218 if bad:
219 print "*** Bad warning for line %d:" % row, bad
220 print "*", line
221 elif intlong and not floatcomplex:
222 print "%dc%d" % (row, row)
223 print "<", line
224 print "---"
225 print ">", line[:col] + "/" + line[col:]
226 elif floatcomplex and not intlong:
227 print "True division / operator at line %d:" % row
228 print "=", line
229 fp.close()
230
231def reportphantomwarnings(warnings, f):
232 blocks = []
233 lastrow = None
234 lastblock = None
235 for row, what in warnings:
236 if row != lastrow:
237 lastblock = [row]
238 blocks.append(lastblock)
239 lastblock.append(what)
240 for block in blocks:
241 row = block[0]
242 whats = "/".join(block[1:])
243 print "*** Phantom %s warnings for line %d:" % (whats, row)
244 f.report(row, mark="*")
245
246def report(slashes, message):
247 lastrow = None
248 for (row, col), line in slashes:
249 if row != lastrow:
250 print "*** %s on line %d:" % (message, row)
251 print "*", chop(line)
252 lastrow = row
253
254class FileContext:
255 def __init__(self, fp, window=5, lineno=1):
256 self.fp = fp
257 self.window = 5
258 self.lineno = 1
259 self.eoflookahead = 0
260 self.lookahead = []
261 self.buffer = []
262 def fill(self):
263 while len(self.lookahead) < self.window and not self.eoflookahead:
264 line = self.fp.readline()
265 if not line:
266 self.eoflookahead = 1
267 break
268 self.lookahead.append(line)
269 def readline(self):
270 self.fill()
271 if not self.lookahead:
272 return ""
273 line = self.lookahead.pop(0)
274 self.buffer.append(line)
275 self.lineno += 1
276 return line
277 def truncate(self):
278 del self.buffer[-window:]
279 def __getitem__(self, index):
280 self.fill()
281 bufstart = self.lineno - len(self.buffer)
282 lookend = self.lineno + len(self.lookahead)
283 if bufstart <= index < self.lineno:
284 return self.buffer[index - bufstart]
285 if self.lineno <= index < lookend:
286 return self.lookahead[index - self.lineno]
287 raise KeyError
288 def report(self, first, last=None, mark="*"):
289 if last is None:
290 last = first
291 for i in range(first, last+1):
292 try:
293 line = self[first]
294 except KeyError:
295 line = "<missing line>"
296 print mark, chop(line)
297
298def scanline(g):
299 slashes = []
300 startlineno = None
301 endlineno = None
302 for type, token, start, end, line in g:
303 endlineno = end[0]
304 if startlineno is None:
305 startlineno = endlineno
306 if token in ("/", "/="):
307 slashes.append((start, line))
Guido van Rossum822218b2001-09-01 21:55:58 +0000308 if type == tokenize.NEWLINE:
309 break
310 return startlineno, endlineno, slashes
311
312def chop(line):
313 if line.endswith("\n"):
314 return line[:-1]
315 else:
316 return line
317
318if __name__ == "__main__":
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000319 sys.exit(main())