| #! /usr/bin/env python | |
| """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, 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, "...", | |
| try: | |
| f = open(file) | |
| except IOError, 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], | |
| if line is None: | |
| print "-- deleted" | |
| else: | |
| print "-- change to:" | |
| print line, | |
| 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() |