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 |
| 21 | until you try it again with a m later Python release). |
| 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) |
| 52 | sys.stderr.write(' '.join(strings)) |
| 53 | sys.stderr.write("\n") |
| 54 | |
| 55 | def main(): |
| 56 | import getopt |
| 57 | global verbose, recurse, dryrun |
| 58 | try: |
| 59 | opts, args = getopt.getopt(sys.argv[1:], "drv") |
| 60 | except getopt.error, msg: |
| 61 | errprint(msg) |
| 62 | return |
| 63 | for o, a in opts: |
| 64 | if o == '-d': |
| 65 | dryrun += 1 |
| 66 | elif o == '-r': |
| 67 | recurse += 1 |
| 68 | elif o == '-v': |
| 69 | verbose += 1 |
| 70 | if not args: |
| 71 | errprint("Usage:", __doc__) |
| 72 | return |
| 73 | for arg in args: |
| 74 | check(arg) |
| 75 | |
| 76 | def check(file): |
| 77 | if os.path.isdir(file) and not os.path.islink(file): |
| 78 | if verbose: |
| 79 | print "listing directory", file |
| 80 | names = os.listdir(file) |
| 81 | for name in names: |
| 82 | fullname = os.path.join(file, name) |
| 83 | if ((recurse and os.path.isdir(fullname) and |
| 84 | not os.path.islink(fullname)) |
| 85 | or name.lower().endswith(".py")): |
| 86 | check(fullname) |
| 87 | return |
| 88 | |
| 89 | if verbose: |
| 90 | print "checking", file, "...", |
| 91 | try: |
| 92 | f = open(file) |
| 93 | except IOError, msg: |
| 94 | errprint("%r: I/O Error: %s" % (file, str(msg))) |
| 95 | return |
| 96 | |
| 97 | ff = FutureFinder(f) |
| 98 | f.close() |
| 99 | changed = ff.run() |
| 100 | if changed: |
| 101 | if verbose: |
| 102 | print "changed." |
| 103 | if dryrun: |
| 104 | print "But this is a dry run, so leaving it alone." |
| 105 | for s, e, line in changed: |
| 106 | print "%r lines %d-%d" % (file, s+1, e+1) |
| 107 | for i in range(s, e+1): |
| 108 | print ff.lines[i], |
| 109 | if line is None: |
| 110 | print "-- deleted" |
| 111 | else: |
| 112 | print "-- change to:" |
| 113 | print line, |
| 114 | if not dryrun: |
| 115 | bak = file + ".bak" |
| 116 | if os.path.exists(bak): |
| 117 | os.remove(bak) |
| 118 | os.rename(file, bak) |
| 119 | if verbose: |
| 120 | print "renamed", file, "to", bak |
| 121 | f = open(file, "w") |
| 122 | ff.write(f) |
| 123 | f.close() |
| 124 | if verbose: |
| 125 | print "wrote new", file |
| 126 | else: |
| 127 | if verbose: |
| 128 | print "unchanged." |
| 129 | |
| 130 | class FutureFinder: |
| 131 | |
| 132 | def __init__(self, f): |
| 133 | # Raw file lines. |
| 134 | self.lines = f.readlines() |
| 135 | self.index = 0 # index into self.lines of next line |
| 136 | |
| 137 | # List of (start_index, end_index, new_line) triples. |
| 138 | self.changed = [] |
| 139 | |
| 140 | # Line-getter for tokenize. |
| 141 | def getline(self): |
| 142 | if self.index >= len(self.lines): |
| 143 | line = "" |
| 144 | else: |
| 145 | line = self.lines[self.index] |
| 146 | self.index += 1 |
| 147 | return line |
| 148 | |
| 149 | def run(self): |
| 150 | STRING = tokenize.STRING |
| 151 | NL = tokenize.NL |
| 152 | NEWLINE = tokenize.NEWLINE |
| 153 | COMMENT = tokenize.COMMENT |
| 154 | NAME = tokenize.NAME |
| 155 | OP = tokenize.OP |
| 156 | |
| 157 | saw_string = 0 |
| 158 | changed = self.changed |
| 159 | get = tokenize.generate_tokens(self.getline).next |
| 160 | type, token, (srow, scol), (erow, ecol), line = get() |
| 161 | |
| 162 | # Chew up initial comments, blank lines, and docstring (if any). |
| 163 | while type in (COMMENT, NL, NEWLINE, STRING): |
| 164 | if type is STRING: |
| 165 | if saw_string: |
| 166 | return changed |
| 167 | saw_string = 1 |
| 168 | type, token, (srow, scol), (erow, ecol), line = get() |
| 169 | |
| 170 | # Analyze the future stmts. |
| 171 | while type is NAME and token == "from": |
| 172 | startline = srow - 1 # tokenize is one-based |
| 173 | type, token, (srow, scol), (erow, ecol), line = get() |
| 174 | |
| 175 | if not (type is NAME and token == "__future__"): |
| 176 | break |
| 177 | type, token, (srow, scol), (erow, ecol), line = get() |
| 178 | |
| 179 | if not (type is NAME and token == "import"): |
| 180 | break |
| 181 | type, token, (srow, scol), (erow, ecol), line = get() |
| 182 | |
| 183 | # Get the list of features. |
| 184 | features = [] |
| 185 | while type is NAME: |
| 186 | features.append(token) |
| 187 | type, token, (srow, scol), (erow, ecol), line = get() |
| 188 | |
| 189 | if not (type is OP and token == ','): |
| 190 | break |
| 191 | type, token, (srow, scol), (erow, ecol), line = get() |
| 192 | |
| 193 | # A trailing comment? |
| 194 | comment = None |
| 195 | if type is COMMENT: |
| 196 | comment = token |
| 197 | type, token, (srow, scol), (erow, ecol), line = get() |
| 198 | |
| 199 | if type is not NEWLINE: |
| 200 | errprint("Skipping file; can't parse line:\n", line) |
| 201 | return [] |
| 202 | |
| 203 | endline = srow - 1 |
| 204 | |
| 205 | # Check for obsolete features. |
| 206 | okfeatures = [] |
| 207 | for f in features: |
| 208 | object = getattr(__future__, f, None) |
| 209 | if object is None: |
| 210 | # A feature we don't know about yet -- leave it in. |
| 211 | # They'll get a compile-time error when they compile |
| 212 | # this program, but that's not our job to sort out. |
| 213 | okfeatures.append(f) |
| 214 | else: |
| 215 | released = object.getMandatoryRelease() |
| 216 | if released is None or released <= sys.version_info: |
| 217 | # Withdrawn or obsolete. |
| 218 | pass |
| 219 | else: |
| 220 | okfeatures.append(f) |
| 221 | |
| 222 | if len(okfeatures) < len(features): |
| 223 | # At least one future-feature is obsolete. |
| 224 | if len(okfeatures) == 0: |
| 225 | line = None |
| 226 | else: |
| 227 | line = "from __future__ import " |
| 228 | line += ', '.join(okfeatures) |
| 229 | if comment is not None: |
| 230 | line += ' ' + comment |
| 231 | line += '\n' |
| 232 | changed.append((startline, endline, line)) |
| 233 | |
| 234 | # Chew up comments and blank lines (if any). |
| 235 | while type in (COMMENT, NL, NEWLINE): |
| 236 | type, token, (srow, scol), (erow, ecol), line = get() |
| 237 | |
| 238 | return changed |
| 239 | |
| 240 | def write(self, f): |
| 241 | changed = self.changed |
| 242 | assert changed |
| 243 | # Prevent calling this again. |
| 244 | self.changed = [] |
| 245 | # Apply changes in reverse order. |
| 246 | changed.reverse() |
| 247 | for s, e, line in changed: |
| 248 | if line is None: |
| 249 | # pure deletion |
| 250 | del self.lines[s:e+1] |
| 251 | else: |
| 252 | self.lines[s:e+1] = [line] |
| 253 | f.writelines(self.lines) |
| 254 | |
| 255 | if __name__ == '__main__': |
| 256 | main() |