17db96d56Sopenharmony_ci#! /usr/bin/env python3 27db96d56Sopenharmony_ci 37db96d56Sopenharmony_ci"""cleanfuture [-d][-r][-v] path ... 47db96d56Sopenharmony_ci 57db96d56Sopenharmony_ci-d Dry run. Analyze, but don't make any changes to, files. 67db96d56Sopenharmony_ci-r Recurse. Search for all .py files in subdirectories too. 77db96d56Sopenharmony_ci-v Verbose. Print informative msgs. 87db96d56Sopenharmony_ci 97db96d56Sopenharmony_ciSearch Python (.py) files for future statements, and remove the features 107db96d56Sopenharmony_cifrom such statements that are already mandatory in the version of Python 117db96d56Sopenharmony_ciyou're using. 127db96d56Sopenharmony_ci 137db96d56Sopenharmony_ciPass one or more file and/or directory paths. When a directory path, all 147db96d56Sopenharmony_ci.py files within the directory will be examined, and, if the -r option is 157db96d56Sopenharmony_cigiven, likewise recursively for subdirectories. 167db96d56Sopenharmony_ci 177db96d56Sopenharmony_ciOverwrites files in place, renaming the originals with a .bak extension. If 187db96d56Sopenharmony_cicleanfuture finds nothing to change, the file is left alone. If cleanfuture 197db96d56Sopenharmony_cidoes change a file, the changed file is a fixed-point (i.e., running 207db96d56Sopenharmony_cicleanfuture on the resulting .py file won't change it again, at least not 217db96d56Sopenharmony_ciuntil you try it again with a later Python release). 227db96d56Sopenharmony_ci 237db96d56Sopenharmony_ciLimitations: You can do these things, but this tool won't help you then: 247db96d56Sopenharmony_ci 257db96d56Sopenharmony_ci+ A future statement cannot be mixed with any other statement on the same 267db96d56Sopenharmony_ci physical line (separated by semicolon). 277db96d56Sopenharmony_ci 287db96d56Sopenharmony_ci+ A future statement cannot contain an "as" clause. 297db96d56Sopenharmony_ci 307db96d56Sopenharmony_ciExample: Assuming you're using Python 2.2, if a file containing 317db96d56Sopenharmony_ci 327db96d56Sopenharmony_cifrom __future__ import nested_scopes, generators 337db96d56Sopenharmony_ci 347db96d56Sopenharmony_ciis analyzed by cleanfuture, the line is rewritten to 357db96d56Sopenharmony_ci 367db96d56Sopenharmony_cifrom __future__ import generators 377db96d56Sopenharmony_ci 387db96d56Sopenharmony_cibecause nested_scopes is no longer optional in 2.2 but generators is. 397db96d56Sopenharmony_ci""" 407db96d56Sopenharmony_ci 417db96d56Sopenharmony_ciimport __future__ 427db96d56Sopenharmony_ciimport tokenize 437db96d56Sopenharmony_ciimport os 447db96d56Sopenharmony_ciimport sys 457db96d56Sopenharmony_ci 467db96d56Sopenharmony_cidryrun = 0 477db96d56Sopenharmony_cirecurse = 0 487db96d56Sopenharmony_civerbose = 0 497db96d56Sopenharmony_ci 507db96d56Sopenharmony_cidef errprint(*args): 517db96d56Sopenharmony_ci strings = map(str, args) 527db96d56Sopenharmony_ci msg = ' '.join(strings) 537db96d56Sopenharmony_ci if msg[-1:] != '\n': 547db96d56Sopenharmony_ci msg += '\n' 557db96d56Sopenharmony_ci sys.stderr.write(msg) 567db96d56Sopenharmony_ci 577db96d56Sopenharmony_cidef main(): 587db96d56Sopenharmony_ci import getopt 597db96d56Sopenharmony_ci global verbose, recurse, dryrun 607db96d56Sopenharmony_ci try: 617db96d56Sopenharmony_ci opts, args = getopt.getopt(sys.argv[1:], "drv") 627db96d56Sopenharmony_ci except getopt.error as msg: 637db96d56Sopenharmony_ci errprint(msg) 647db96d56Sopenharmony_ci return 657db96d56Sopenharmony_ci for o, a in opts: 667db96d56Sopenharmony_ci if o == '-d': 677db96d56Sopenharmony_ci dryrun += 1 687db96d56Sopenharmony_ci elif o == '-r': 697db96d56Sopenharmony_ci recurse += 1 707db96d56Sopenharmony_ci elif o == '-v': 717db96d56Sopenharmony_ci verbose += 1 727db96d56Sopenharmony_ci if not args: 737db96d56Sopenharmony_ci errprint("Usage:", __doc__) 747db96d56Sopenharmony_ci return 757db96d56Sopenharmony_ci for arg in args: 767db96d56Sopenharmony_ci check(arg) 777db96d56Sopenharmony_ci 787db96d56Sopenharmony_cidef check(file): 797db96d56Sopenharmony_ci if os.path.isdir(file) and not os.path.islink(file): 807db96d56Sopenharmony_ci if verbose: 817db96d56Sopenharmony_ci print("listing directory", file) 827db96d56Sopenharmony_ci names = os.listdir(file) 837db96d56Sopenharmony_ci for name in names: 847db96d56Sopenharmony_ci fullname = os.path.join(file, name) 857db96d56Sopenharmony_ci if ((recurse and os.path.isdir(fullname) and 867db96d56Sopenharmony_ci not os.path.islink(fullname)) 877db96d56Sopenharmony_ci or name.lower().endswith(".py")): 887db96d56Sopenharmony_ci check(fullname) 897db96d56Sopenharmony_ci return 907db96d56Sopenharmony_ci 917db96d56Sopenharmony_ci if verbose: 927db96d56Sopenharmony_ci print("checking", file, "...", end=' ') 937db96d56Sopenharmony_ci try: 947db96d56Sopenharmony_ci f = open(file) 957db96d56Sopenharmony_ci except IOError as msg: 967db96d56Sopenharmony_ci errprint("%r: I/O Error: %s" % (file, str(msg))) 977db96d56Sopenharmony_ci return 987db96d56Sopenharmony_ci 997db96d56Sopenharmony_ci with f: 1007db96d56Sopenharmony_ci ff = FutureFinder(f, file) 1017db96d56Sopenharmony_ci changed = ff.run() 1027db96d56Sopenharmony_ci if changed: 1037db96d56Sopenharmony_ci ff.gettherest() 1047db96d56Sopenharmony_ci if changed: 1057db96d56Sopenharmony_ci if verbose: 1067db96d56Sopenharmony_ci print("changed.") 1077db96d56Sopenharmony_ci if dryrun: 1087db96d56Sopenharmony_ci print("But this is a dry run, so leaving it alone.") 1097db96d56Sopenharmony_ci for s, e, line in changed: 1107db96d56Sopenharmony_ci print("%r lines %d-%d" % (file, s+1, e+1)) 1117db96d56Sopenharmony_ci for i in range(s, e+1): 1127db96d56Sopenharmony_ci print(ff.lines[i], end=' ') 1137db96d56Sopenharmony_ci if line is None: 1147db96d56Sopenharmony_ci print("-- deleted") 1157db96d56Sopenharmony_ci else: 1167db96d56Sopenharmony_ci print("-- change to:") 1177db96d56Sopenharmony_ci print(line, end=' ') 1187db96d56Sopenharmony_ci if not dryrun: 1197db96d56Sopenharmony_ci bak = file + ".bak" 1207db96d56Sopenharmony_ci if os.path.exists(bak): 1217db96d56Sopenharmony_ci os.remove(bak) 1227db96d56Sopenharmony_ci os.rename(file, bak) 1237db96d56Sopenharmony_ci if verbose: 1247db96d56Sopenharmony_ci print("renamed", file, "to", bak) 1257db96d56Sopenharmony_ci with open(file, "w") as g: 1267db96d56Sopenharmony_ci ff.write(g) 1277db96d56Sopenharmony_ci if verbose: 1287db96d56Sopenharmony_ci print("wrote new", file) 1297db96d56Sopenharmony_ci else: 1307db96d56Sopenharmony_ci if verbose: 1317db96d56Sopenharmony_ci print("unchanged.") 1327db96d56Sopenharmony_ci 1337db96d56Sopenharmony_ciclass FutureFinder: 1347db96d56Sopenharmony_ci 1357db96d56Sopenharmony_ci def __init__(self, f, fname): 1367db96d56Sopenharmony_ci self.f = f 1377db96d56Sopenharmony_ci self.fname = fname 1387db96d56Sopenharmony_ci self.ateof = 0 1397db96d56Sopenharmony_ci self.lines = [] # raw file lines 1407db96d56Sopenharmony_ci 1417db96d56Sopenharmony_ci # List of (start_index, end_index, new_line) triples. 1427db96d56Sopenharmony_ci self.changed = [] 1437db96d56Sopenharmony_ci 1447db96d56Sopenharmony_ci # Line-getter for tokenize. 1457db96d56Sopenharmony_ci def getline(self): 1467db96d56Sopenharmony_ci if self.ateof: 1477db96d56Sopenharmony_ci return "" 1487db96d56Sopenharmony_ci line = self.f.readline() 1497db96d56Sopenharmony_ci if line == "": 1507db96d56Sopenharmony_ci self.ateof = 1 1517db96d56Sopenharmony_ci else: 1527db96d56Sopenharmony_ci self.lines.append(line) 1537db96d56Sopenharmony_ci return line 1547db96d56Sopenharmony_ci 1557db96d56Sopenharmony_ci def run(self): 1567db96d56Sopenharmony_ci STRING = tokenize.STRING 1577db96d56Sopenharmony_ci NL = tokenize.NL 1587db96d56Sopenharmony_ci NEWLINE = tokenize.NEWLINE 1597db96d56Sopenharmony_ci COMMENT = tokenize.COMMENT 1607db96d56Sopenharmony_ci NAME = tokenize.NAME 1617db96d56Sopenharmony_ci OP = tokenize.OP 1627db96d56Sopenharmony_ci 1637db96d56Sopenharmony_ci changed = self.changed 1647db96d56Sopenharmony_ci get = tokenize.generate_tokens(self.getline).__next__ 1657db96d56Sopenharmony_ci type, token, (srow, scol), (erow, ecol), line = get() 1667db96d56Sopenharmony_ci 1677db96d56Sopenharmony_ci # Chew up initial comments and blank lines (if any). 1687db96d56Sopenharmony_ci while type in (COMMENT, NL, NEWLINE): 1697db96d56Sopenharmony_ci type, token, (srow, scol), (erow, ecol), line = get() 1707db96d56Sopenharmony_ci 1717db96d56Sopenharmony_ci # Chew up docstring (if any -- and it may be implicitly catenated!). 1727db96d56Sopenharmony_ci while type is STRING: 1737db96d56Sopenharmony_ci type, token, (srow, scol), (erow, ecol), line = get() 1747db96d56Sopenharmony_ci 1757db96d56Sopenharmony_ci # Analyze the future stmts. 1767db96d56Sopenharmony_ci while 1: 1777db96d56Sopenharmony_ci # Chew up comments and blank lines (if any). 1787db96d56Sopenharmony_ci while type in (COMMENT, NL, NEWLINE): 1797db96d56Sopenharmony_ci type, token, (srow, scol), (erow, ecol), line = get() 1807db96d56Sopenharmony_ci 1817db96d56Sopenharmony_ci if not (type is NAME and token == "from"): 1827db96d56Sopenharmony_ci break 1837db96d56Sopenharmony_ci startline = srow - 1 # tokenize is one-based 1847db96d56Sopenharmony_ci type, token, (srow, scol), (erow, ecol), line = get() 1857db96d56Sopenharmony_ci 1867db96d56Sopenharmony_ci if not (type is NAME and token == "__future__"): 1877db96d56Sopenharmony_ci break 1887db96d56Sopenharmony_ci type, token, (srow, scol), (erow, ecol), line = get() 1897db96d56Sopenharmony_ci 1907db96d56Sopenharmony_ci if not (type is NAME and token == "import"): 1917db96d56Sopenharmony_ci break 1927db96d56Sopenharmony_ci type, token, (srow, scol), (erow, ecol), line = get() 1937db96d56Sopenharmony_ci 1947db96d56Sopenharmony_ci # Get the list of features. 1957db96d56Sopenharmony_ci features = [] 1967db96d56Sopenharmony_ci while type is NAME: 1977db96d56Sopenharmony_ci features.append(token) 1987db96d56Sopenharmony_ci type, token, (srow, scol), (erow, ecol), line = get() 1997db96d56Sopenharmony_ci 2007db96d56Sopenharmony_ci if not (type is OP and token == ','): 2017db96d56Sopenharmony_ci break 2027db96d56Sopenharmony_ci type, token, (srow, scol), (erow, ecol), line = get() 2037db96d56Sopenharmony_ci 2047db96d56Sopenharmony_ci # A trailing comment? 2057db96d56Sopenharmony_ci comment = None 2067db96d56Sopenharmony_ci if type is COMMENT: 2077db96d56Sopenharmony_ci comment = token 2087db96d56Sopenharmony_ci type, token, (srow, scol), (erow, ecol), line = get() 2097db96d56Sopenharmony_ci 2107db96d56Sopenharmony_ci if type is not NEWLINE: 2117db96d56Sopenharmony_ci errprint("Skipping file %r; can't parse line %d:\n%s" % 2127db96d56Sopenharmony_ci (self.fname, srow, line)) 2137db96d56Sopenharmony_ci return [] 2147db96d56Sopenharmony_ci 2157db96d56Sopenharmony_ci endline = srow - 1 2167db96d56Sopenharmony_ci 2177db96d56Sopenharmony_ci # Check for obsolete features. 2187db96d56Sopenharmony_ci okfeatures = [] 2197db96d56Sopenharmony_ci for f in features: 2207db96d56Sopenharmony_ci object = getattr(__future__, f, None) 2217db96d56Sopenharmony_ci if object is None: 2227db96d56Sopenharmony_ci # A feature we don't know about yet -- leave it in. 2237db96d56Sopenharmony_ci # They'll get a compile-time error when they compile 2247db96d56Sopenharmony_ci # this program, but that's not our job to sort out. 2257db96d56Sopenharmony_ci okfeatures.append(f) 2267db96d56Sopenharmony_ci else: 2277db96d56Sopenharmony_ci released = object.getMandatoryRelease() 2287db96d56Sopenharmony_ci if released is None or released <= sys.version_info: 2297db96d56Sopenharmony_ci # Withdrawn or obsolete. 2307db96d56Sopenharmony_ci pass 2317db96d56Sopenharmony_ci else: 2327db96d56Sopenharmony_ci okfeatures.append(f) 2337db96d56Sopenharmony_ci 2347db96d56Sopenharmony_ci # Rewrite the line if at least one future-feature is obsolete. 2357db96d56Sopenharmony_ci if len(okfeatures) < len(features): 2367db96d56Sopenharmony_ci if len(okfeatures) == 0: 2377db96d56Sopenharmony_ci line = None 2387db96d56Sopenharmony_ci else: 2397db96d56Sopenharmony_ci line = "from __future__ import " 2407db96d56Sopenharmony_ci line += ', '.join(okfeatures) 2417db96d56Sopenharmony_ci if comment is not None: 2427db96d56Sopenharmony_ci line += ' ' + comment 2437db96d56Sopenharmony_ci line += '\n' 2447db96d56Sopenharmony_ci changed.append((startline, endline, line)) 2457db96d56Sopenharmony_ci 2467db96d56Sopenharmony_ci # Loop back for more future statements. 2477db96d56Sopenharmony_ci 2487db96d56Sopenharmony_ci return changed 2497db96d56Sopenharmony_ci 2507db96d56Sopenharmony_ci def gettherest(self): 2517db96d56Sopenharmony_ci if self.ateof: 2527db96d56Sopenharmony_ci self.therest = '' 2537db96d56Sopenharmony_ci else: 2547db96d56Sopenharmony_ci self.therest = self.f.read() 2557db96d56Sopenharmony_ci 2567db96d56Sopenharmony_ci def write(self, f): 2577db96d56Sopenharmony_ci changed = self.changed 2587db96d56Sopenharmony_ci assert changed 2597db96d56Sopenharmony_ci # Prevent calling this again. 2607db96d56Sopenharmony_ci self.changed = [] 2617db96d56Sopenharmony_ci # Apply changes in reverse order. 2627db96d56Sopenharmony_ci changed.reverse() 2637db96d56Sopenharmony_ci for s, e, line in changed: 2647db96d56Sopenharmony_ci if line is None: 2657db96d56Sopenharmony_ci # pure deletion 2667db96d56Sopenharmony_ci del self.lines[s:e+1] 2677db96d56Sopenharmony_ci else: 2687db96d56Sopenharmony_ci self.lines[s:e+1] = [line] 2697db96d56Sopenharmony_ci f.writelines(self.lines) 2707db96d56Sopenharmony_ci # Copy over the remainder of the file. 2717db96d56Sopenharmony_ci if self.therest: 2727db96d56Sopenharmony_ci f.write(self.therest) 2737db96d56Sopenharmony_ci 2747db96d56Sopenharmony_ciif __name__ == '__main__': 2757db96d56Sopenharmony_ci main() 276