blob: 9c56a89dc920d1f0318005e5c4546d8ffbade489 [file] [log] [blame]
Guido van Rossum2c4d7e71991-12-26 12:58:17 +00001#! /usr/local/python
2
3# Fix Python source files to use the new class definition syntax,
4# i.e.,
5# class C(base, base, ...): ...
6# instead of
7# class C() = base(), base(), ...: ...
8#
9# Command line arguments are files or directories to be processed.
10# Directories are searched recursively for files whose name looks
11# like a python module.
12# Symbolic links are always ignored (except as explicit directory
13# arguments). Of course, the original file is kept as a back-up
14# (with a "~" attached to its name).
15#
16# Undoubtedly you can do this using find and sed, but this is
17# a nice example of Python code that recurses down a directory tree
18# and uses regular expressions. Also note several subtleties like
19# preserving the file's mode and avoiding to even write a temp file
20# when no changes are needed for a file.
21#
22# Changes made are reported to stdout in a diff-like format.
23
24import sys
25import regexp
26import posix
27import path
28import string
29from stat import *
30
31err = sys.stderr.write
32dbg = err
33rep = sys.stdout.write
34
35def main():
36 bad = 0
37 if not sys.argv[1:]: # No arguments
38 err('usage: classfix file-or-directory ...\n')
39 sys.exit(2)
40 for arg in sys.argv[1:]:
41 if path.isdir(arg):
42 if recursedown(arg): bad = 1
43 elif path.islink(arg):
44 err(arg + ': will not process symbolic links\n')
45 bad = 1
46 else:
47 if fix(arg): bad = 1
48 sys.exit(bad)
49
50ispython = regexp.compile('^[a-zA-Z0-9_]+\.py$').match # This is a method!
51
52def recursedown(dirname):
53 dbg('recursedown(' + `dirname` + ')\n')
54 bad = 0
55 try:
56 names = posix.listdir(dirname)
57 except posix.error, msg:
58 err(dirname + ': cannot list directory: ' + `msg` + '\n')
59 return 1
60 for name in names:
61 if name in ('.', '..'): continue
62 fullname = path.join(dirname, name)
63 if path.islink(fullname): pass
64 elif path.isdir(fullname):
65 if recursedown(fullname): bad = 1
66 elif ispython(name):
67 if fix(fullname): bad = 1
68 return bad
69
70# This expression doesn't catch *all* class definition headers,
71# but it's darn pretty close.
72classexpr = '^([ \t]*class +[a-zA-Z0-9_]+) *\( *\) *((=.*)?):'
73findclass = regexp.compile(classexpr).match # This is a method!
74
75baseexpr = '^ *(.*) *\( *\) *$'
76findbase = regexp.compile(baseexpr).match # This is a method, too!
77
78def fix(filename):
79## dbg('fix(' + `filename` + ')\n')
80 try:
81 f = open(filename, 'r')
82 except IOError, msg:
83 err(filename + ': cannot open: ' + `msg` + '\n')
84 return 1
85 head, tail = path.split(filename)
86 tempname = path.join(head, '@' + tail)
87 tf = None
88 # If we find a match, we rewind the file and start over but
89 # now copy everything to a temp file.
90 while 1:
91 line = f.readline()
92 if not line: break
93 res = findclass(line)
94 if not res:
95 if tf: tf.write(line)
96 continue
97 if not tf:
98 try:
99 tf = open(tempname, 'w')
100 except IOError, msg:
101 f.close()
102 err(tempname+': cannot create: '+`msg`+'\n')
103 return 1
104 rep(filename + ':\n')
105 # Rewind the input file and start all over:
106 f.seek(0)
107 continue
108 a0, b0 = res[0] # Whole match (up to ':')
109 a1, b1 = res[1] # First subexpression (up to classname)
110 a2, b2 = res[2] # Second subexpression (=.*)
111 head = line[:b1]
112 tail = line[b0:] # Unmatched rest of line
113 if a2 = b2: # No base classes -- easy case
114 newline = head + ':' + tail
115 else:
116 # Get rid of leading '='
117 basepart = line[a2+1:b2]
118 # Extract list of base expressions
119 bases = string.splitfields(basepart, ',')
120 # Strip trailing '()' from each base expression
121 for i in range(len(bases)):
122 res = findbase(bases[i])
123 if res:
124 (x0, y0), (x1, y1) = res
125 bases[i] = bases[i][x1:y1]
126 # Join the bases back again and build the new line
127 basepart = string.joinfields(bases, ', ')
128 newline = head + '(' + basepart + '):' + tail
129 rep('< ' + line)
130 rep('> ' + newline)
131 tf.write(newline)
132 f.close()
133 if not tf: return 0 # No changes
134
135 # Finishing touch -- move files
136
137 # First copy the file's mode to the temp file
138 try:
139 statbuf = posix.stat(filename)
Guido van Rossum7e73fd01991-12-26 13:23:22 +0000140 posix.chmod(tempname, statbuf[ST_MODE] & 07777)
Guido van Rossum2c4d7e71991-12-26 12:58:17 +0000141 except posix.error, msg:
142 err(tempname + ': warning: chmod failed (' + `msg` + ')\n')
143 # Then make a backup of the original file as filename~
144 try:
145 posix.rename(filename, filename + '~')
146 except posix.error, msg:
147 err(filename + ': warning: backup failed (' + `msg` + ')\n')
148 # Now move the temp file to the original file
149 try:
150 posix.rename(tempname, filename)
151 except posix.error, msg:
152 err(filename + ': rename failed (' + `msg` + ')\n')
153 return 1
154 # Return succes
155 return 0
156
157main()