1#! /usr/bin/env python3 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 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 21until you try it again with a later Python release). 22 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) 52 msg = ' '.join(strings) 53 if msg[-1:] != '\n': 54 msg += '\n' 55 sys.stderr.write(msg) 56 57def main(): 58 import getopt 59 global verbose, recurse, dryrun 60 try: 61 opts, args = getopt.getopt(sys.argv[1:], "drv") 62 except getopt.error as 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 78def 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, "...", end=' ') 93 try: 94 f = open(file) 95 except IOError as msg: 96 errprint("%r: I/O Error: %s" % (file, str(msg))) 97 return 98 99 with f: 100 ff = FutureFinder(f, file) 101 changed = ff.run() 102 if changed: 103 ff.gettherest() 104 if changed: 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], end=' ') 113 if line is None: 114 print("-- deleted") 115 else: 116 print("-- change to:") 117 print(line, end=' ') 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) 125 with open(file, "w") as g: 126 ff.write(g) 127 if verbose: 128 print("wrote new", file) 129 else: 130 if verbose: 131 print("unchanged.") 132 133class FutureFinder: 134 135 def __init__(self, f, fname): 136 self.f = f 137 self.fname = fname 138 self.ateof = 0 139 self.lines = [] # raw file lines 140 141 # List of (start_index, end_index, new_line) triples. 142 self.changed = [] 143 144 # Line-getter for tokenize. 145 def getline(self): 146 if self.ateof: 147 return "" 148 line = self.f.readline() 149 if line == "": 150 self.ateof = 1 151 else: 152 self.lines.append(line) 153 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 163 changed = self.changed 164 get = tokenize.generate_tokens(self.getline).__next__ 165 type, token, (srow, scol), (erow, ecol), line = get() 166 167 # 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: 173 type, token, (srow, scol), (erow, ecol), line = get() 174 175 # Analyze the future stmts. 176 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 183 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: 211 errprint("Skipping file %r; can't parse line %d:\n%s" % 212 (self.fname, srow, line)) 213 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 234 # Rewrite the line if at least one future-feature is obsolete. 235 if len(okfeatures) < len(features): 236 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 246 # Loop back for more future statements. 247 248 return changed 249 250 def gettherest(self): 251 if self.ateof: 252 self.therest = '' 253 else: 254 self.therest = self.f.read() 255 256 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) 270 # Copy over the remainder of the file. 271 if self.therest: 272 f.write(self.therest) 273 274if __name__ == '__main__': 275 main() 276