| | |
| |
|
| | """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() |
| |
|