summaryrefslogtreecommitdiff
path: root/Tools/scripts/cleanfuture.py
blob: b48ab60dd65becdecab973f84c6e4c70ca5a51af (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#! /usr/bin/env python3

"""cleanfuture [-d][-r][-v] path ...

-d  Dry run.  Analyze, but don't make any changes to, files.
-r  Recurse.  Search for all .py files in subdirectories too.
-v  Verbose.  Print informative msgs.

Search Python (.py) files for future statements, and remove the features
from such statements that are already mandatory in the version of Python
you're using.

Pass one or more file and/or directory paths.  When a directory path, all
.py files within the directory will be examined, and, if the -r option is
given, likewise recursively for subdirectories.

Overwrites files in place, renaming the originals with a .bak extension. If
cleanfuture finds nothing to change, the file is left alone.  If cleanfuture
does change a file, the changed file is a fixed-point (i.e., running
cleanfuture on the resulting .py file won't change it again, at least not
until you try it again with a later Python release).

Limitations:  You can do these things, but this tool won't help you then:

+ A future statement cannot be mixed with any other statement on the same
  physical line (separated by semicolon).

+ A future statement cannot contain an "as" clause.

Example:  Assuming you're using Python 2.2, if a file containing

from __future__ import nested_scopes, generators

is analyzed by cleanfuture, the line is rewritten to

from __future__ import generators

because nested_scopes is no longer optional in 2.2 but generators is.
"""

import __future__
import tokenize
import os
import sys

dryrun  = 0
recurse = 0
verbose = 0

def errprint(*args):
    strings = map(str, args)
    msg = ' '.join(strings)
    if msg[-1:] != '\n':
        msg += '\n'
    sys.stderr.write(msg)

def main():
    import getopt
    global verbose, recurse, dryrun
    try:
        opts, args = getopt.getopt(sys.argv[1:], "drv")
    except getopt.error as msg:
        errprint(msg)
        return
    for o, a in opts:
        if o == '-d':
            dryrun += 1
        elif o == '-r':
            recurse += 1
        elif o == '-v':
            verbose += 1
    if not args:
        errprint("Usage:", __doc__)
        return
    for arg in args:
        check(arg)

def check(file):
    if os.path.isdir(file) and not os.path.islink(file):
        if verbose:
            print("listing directory", file)
        names = os.listdir(file)
        for name in names:
            fullname = os.path.join(file, name)
            if ((recurse and os.path.isdir(fullname) and
                 not os.path.islink(fullname))
                or name.lower().endswith(".py")):
                check(fullname)
        return

    if verbose:
        print("checking", file, "...", end=' ')
    try:
        f = open(file)
    except IOError as msg:
        errprint("%r: I/O Error: %s" % (file, str(msg)))
        return

    ff = FutureFinder(f, file)
    changed = ff.run()
    if changed:
        ff.gettherest()
    f.close()
    if changed:
        if verbose:
            print("changed.")
            if dryrun:
                print("But this is a dry run, so leaving it alone.")
        for s, e, line in changed:
            print("%r lines %d-%d" % (file, s+1, e+1))
            for i in range(s, e+1):
                print(ff.lines[i], end=' ')
            if line is None:
                print("-- deleted")
            else:
                print("-- change to:")
                print(line, end=' ')
        if not dryrun:
            bak = file + ".bak"
            if os.path.exists(bak):
                os.remove(bak)
            os.rename(file, bak)
            if verbose:
                print("renamed", file, "to", bak)
            g = open(file, "w")
            ff.write(g)
            g.close()
            if verbose:
                print("wrote new", file)
    else:
        if verbose:
            print("unchanged.")

class FutureFinder:

    def __init__(self, f, fname):
        self.f = f
        self.fname = fname
        self.ateof = 0
        self.lines = [] # raw file lines

        # List of (start_index, end_index, new_line) triples.
        self.changed = []

    # Line-getter for tokenize.
    def getline(self):
        if self.ateof:
            return ""
        line = self.f.readline()
        if line == "":
            self.ateof = 1
        else:
            self.lines.append(line)
        return line

    def run(self):
        STRING = tokenize.STRING
        NL = tokenize.NL
        NEWLINE = tokenize.NEWLINE
        COMMENT = tokenize.COMMENT
        NAME = tokenize.NAME
        OP = tokenize.OP

        changed = self.changed
        get = tokenize.generate_tokens(self.getline).__next__
        type, token, (srow, scol), (erow, ecol), line = get()

        # Chew up initial comments and blank lines (if any).
        while type in (COMMENT, NL, NEWLINE):
            type, token, (srow, scol), (erow, ecol), line = get()

        # Chew up docstring (if any -- and it may be implicitly catenated!).
        while type is STRING:
            type, token, (srow, scol), (erow, ecol), line = get()

        # Analyze the future stmts.
        while 1:
            # Chew up comments and blank lines (if any).
            while type in (COMMENT, NL, NEWLINE):
                type, token, (srow, scol), (erow, ecol), line = get()

            if not (type is NAME and token == "from"):
                break
            startline = srow - 1    # tokenize is one-based
            type, token, (srow, scol), (erow, ecol), line = get()

            if not (type is NAME and token == "__future__"):
                break
            type, token, (srow, scol), (erow, ecol), line = get()

            if not (type is NAME and token == "import"):
                break
            type, token, (srow, scol), (erow, ecol), line = get()

            # Get the list of features.
            features = []
            while type is NAME:
                features.append(token)
                type, token, (srow, scol), (erow, ecol), line = get()

                if not (type is OP and token == ','):
                    break
                type, token, (srow, scol), (erow, ecol), line = get()

            # A trailing comment?
            comment = None
            if type is COMMENT:
                comment = token
                type, token, (srow, scol), (erow, ecol), line = get()

            if type is not NEWLINE:
                errprint("Skipping file %r; can't parse line %d:\n%s" %
                         (self.fname, srow, line))
                return []

            endline = srow - 1

            # Check for obsolete features.
            okfeatures = []
            for f in features:
                object = getattr(__future__, f, None)
                if object is None:
                    # A feature we don't know about yet -- leave it in.
                    # They'll get a compile-time error when they compile
                    # this program, but that's not our job to sort out.
                    okfeatures.append(f)
                else:
                    released = object.getMandatoryRelease()
                    if released is None or released <= sys.version_info:
                        # Withdrawn or obsolete.
                        pass
                    else:
                        okfeatures.append(f)

            # Rewrite the line if at least one future-feature is obsolete.
            if len(okfeatures) < len(features):
                if len(okfeatures) == 0:
                    line = None
                else:
                    line = "from __future__ import "
                    line += ', '.join(okfeatures)
                    if comment is not None:
                        line += ' ' + comment
                    line += '\n'
                changed.append((startline, endline, line))

            # Loop back for more future statements.

        return changed

    def gettherest(self):
        if self.ateof:
            self.therest = ''
        else:
            self.therest = self.f.read()

    def write(self, f):
        changed = self.changed
        assert changed
        # Prevent calling this again.
        self.changed = []
        # Apply changes in reverse order.
        changed.reverse()
        for s, e, line in changed:
            if line is None:
                # pure deletion
                del self.lines[s:e+1]
            else:
                self.lines[s:e+1] = [line]
        f.writelines(self.lines)
        # Copy over the remainder of the file.
        if self.therest:
            f.write(self.therest)

if __name__ == '__main__':
    main()