blob: c6396d88a12253e12228b0be9c97a76d519a0dfd [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
Guido van Rossum1832de42001-09-04 03:51:09 +00005To use this tool, first run `python -Qwarnall yourscript.py 2>warnings'.
Guido van Rossum822218b2001-09-01 21:55:58 +00006This 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.
Guido van Rossume7a95982001-09-02 14:11:30 +000065 The -m option issues warnings for these cases instead.
Guido van Rossum13c51ec2001-09-02 04:49:36 +000066
67Notes:
68
69- The augmented assignment operator /= is handled the same way as the
70 / operator.
71
72- This tool never looks at the // operator; no warnings are ever
73 generated for use of this operator.
74
75- This tool never looks at the / operator when a future division
76 statement is in effect; no warnings are generated in this case, and
77 because the tool only looks at files for which at least one classic
78 division warning was seen, it will never look at files containing a
79 future division statement.
80
81- Warnings may be issued for code not read from a file, but executed
82 using an exec statement or the eval() function. These will have
83 <string> in the filename position. The fixdiv script will attempt
84 and fail to open a file named "<string>", and issue a warning about
85 this failure. You're on your own to deal with this. You could make
86 all recommended changes and add a future division statement to all
87 affected files, and then re-run the test script; it should not issue
88 any warnings. If there are any, and you have a hard time tracking
89 down where they are generated, you can use the -Werror option to
90 force an error instead of a first warning, generating a traceback.
91
92- The tool should be run from the same directory as that from which
93 the original script was run, otherwise it won't be able to open
94 files given by relative pathnames.
Guido van Rossum822218b2001-09-01 21:55:58 +000095"""
96
97import sys
98import getopt
99import re
100import tokenize
101from pprint import pprint
102
Guido van Rossume7a95982001-09-02 14:11:30 +0000103multi_ok = 1
104
Guido van Rossum822218b2001-09-01 21:55:58 +0000105def main():
106 try:
Guido van Rossume7a95982001-09-02 14:11:30 +0000107 opts, args = getopt.getopt(sys.argv[1:], "hm")
Guido van Rossum822218b2001-09-01 21:55:58 +0000108 except getopt.error, msg:
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000109 usage(msg)
110 return 2
Guido van Rossum822218b2001-09-01 21:55:58 +0000111 for o, a in opts:
112 if o == "-h":
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000113 print __doc__
114 return
Guido van Rossume7a95982001-09-02 14:11:30 +0000115 if o == "-m":
116 global multi_ok
117 multi_ok = 0
Guido van Rossum822218b2001-09-01 21:55:58 +0000118 if not args:
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000119 usage("at least one file argument is required")
120 return 2
Guido van Rossum822218b2001-09-01 21:55:58 +0000121 if args[1:]:
122 sys.stderr.write("%s: extra file arguments ignored\n", sys.argv[0])
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000123 warnings = readwarnings(args[0])
124 if warnings is None:
125 return 1
126 files = warnings.keys()
127 if not files:
128 print "No classic division warnings read from", args[0]
129 return
130 files.sort()
131 exit = None
132 for file in files:
133 x = process(file, warnings[file])
134 exit = exit or x
135 return exit
Guido van Rossum822218b2001-09-01 21:55:58 +0000136
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000137def usage(msg):
138 sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
Guido van Rossume7a95982001-09-02 14:11:30 +0000139 sys.stderr.write("Usage: %s [-m] warnings\n" % sys.argv[0])
Guido van Rossum822218b2001-09-01 21:55:58 +0000140 sys.stderr.write("Try `%s -h' for more information.\n" % sys.argv[0])
Guido van Rossum822218b2001-09-01 21:55:58 +0000141
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000142PATTERN = ("^(.+?):(\d+): DeprecationWarning: "
143 "classic (int|long|float|complex) division$")
Guido van Rossum822218b2001-09-01 21:55:58 +0000144
145def readwarnings(warningsfile):
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000146 prog = re.compile(PATTERN)
Guido van Rossum822218b2001-09-01 21:55:58 +0000147 try:
148 f = open(warningsfile)
149 except IOError, msg:
150 sys.stderr.write("can't open: %s\n" % msg)
151 return
152 warnings = {}
153 while 1:
154 line = f.readline()
155 if not line:
156 break
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000157 m = prog.match(line)
Guido van Rossum822218b2001-09-01 21:55:58 +0000158 if not m:
159 if line.find("division") >= 0:
160 sys.stderr.write("Warning: ignored input " + line)
161 continue
162 file, lineno, what = m.groups()
163 list = warnings.get(file)
164 if list is None:
165 warnings[file] = list = []
166 list.append((int(lineno), intern(what)))
167 f.close()
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000168 return warnings
Guido van Rossum822218b2001-09-01 21:55:58 +0000169
170def process(file, list):
171 print "-"*70
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000172 assert list # if this fails, readwarnings() is broken
Guido van Rossum822218b2001-09-01 21:55:58 +0000173 try:
174 fp = open(file)
175 except IOError, msg:
176 sys.stderr.write("can't open: %s\n" % msg)
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000177 return 1
178 print "Index:", file
Guido van Rossum822218b2001-09-01 21:55:58 +0000179 f = FileContext(fp)
180 list.sort()
181 index = 0 # list[:index] has been processed, list[index:] is still to do
Guido van Rossum822218b2001-09-01 21:55:58 +0000182 g = tokenize.generate_tokens(f.readline)
183 while 1:
184 startlineno, endlineno, slashes = lineinfo = scanline(g)
185 if startlineno is None:
186 break
187 assert startlineno <= endlineno is not None
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000188 orphans = []
Guido van Rossum822218b2001-09-01 21:55:58 +0000189 while index < len(list) and list[index][0] < startlineno:
190 orphans.append(list[index])
191 index += 1
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000192 if orphans:
193 reportphantomwarnings(orphans, f)
Guido van Rossum822218b2001-09-01 21:55:58 +0000194 warnings = []
195 while index < len(list) and list[index][0] <= endlineno:
196 warnings.append(list[index])
197 index += 1
198 if not slashes and not warnings:
199 pass
200 elif slashes and not warnings:
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000201 report(slashes, "No conclusive evidence")
Guido van Rossum822218b2001-09-01 21:55:58 +0000202 elif warnings and not slashes:
203 reportphantomwarnings(warnings, f)
204 else:
205 if len(slashes) > 1:
Guido van Rossume7a95982001-09-02 14:11:30 +0000206 if not multi_ok:
207 report(slashes, "More than one / operator per statement")
208 continue
209 intlong = []
210 floatcomplex = []
211 bad = []
212 for lineno, what in warnings:
213 if what in ("int", "long"):
214 intlong.append(what)
215 elif what in ("float", "complex"):
216 floatcomplex.append(what)
217 else:
218 bad.append(what)
219 lastrow = None
220 for (row, col), line in slashes:
221 if row == lastrow:
222 continue
223 lastrow = row
Guido van Rossum822218b2001-09-01 21:55:58 +0000224 line = chop(line)
225 if line[col:col+1] != "/":
226 print "*** Can't find the / operator in line %d:" % row
227 print "*", line
228 continue
Guido van Rossum822218b2001-09-01 21:55:58 +0000229 if bad:
230 print "*** Bad warning for line %d:" % row, bad
231 print "*", line
232 elif intlong and not floatcomplex:
233 print "%dc%d" % (row, row)
234 print "<", line
235 print "---"
236 print ">", line[:col] + "/" + line[col:]
237 elif floatcomplex and not intlong:
238 print "True division / operator at line %d:" % row
239 print "=", line
240 fp.close()
241
242def reportphantomwarnings(warnings, f):
243 blocks = []
244 lastrow = None
245 lastblock = None
246 for row, what in warnings:
247 if row != lastrow:
248 lastblock = [row]
249 blocks.append(lastblock)
250 lastblock.append(what)
251 for block in blocks:
252 row = block[0]
253 whats = "/".join(block[1:])
254 print "*** Phantom %s warnings for line %d:" % (whats, row)
255 f.report(row, mark="*")
256
257def report(slashes, message):
258 lastrow = None
259 for (row, col), line in slashes:
260 if row != lastrow:
261 print "*** %s on line %d:" % (message, row)
262 print "*", chop(line)
263 lastrow = row
264
265class FileContext:
266 def __init__(self, fp, window=5, lineno=1):
267 self.fp = fp
268 self.window = 5
269 self.lineno = 1
270 self.eoflookahead = 0
271 self.lookahead = []
272 self.buffer = []
273 def fill(self):
274 while len(self.lookahead) < self.window and not self.eoflookahead:
275 line = self.fp.readline()
276 if not line:
277 self.eoflookahead = 1
278 break
279 self.lookahead.append(line)
280 def readline(self):
281 self.fill()
282 if not self.lookahead:
283 return ""
284 line = self.lookahead.pop(0)
285 self.buffer.append(line)
286 self.lineno += 1
287 return line
288 def truncate(self):
289 del self.buffer[-window:]
290 def __getitem__(self, index):
291 self.fill()
292 bufstart = self.lineno - len(self.buffer)
293 lookend = self.lineno + len(self.lookahead)
294 if bufstart <= index < self.lineno:
295 return self.buffer[index - bufstart]
296 if self.lineno <= index < lookend:
297 return self.lookahead[index - self.lineno]
298 raise KeyError
299 def report(self, first, last=None, mark="*"):
300 if last is None:
301 last = first
302 for i in range(first, last+1):
303 try:
304 line = self[first]
305 except KeyError:
306 line = "<missing line>"
307 print mark, chop(line)
308
309def scanline(g):
310 slashes = []
311 startlineno = None
312 endlineno = None
313 for type, token, start, end, line in g:
314 endlineno = end[0]
315 if startlineno is None:
316 startlineno = endlineno
317 if token in ("/", "/="):
318 slashes.append((start, line))
Guido van Rossum822218b2001-09-01 21:55:58 +0000319 if type == tokenize.NEWLINE:
320 break
321 return startlineno, endlineno, slashes
322
323def chop(line):
324 if line.endswith("\n"):
325 return line[:-1]
326 else:
327 return line
328
329if __name__ == "__main__":
Guido van Rossum13c51ec2001-09-02 04:49:36 +0000330 sys.exit(main())