summaryrefslogtreecommitdiffstats
path: root/Tools/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/scripts')
-rw-r--r--Tools/scripts/cleanfuture.py256
1 files changed, 256 insertions, 0 deletions
diff --git a/Tools/scripts/cleanfuture.py b/Tools/scripts/cleanfuture.py
new file mode 100644
index 0000000..7ef5d54
--- /dev/null
+++ b/Tools/scripts/cleanfuture.py
@@ -0,0 +1,256 @@
+#! /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 m 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)
+ sys.stderr.write(' '.join(strings))
+ sys.stderr.write("\n")
+
+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)
+ f.close()
+ changed = ff.run()
+ 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
+ f = open(file, "w")
+ ff.write(f)
+ f.close()
+ if verbose:
+ print "wrote new", file
+ else:
+ if verbose:
+ print "unchanged."
+
+class FutureFinder:
+
+ def __init__(self, f):
+ # Raw file lines.
+ self.lines = f.readlines()
+ self.index = 0 # index into self.lines of next line
+
+ # List of (start_index, end_index, new_line) triples.
+ self.changed = []
+
+ # Line-getter for tokenize.
+ def getline(self):
+ if self.index >= len(self.lines):
+ line = ""
+ else:
+ line = self.lines[self.index]
+ self.index += 1
+ return line
+
+ def run(self):
+ STRING = tokenize.STRING
+ NL = tokenize.NL
+ NEWLINE = tokenize.NEWLINE
+ COMMENT = tokenize.COMMENT
+ NAME = tokenize.NAME
+ OP = tokenize.OP
+
+ saw_string = 0
+ changed = self.changed
+ get = tokenize.generate_tokens(self.getline).next
+ type, token, (srow, scol), (erow, ecol), line = get()
+
+ # Chew up initial comments, blank lines, and docstring (if any).
+ while type in (COMMENT, NL, NEWLINE, STRING):
+ if type is STRING:
+ if saw_string:
+ return changed
+ saw_string = 1
+ type, token, (srow, scol), (erow, ecol), line = get()
+
+ # Analyze the future stmts.
+ while type is NAME and token == "from":
+ 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; can't parse line:\n", 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)
+
+ if len(okfeatures) < len(features):
+ # At least one future-feature is obsolete.
+ 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))
+
+ # Chew up comments and blank lines (if any).
+ while type in (COMMENT, NL, NEWLINE):
+ type, token, (srow, scol), (erow, ecol), line = get()
+
+ return changed
+
+ 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)
+
+if __name__ == '__main__':
+ main()