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