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    ff = FutureFinder(f, file)
100    changed = ff.run()
101    if changed:
102        ff.gettherest()
103    f.close()
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            g = open(file, "w")
126            ff.write(g)
127            g.close()
128            if verbose:
129                print("wrote new", file)
130    else:
131        if verbose:
132            print("unchanged.")
133
134class FutureFinder:
135
136    def __init__(self, f, fname):
137        self.f = f
138        self.fname = fname
139        self.ateof = 0
140        self.lines = [] # raw file lines
141
142        # List of (start_index, end_index, new_line) triples.
143        self.changed = []
144
145    # Line-getter for tokenize.
146    def getline(self):
147        if self.ateof:
148            return ""
149        line = self.f.readline()
150        if line == "":
151            self.ateof = 1
152        else:
153            self.lines.append(line)
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
164        changed = self.changed
165        get = tokenize.generate_tokens(self.getline).__next__
166        type, token, (srow, scol), (erow, ecol), line = get()
167
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:
174            type, token, (srow, scol), (erow, ecol), line = get()
175
176        # Analyze the future stmts.
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
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:
212                errprint("Skipping file %r; can't parse line %d:\n%s" %
213                         (self.fname, srow, line))
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
235            # Rewrite the line if at least one future-feature is obsolete.
236            if len(okfeatures) < len(features):
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
247            # Loop back for more future statements.
248
249        return changed
250
251    def gettherest(self):
252        if self.ateof:
253            self.therest = ''
254        else:
255            self.therest = self.f.read()
256
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)
271        # Copy over the remainder of the file.
272        if self.therest:
273            f.write(self.therest)
274
275if __name__ == '__main__':
276    main()
277