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