Package translate :: Package tools :: Module pydiff
[hide private]
[frames] | no frames]

Source Code for Module translate.tools.pydiff

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3  # 
  4  # Copyright 2005, 2006 Zuza Software Foundation 
  5  # 
  6  # This file is part of translate. 
  7  # 
  8  # translate is free software; you can redistribute it and/or modify 
  9  # it under the terms of the GNU General Public License as published by 
 10  # the Free Software Foundation; either version 2 of the License, or 
 11  # (at your option) any later version. 
 12  # 
 13  # translate is distributed in the hope that it will be useful, 
 14  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 16  # GNU General Public License for more details. 
 17  # 
 18  # You should have received a copy of the GNU General Public License 
 19  # along with translate; if not, write to the Free Software 
 20  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 21   
 22  """diff tool like GNU diff, but lets you have special options that are useful in dealing with PO files""" 
 23   
 24  import difflib 
 25  import optparse 
 26  import time 
 27  import os 
 28  import sys 
 29  import fnmatch 
 30   
 31  lineterm = "\n" 
 32   
 33   
34 -def main():
35 """main program for pydiff""" 36 usage = "usage: %prog [options] fromfile tofile" 37 parser = optparse.OptionParser(usage) 38 # GNU diff like options 39 parser.add_option("-i", "--ignore-case", default=False, action="store_true", 40 help='Ignore case differences in file contents.') 41 parser.add_option("-U", "--unified", type="int", metavar="NUM", default=3, dest="unified_lines", 42 help='Output NUM (default 3) lines of unified context') 43 parser.add_option("-r", "--recursive", default=False, action="store_true", 44 help='Recursively compare any subdirectories found.') 45 parser.add_option("-N", "--new-file", default=False, action="store_true", 46 help='Treat absent files as empty.') 47 parser.add_option("", "--unidirectional-new-file", default=False, action="store_true", 48 help='Treat absent first files as empty.') 49 parser.add_option("-s", "--report-identical-files", default=False, action="store_true", 50 help='Report when two files are the same.') 51 parser.add_option("-x", "--exclude", default=["CVS", "*.po~"], action="append", metavar="PAT", 52 help='Exclude files that match PAT.') 53 # our own options 54 parser.add_option("", "--fromcontains", type="string", default=None, metavar="TEXT", 55 help='Only show changes where fromfile contains TEXT') 56 parser.add_option("", "--tocontains", type="string", default=None, metavar="TEXT", 57 help='Only show changes where tofile contains TEXT') 58 parser.add_option("", "--contains", type="string", default=None, metavar="TEXT", 59 help='Only show changes where fromfile or tofile contains TEXT') 60 parser.add_option("-I", "--ignore-case-contains", default=False, action="store_true", 61 help='Ignore case differences when matching any of the changes') 62 parser.add_option("", "--accelerator", dest="accelchars", default="", 63 metavar="ACCELERATORS", help="ignores the given accelerator characters when matching") 64 (options, args) = parser.parse_args() 65 66 if len(args) != 2: 67 parser.error("fromfile and tofile required") 68 fromfile, tofile = args 69 if fromfile == "-" and tofile == "-": 70 parser.error("Only one of fromfile and tofile can be read from stdin") 71 72 if os.path.isdir(fromfile): 73 if os.path.isdir(tofile): 74 differ = DirDiffer(fromfile, tofile, options) 75 else: 76 parser.error("File %s is a directory while file %s is a regular file" % (fromfile, tofile)) 77 else: 78 if os.path.isdir(tofile): 79 parser.error("File %s is a regular file while file %s is a directory" % (fromfile, tofile)) 80 else: 81 differ = FileDiffer(fromfile, tofile, options) 82 differ.writediff(sys.stdout)
83 84
85 -class DirDiffer:
86 """generates diffs between directories""" 87
88 - def __init__(self, fromdir, todir, options):
89 """constructs a comparison between the two dirs using the given options""" 90 self.fromdir = fromdir 91 self.todir = todir 92 self.options = options
93
94 - def isexcluded(self, difffile):
95 """checks if the given filename has been excluded from the diff""" 96 for exclude_pat in self.options.exclude: 97 if fnmatch.fnmatch(difffile, exclude_pat): 98 return True 99 return False
100
101 - def writediff(self, outfile):
102 """writes the actual diff to the given file""" 103 fromfiles = os.listdir(self.fromdir) 104 tofiles = os.listdir(self.todir) 105 difffiles = dict.fromkeys(fromfiles + tofiles).keys() 106 difffiles.sort() 107 for difffile in difffiles: 108 if self.isexcluded(difffile): 109 continue 110 from_ok = (difffile in fromfiles or self.options.new_file or self.options.unidirectional_new_file) 111 to_ok = (difffile in tofiles or self.options.new_file) 112 if from_ok and to_ok: 113 fromfile = os.path.join(self.fromdir, difffile) 114 tofile = os.path.join(self.todir, difffile) 115 if os.path.isdir(fromfile): 116 if os.path.isdir(tofile): 117 if self.options.recursive: 118 differ = DirDiffer(fromfile, tofile, self.options) 119 differ.writediff(outfile) 120 else: 121 outfile.write("Common subdirectories: %s and %s\n" % (fromfile, tofile)) 122 else: 123 outfile.write("File %s is a directory while file %s is a regular file\n" % (fromfile, tofile)) 124 else: 125 if os.path.isdir(tofile): 126 parser.error("File %s is a regular file while file %s is a directory\n" % (fromfile, tofile)) 127 else: 128 filediffer = FileDiffer(fromfile, tofile, self.options) 129 filediffer.writediff(outfile) 130 elif from_ok: 131 outfile.write("Only in %s: %s\n" % (self.fromdir, difffile)) 132 elif to_ok: 133 outfile.write("Only in %s: %s\n" % (self.todir, difffile))
134 135
136 -class FileDiffer:
137 """generates diffs between files""" 138
139 - def __init__(self, fromfile, tofile, options):
140 """constructs a comparison between the two files using the given options""" 141 self.fromfile = fromfile 142 self.tofile = tofile 143 self.options = options
144
145 - def writediff(self, outfile):
146 """writes the actual diff to the given file""" 147 validfiles = True 148 if os.path.exists(self.fromfile): 149 self.from_lines = open(self.fromfile, 'U').readlines() 150 fromfiledate = os.stat(self.fromfile).st_mtime 151 elif self.fromfile == "-": 152 self.from_lines = sys.stdin.readlines() 153 fromfiledate = time.time() 154 elif self.options.new_file or self.options.unidirectional_new_file: 155 self.from_lines = [] 156 fromfiledate = 0 157 else: 158 outfile.write("%s: No such file or directory\n" % self.fromfile) 159 validfiles = False 160 if os.path.exists(self.tofile): 161 self.to_lines = open(self.tofile, 'U').readlines() 162 tofiledate = os.stat(self.tofile).st_mtime 163 elif self.tofile == "-": 164 self.to_lines = sys.stdin.readlines() 165 tofiledate = time.time() 166 elif self.options.new_file: 167 self.to_lines = [] 168 tofiledate = 0 169 else: 170 outfile.write("%s: No such file or directory\n" % self.tofile) 171 validfiles = False 172 if not validfiles: 173 return 174 fromfiledate = time.ctime(fromfiledate) 175 tofiledate = time.ctime(tofiledate) 176 compare_from_lines = self.from_lines 177 compare_to_lines = self.to_lines 178 if self.options.ignore_case: 179 compare_from_lines = [line.lower() for line in compare_from_lines] 180 compare_to_lines = [line.lower() for line in compare_to_lines] 181 matcher = difflib.SequenceMatcher(None, compare_from_lines, compare_to_lines) 182 groups = matcher.get_grouped_opcodes(self.options.unified_lines) 183 started = False 184 fromstring = '--- %s\t%s%s' % (self.fromfile, fromfiledate, lineterm) 185 tostring = '+++ %s\t%s%s' % (self.tofile, tofiledate, lineterm) 186 187 for group in groups: 188 hunk = "".join([line for line in self.unified_diff(group)]) 189 if self.options.fromcontains: 190 if self.options.ignore_case_contains: 191 hunk_from_lines = "".join([line.lower() for line in self.get_from_lines(group)]) 192 else: 193 hunk_from_lines = "".join(self.get_from_lines(group)) 194 for accelerator in self.options.accelchars: 195 hunk_from_lines = hunk_from_lines.replace(accelerator, "") 196 if self.options.fromcontains not in hunk_from_lines: 197 continue 198 if self.options.tocontains: 199 if self.options.ignore_case_contains: 200 hunk_to_lines = "".join([line.lower() for line in self.get_to_lines(group)]) 201 else: 202 hunk_to_lines = "".join(self.get_to_lines(group)) 203 for accelerator in self.options.accelchars: 204 hunk_to_lines = hunk_to_lines.replace(accelerator, "") 205 if self.options.tocontains not in hunk_to_lines: 206 continue 207 if self.options.contains: 208 if self.options.ignore_case_contains: 209 hunk_lines = "".join([line.lower() for line in self.get_from_lines(group) + self.get_to_lines(group)]) 210 else: 211 hunk_lines = "".join(self.get_from_lines(group) + self.get_to_lines(group)) 212 for accelerator in self.options.accelchars: 213 hunk_lines = hunk_lines.replace(accelerator, "") 214 if self.options.contains not in hunk_lines: 215 continue 216 if not started: 217 outfile.write(fromstring) 218 outfile.write(tostring) 219 started = True 220 outfile.write(hunk) 221 if not started and self.options.report_identical_files: 222 outfile.write("Files %s and %s are identical\n" % (self.fromfile, self.tofile))
223
224 - def get_from_lines(self, group):
225 """returns the lines referred to by group, from the fromfile""" 226 from_lines = [] 227 for tag, i1, i2, j1, j2 in group: 228 from_lines.extend(self.from_lines[i1:i2]) 229 return from_lines
230
231 - def get_to_lines(self, group):
232 """returns the lines referred to by group, from the tofile""" 233 to_lines = [] 234 for tag, i1, i2, j1, j2 in group: 235 to_lines.extend(self.to_lines[j1:j2]) 236 return to_lines
237
238 - def unified_diff(self, group):
239 """takes the group of opcodes and generates a unified diff line by line""" 240 i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] 241 yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm) 242 for tag, i1, i2, j1, j2 in group: 243 if tag == 'equal': 244 for line in self.from_lines[i1:i2]: 245 yield ' ' + line 246 continue 247 if tag == 'replace' or tag == 'delete': 248 for line in self.from_lines[i1:i2]: 249 yield '-' + line 250 if tag == 'replace' or tag == 'insert': 251 for line in self.to_lines[j1:j2]: 252 yield '+' + line
253 254 255 if __name__ == "__main__": 256 main() 257