|
|
|
|
|
"""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 |
|
|
|
with f: |
|
ff = FutureFinder(f, file) |
|
changed = ff.run() |
|
if changed: |
|
ff.gettherest() |
|
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) |
|
with open(file, "w") as g: |
|
ff.write(g) |
|
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 = [] |
|
|
|
|
|
self.changed = [] |
|
|
|
|
|
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() |
|
|
|
|
|
while type in (COMMENT, NL, NEWLINE): |
|
type, token, (srow, scol), (erow, ecol), line = get() |
|
|
|
|
|
while type is STRING: |
|
type, token, (srow, scol), (erow, ecol), line = get() |
|
|
|
|
|
while 1: |
|
|
|
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 |
|
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() |
|
|
|
|
|
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() |
|
|
|
|
|
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 |
|
|
|
|
|
okfeatures = [] |
|
for f in features: |
|
object = getattr(__future__, f, None) |
|
if object is None: |
|
|
|
|
|
|
|
okfeatures.append(f) |
|
else: |
|
released = object.getMandatoryRelease() |
|
if released is None or released <= sys.version_info: |
|
|
|
pass |
|
else: |
|
okfeatures.append(f) |
|
|
|
|
|
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)) |
|
|
|
|
|
|
|
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 |
|
|
|
self.changed = [] |
|
|
|
changed.reverse() |
|
for s, e, line in changed: |
|
if line is None: |
|
|
|
del self.lines[s:e+1] |
|
else: |
|
self.lines[s:e+1] = [line] |
|
f.writelines(self.lines) |
|
|
|
if self.therest: |
|
f.write(self.therest) |
|
|
|
if __name__ == '__main__': |
|
main() |
|
|