Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 1 | #! /usr/bin/env python |
| 2 | |
| 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 | |
| 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 |
| 11 | you're using. |
| 12 | |
| 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. |
| 16 | |
| 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 |
Tim Peters | ebb7133 | 2001-08-15 06:07:42 +0000 | [diff] [blame] | 21 | until you try it again with a later Python release). |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 22 | |
| 23 | Limitations: 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 | |
| 30 | Example: Assuming you're using Python 2.2, if a file containing |
| 31 | |
| 32 | from __future__ import nested_scopes, generators |
| 33 | |
| 34 | is analyzed by cleanfuture, the line is rewritten to |
| 35 | |
| 36 | from __future__ import generators |
| 37 | |
| 38 | because nested_scopes is no longer optional in 2.2 but generators is. |
| 39 | """ |
| 40 | |
| 41 | import __future__ |
| 42 | import tokenize |
| 43 | import os |
| 44 | import sys |
| 45 | |
| 46 | dryrun = 0 |
| 47 | recurse = 0 |
| 48 | verbose = 0 |
| 49 | |
| 50 | def errprint(*args): |
| 51 | strings = map(str, args) |
Tim Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 52 | msg = ' '.join(strings) |
| 53 | if msg[-1:] != '\n': |
| 54 | msg += '\n' |
| 55 | sys.stderr.write(msg) |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 56 | |
| 57 | def main(): |
| 58 | import getopt |
| 59 | global verbose, recurse, dryrun |
| 60 | try: |
| 61 | opts, args = getopt.getopt(sys.argv[1:], "drv") |
| 62 | except getopt.error, msg: |
| 63 | 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 | |
| 78 | def check(file): |
| 79 | if os.path.isdir(file) and not os.path.islink(file): |
| 80 | if verbose: |
| 81 | print "listing directory", file |
| 82 | 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: |
| 92 | print "checking", file, "...", |
| 93 | try: |
| 94 | f = open(file) |
| 95 | except IOError, msg: |
| 96 | errprint("%r: I/O Error: %s" % (file, str(msg))) |
| 97 | return |
| 98 | |
Tim Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 99 | ff = FutureFinder(f, file) |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 100 | changed = ff.run() |
| 101 | if changed: |
Tim Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 102 | ff.gettherest() |
| 103 | f.close() |
| 104 | if changed: |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 105 | if verbose: |
| 106 | print "changed." |
| 107 | if dryrun: |
| 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): |
| 112 | print ff.lines[i], |
| 113 | if line is None: |
| 114 | print "-- deleted" |
| 115 | else: |
| 116 | print "-- change to:" |
| 117 | print line, |
| 118 | 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: |
| 124 | print "renamed", file, "to", bak |
Tim Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 125 | g = open(file, "w") |
| 126 | ff.write(g) |
| 127 | g.close() |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 128 | if verbose: |
| 129 | print "wrote new", file |
| 130 | else: |
| 131 | if verbose: |
| 132 | print "unchanged." |
| 133 | |
| 134 | class FutureFinder: |
| 135 | |
Tim Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 136 | def __init__(self, f, fname): |
| 137 | self.f = f |
| 138 | self.fname = fname |
| 139 | self.ateof = 0 |
| 140 | self.lines = [] # raw file lines |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 141 | |
| 142 | # List of (start_index, end_index, new_line) triples. |
| 143 | self.changed = [] |
| 144 | |
| 145 | # Line-getter for tokenize. |
| 146 | def getline(self): |
Tim Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 147 | if self.ateof: |
| 148 | return "" |
| 149 | line = self.f.readline() |
| 150 | if line == "": |
| 151 | self.ateof = 1 |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 152 | else: |
Tim Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 153 | self.lines.append(line) |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 154 | 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 Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 164 | changed = self.changed |
| 165 | get = tokenize.generate_tokens(self.getline).next |
| 166 | type, token, (srow, scol), (erow, ecol), line = get() |
| 167 | |
Tim Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 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() |
| 171 | |
| 172 | # Chew up docstring (if any -- and it may be implicitly catenated!). |
| 173 | while type is STRING: |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 174 | type, token, (srow, scol), (erow, ecol), line = get() |
| 175 | |
| 176 | # Analyze the future stmts. |
Tim Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 177 | 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 Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 184 | 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 Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 212 | errprint("Skipping file %r; can't parse line %d:\n%s" % |
| 213 | (self.fname, srow, line)) |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 214 | 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 Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 235 | # Rewrite the line if at least one future-feature is obsolete. |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 236 | if len(okfeatures) < len(features): |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 237 | 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 Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 247 | # Loop back for more future statements. |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 248 | |
| 249 | return changed |
| 250 | |
Tim Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 251 | def gettherest(self): |
| 252 | if self.ateof: |
| 253 | self.therest = '' |
| 254 | else: |
| 255 | self.therest = self.f.read() |
| 256 | |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 257 | 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 Peters | 3055ad2 | 2001-08-13 05:33:53 +0000 | [diff] [blame] | 271 | # Copy over the remainder of the file. |
| 272 | if self.therest: |
| 273 | f.write(self.therest) |
Tim Peters | b704238 | 2001-08-12 08:41:13 +0000 | [diff] [blame] | 274 | |
| 275 | if __name__ == '__main__': |
| 276 | main() |