/Tools/scripts/cleanfuture.py

http://unladen-swallow.googlecode.com/ · Python · 276 lines · 253 code · 7 blank · 16 comment · 6 complexity · 5e039e3ec0eacc7c00bdbd82c52a82e3 MD5 · raw file

  1. #! /usr/bin/env python
  2. """cleanfuture [-d][-r][-v] path ...
  3. -d Dry run. Analyze, but don't make any changes to, files.
  4. -r Recurse. Search for all .py files in subdirectories too.
  5. -v Verbose. Print informative msgs.
  6. Search Python (.py) files for future statements, and remove the features
  7. from such statements that are already mandatory in the version of Python
  8. you're using.
  9. Pass one or more file and/or directory paths. When a directory path, all
  10. .py files within the directory will be examined, and, if the -r option is
  11. given, likewise recursively for subdirectories.
  12. Overwrites files in place, renaming the originals with a .bak extension. If
  13. cleanfuture finds nothing to change, the file is left alone. If cleanfuture
  14. does change a file, the changed file is a fixed-point (i.e., running
  15. cleanfuture on the resulting .py file won't change it again, at least not
  16. until you try it again with a later Python release).
  17. Limitations: You can do these things, but this tool won't help you then:
  18. + A future statement cannot be mixed with any other statement on the same
  19. physical line (separated by semicolon).
  20. + A future statement cannot contain an "as" clause.
  21. Example: Assuming you're using Python 2.2, if a file containing
  22. from __future__ import nested_scopes, generators
  23. is analyzed by cleanfuture, the line is rewritten to
  24. from __future__ import generators
  25. because nested_scopes is no longer optional in 2.2 but generators is.
  26. """
  27. import __future__
  28. import tokenize
  29. import os
  30. import sys
  31. dryrun = 0
  32. recurse = 0
  33. verbose = 0
  34. def errprint(*args):
  35. strings = map(str, args)
  36. msg = ' '.join(strings)
  37. if msg[-1:] != '\n':
  38. msg += '\n'
  39. sys.stderr.write(msg)
  40. def main():
  41. import getopt
  42. global verbose, recurse, dryrun
  43. try:
  44. opts, args = getopt.getopt(sys.argv[1:], "drv")
  45. except getopt.error, msg:
  46. errprint(msg)
  47. return
  48. for o, a in opts:
  49. if o == '-d':
  50. dryrun += 1
  51. elif o == '-r':
  52. recurse += 1
  53. elif o == '-v':
  54. verbose += 1
  55. if not args:
  56. errprint("Usage:", __doc__)
  57. return
  58. for arg in args:
  59. check(arg)
  60. def check(file):
  61. if os.path.isdir(file) and not os.path.islink(file):
  62. if verbose:
  63. print "listing directory", file
  64. names = os.listdir(file)
  65. for name in names:
  66. fullname = os.path.join(file, name)
  67. if ((recurse and os.path.isdir(fullname) and
  68. not os.path.islink(fullname))
  69. or name.lower().endswith(".py")):
  70. check(fullname)
  71. return
  72. if verbose:
  73. print "checking", file, "...",
  74. try:
  75. f = open(file)
  76. except IOError, msg:
  77. errprint("%r: I/O Error: %s" % (file, str(msg)))
  78. return
  79. ff = FutureFinder(f, file)
  80. changed = ff.run()
  81. if changed:
  82. ff.gettherest()
  83. f.close()
  84. if changed:
  85. if verbose:
  86. print "changed."
  87. if dryrun:
  88. print "But this is a dry run, so leaving it alone."
  89. for s, e, line in changed:
  90. print "%r lines %d-%d" % (file, s+1, e+1)
  91. for i in range(s, e+1):
  92. print ff.lines[i],
  93. if line is None:
  94. print "-- deleted"
  95. else:
  96. print "-- change to:"
  97. print line,
  98. if not dryrun:
  99. bak = file + ".bak"
  100. if os.path.exists(bak):
  101. os.remove(bak)
  102. os.rename(file, bak)
  103. if verbose:
  104. print "renamed", file, "to", bak
  105. g = open(file, "w")
  106. ff.write(g)
  107. g.close()
  108. if verbose:
  109. print "wrote new", file
  110. else:
  111. if verbose:
  112. print "unchanged."
  113. class FutureFinder:
  114. def __init__(self, f, fname):
  115. self.f = f
  116. self.fname = fname
  117. self.ateof = 0
  118. self.lines = [] # raw file lines
  119. # List of (start_index, end_index, new_line) triples.
  120. self.changed = []
  121. # Line-getter for tokenize.
  122. def getline(self):
  123. if self.ateof:
  124. return ""
  125. line = self.f.readline()
  126. if line == "":
  127. self.ateof = 1
  128. else:
  129. self.lines.append(line)
  130. return line
  131. def run(self):
  132. STRING = tokenize.STRING
  133. NL = tokenize.NL
  134. NEWLINE = tokenize.NEWLINE
  135. COMMENT = tokenize.COMMENT
  136. NAME = tokenize.NAME
  137. OP = tokenize.OP
  138. changed = self.changed
  139. get = tokenize.generate_tokens(self.getline).next
  140. type, token, (srow, scol), (erow, ecol), line = get()
  141. # Chew up initial comments and blank lines (if any).
  142. while type in (COMMENT, NL, NEWLINE):
  143. type, token, (srow, scol), (erow, ecol), line = get()
  144. # Chew up docstring (if any -- and it may be implicitly catenated!).
  145. while type is STRING:
  146. type, token, (srow, scol), (erow, ecol), line = get()
  147. # Analyze the future stmts.
  148. while 1:
  149. # Chew up comments and blank lines (if any).
  150. while type in (COMMENT, NL, NEWLINE):
  151. type, token, (srow, scol), (erow, ecol), line = get()
  152. if not (type is NAME and token == "from"):
  153. break
  154. startline = srow - 1 # tokenize is one-based
  155. type, token, (srow, scol), (erow, ecol), line = get()
  156. if not (type is NAME and token == "__future__"):
  157. break
  158. type, token, (srow, scol), (erow, ecol), line = get()
  159. if not (type is NAME and token == "import"):
  160. break
  161. type, token, (srow, scol), (erow, ecol), line = get()
  162. # Get the list of features.
  163. features = []
  164. while type is NAME:
  165. features.append(token)
  166. type, token, (srow, scol), (erow, ecol), line = get()
  167. if not (type is OP and token == ','):
  168. break
  169. type, token, (srow, scol), (erow, ecol), line = get()
  170. # A trailing comment?
  171. comment = None
  172. if type is COMMENT:
  173. comment = token
  174. type, token, (srow, scol), (erow, ecol), line = get()
  175. if type is not NEWLINE:
  176. errprint("Skipping file %r; can't parse line %d:\n%s" %
  177. (self.fname, srow, line))
  178. return []
  179. endline = srow - 1
  180. # Check for obsolete features.
  181. okfeatures = []
  182. for f in features:
  183. object = getattr(__future__, f, None)
  184. if object is None:
  185. # A feature we don't know about yet -- leave it in.
  186. # They'll get a compile-time error when they compile
  187. # this program, but that's not our job to sort out.
  188. okfeatures.append(f)
  189. else:
  190. released = object.getMandatoryRelease()
  191. if released is None or released <= sys.version_info:
  192. # Withdrawn or obsolete.
  193. pass
  194. else:
  195. okfeatures.append(f)
  196. # Rewrite the line if at least one future-feature is obsolete.
  197. if len(okfeatures) < len(features):
  198. if len(okfeatures) == 0:
  199. line = None
  200. else:
  201. line = "from __future__ import "
  202. line += ', '.join(okfeatures)
  203. if comment is not None:
  204. line += ' ' + comment
  205. line += '\n'
  206. changed.append((startline, endline, line))
  207. # Loop back for more future statements.
  208. return changed
  209. def gettherest(self):
  210. if self.ateof:
  211. self.therest = ''
  212. else:
  213. self.therest = self.f.read()
  214. def write(self, f):
  215. changed = self.changed
  216. assert changed
  217. # Prevent calling this again.
  218. self.changed = []
  219. # Apply changes in reverse order.
  220. changed.reverse()
  221. for s, e, line in changed:
  222. if line is None:
  223. # pure deletion
  224. del self.lines[s:e+1]
  225. else:
  226. self.lines[s:e+1] = [line]
  227. f.writelines(self.lines)
  228. # Copy over the remainder of the file.
  229. if self.therest:
  230. f.write(self.therest)
  231. if __name__ == '__main__':
  232. main()