| #! /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 |
| |
| 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 = [] # 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() |