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 -def main():
34 """main program for pydiff""" 35 usage = "usage: %prog [options] fromfile tofile" 36 parser = optparse.OptionParser(usage) 37 # GNU diff like options 38 parser.add_option("-i", "--ignore-case", default=False, action="store_true", 39 help='Ignore case differences in file contents.') 40 parser.add_option("-U", "--unified", type="int", metavar="NUM", default=3, dest="unified_lines", 41 help='Output NUM (default 3) lines of unified context') 42 parser.add_option("-r", "--recursive", default=False, action="store_true", 43 help='Recursively compare any subdirectories found.') 44 parser.add_option("-N", "--new-file", default=False, action="store_true", 45 help='Treat absent files as empty.') 46 parser.add_option("", "--unidirectional-new-file", default=False, action="store_true", 47 help='Treat absent first files as empty.') 48 parser.add_option("-s", "--report-identical-files", default=False, action="store_true", 49 help='Report when two files are the same.') 50 parser.add_option("-x", "--exclude", default=["CVS", "*.po~"], action="append", metavar="PAT", 51 help='Exclude files that match PAT.') 52 # our own options 53 parser.add_option("", "--fromcontains", type="string", default=None, metavar="TEXT", 54 help='Only show changes where fromfile contains TEXT') 55 parser.add_option("", "--tocontains", type="string", default=None, metavar="TEXT", 56 help='Only show changes where tofile contains TEXT') 57 parser.add_option("", "--contains", type="string", default=None, metavar="TEXT", 58 help='Only show changes where fromfile or tofile contains TEXT') 59 parser.add_option("-I", "--ignore-case-contains", default=False, action="store_true", 60 help='Ignore case differences when matching any of the changes') 61 parser.add_option("", "--accelerator", dest="accelchars", default="", 62 metavar="ACCELERATORS", help="ignores the given accelerator characters when matching") 63 (options, args) = parser.parse_args() 64 65 if len(args) != 2: 66 parser.error("fromfile and tofile required") 67 fromfile, tofile = args 68 if fromfile == "-" and tofile == "-": 69 parser.error("Only one of fromfile and tofile can be read from stdin") 70 71 if os.path.isdir(fromfile): 72 if os.path.isdir(tofile): 73 differ = DirDiffer(fromfile, tofile, options) 74 else: 75 parser.error("File %s is a directory while file %s is a regular file" % (fromfile, tofile)) 76 else: 77 if os.path.isdir(tofile): 78 parser.error("File %s is a regular file while file %s is a directory" % (fromfile, tofile)) 79 else: 80 differ = FileDiffer(fromfile, tofile, options) 81 differ.writediff(sys.stdout)
82
83 -class DirDiffer:
84 """generates diffs between directories"""
85 - def __init__(self, fromdir, todir, options):
86 """constructs a comparison between the two dirs using the given options""" 87 self.fromdir = fromdir 88 self.todir = todir 89 self.options = options
90
91 - def isexcluded(self, difffile):
92 """checks if the given filename has been excluded from the diff""" 93 for exclude_pat in self.options.exclude: 94 if fnmatch.fnmatch(difffile, exclude_pat): 95 return True 96 return False
97
98 - def writediff(self, outfile):
99 """writes the actual diff to the given file""" 100 fromfiles = os.listdir(self.fromdir) 101 tofiles = os.listdir(self.todir) 102 difffiles = dict.fromkeys(fromfiles + tofiles).keys() 103 difffiles.sort() 104 for difffile in difffiles: 105 if self.isexcluded(difffile): 106 continue 107 from_ok = (difffile in fromfiles or self.options.new_file or self.options.unidirectional_new_file) 108 to_ok = (difffile in tofiles or self.options.new_file) 109 if from_ok and to_ok: 110 fromfile = os.path.join(self.fromdir, difffile) 111 tofile = os.path.join(self.todir, difffile) 112 if os.path.isdir(fromfile): 113 if os.path.isdir(tofile): 114 if self.options.recursive: 115 differ = DirDiffer(fromfile, tofile, self.options) 116 differ.writediff(outfile) 117 else: 118 outfile.write("Common subdirectories: %s and %s\n" % (fromfile, tofile)) 119 else: 120 outfile.write("File %s is a directory while file %s is a regular file\n" % (fromfile, tofile)) 121 else: 122 if os.path.isdir(tofile): 123 parser.error("File %s is a regular file while file %s is a directory\n" % (fromfile, tofile)) 124 else: 125 filediffer = FileDiffer(fromfile, tofile, self.options) 126 filediffer.writediff(outfile) 127 elif from_ok: 128 outfile.write("Only in %s: %s\n" % (self.fromdir, difffile)) 129 elif to_ok: 130 outfile.write("Only in %s: %s\n" % (self.todir, difffile))
131
132 -class FileDiffer:
133 """generates diffs between files"""
134 - def __init__(self, fromfile, tofile, options):
135 """constructs a comparison between the two files using the given options""" 136 self.fromfile = fromfile 137 self.tofile = tofile 138 self.options = options
139
140 - def writediff(self, outfile):
141 """writes the actual diff to the given file""" 142 validfiles = True 143 if os.path.exists(self.fromfile): 144 self.from_lines = open(self.fromfile, 'U').readlines() 145 fromfiledate = os.stat(self.fromfile).st_mtime 146 elif self.fromfile == "-": 147 self.from_lines = sys.stdin.readlines() 148 fromfiledate = time.time() 149 elif self.options.new_file or self.options.unidirectional_new_file: 150 self.from_lines = [] 151 fromfiledate = 0 152 else: 153 outfile.write("%s: No such file or directory\n" % self.fromfile) 154 validfiles = False 155 if os.path.exists(self.tofile): 156 self.to_lines = open(self.tofile, 'U').readlines() 157 tofiledate = os.stat(self.tofile).st_mtime 158 elif self.tofile == "-": 159 self.to_lines = sys.stdin.readlines() 160 tofiledate = time.time() 161 elif self.options.new_file: 162 self.to_lines = [] 163 tofiledate = 0 164 else: 165 outfile.write("%s: No such file or directory\n" % self.tofile) 166 validfiles = False 167 if not validfiles: 168 return 169 fromfiledate = time.ctime(fromfiledate) 170 tofiledate = time.ctime(tofiledate) 171 compare_from_lines = self.from_lines 172 compare_to_lines = self.to_lines 173 if self.options.ignore_case: 174 compare_from_lines = [line.lower() for line in compare_from_lines] 175 compare_to_lines = [line.lower() for line in compare_to_lines] 176 matcher = difflib.SequenceMatcher(None, compare_from_lines, compare_to_lines) 177 groups = matcher.get_grouped_opcodes(self.options.unified_lines) 178 started = False 179 fromstring = '--- %s\t%s%s' % (self.fromfile, fromfiledate, lineterm) 180 tostring = '+++ %s\t%s%s' % (self.tofile, tofiledate, lineterm) 181 182 for group in groups: 183 hunk = "".join([line for line in self.unified_diff(group)]) 184 if self.options.fromcontains: 185 if self.options.ignore_case_contains: 186 hunk_from_lines = "".join([line.lower() for line in self.get_from_lines(group)]) 187 else: 188 hunk_from_lines = "".join(self.get_from_lines(group)) 189 for accelerator in self.options.accelchars: 190 hunk_from_lines = hunk_from_lines.replace(accelerator, "") 191 if self.options.fromcontains not in hunk_from_lines: 192 continue 193 if self.options.tocontains: 194 if self.options.ignore_case_contains: 195 hunk_to_lines = "".join([line.lower() for line in self.get_to_lines(group)]) 196 else: 197 hunk_to_lines = "".join(self.get_to_lines(group)) 198 for accelerator in self.options.accelchars: 199 hunk_to_lines = hunk_to_lines.replace(accelerator, "") 200 if self.options.tocontains not in hunk_to_lines: 201 continue 202 if self.options.contains: 203 if self.options.ignore_case_contains: 204 hunk_lines = "".join([line.lower() for line in self.get_from_lines(group) + self.get_to_lines(group)]) 205 else: 206 hunk_lines = "".join(self.get_from_lines(group) + self.get_to_lines(group)) 207 for accelerator in self.options.accelchars: 208 hunk_lines = hunk_lines.replace(accelerator, "") 209 if self.options.contains not in hunk_lines: 210 continue 211 if not started: 212 outfile.write(fromstring) 213 outfile.write(tostring) 214 started = True 215 outfile.write(hunk) 216 if not started and self.options.report_identical_files: 217 outfile.write("Files %s and %s are identical\n" % (self.fromfile, self.tofile))
218
219 - def get_from_lines(self, group):
220 """returns the lines referred to by group, from the fromfile""" 221 from_lines = [] 222 for tag, i1, i2, j1, j2 in group: 223 from_lines.extend(self.from_lines[i1:i2]) 224 return from_lines
225
226 - def get_to_lines(self, group):
227 """returns the lines referred to by group, from the tofile""" 228 to_lines = [] 229 for tag, i1, i2, j1, j2 in group: 230 to_lines.extend(self.to_lines[j1:j2]) 231 return to_lines
232
233 - def unified_diff(self, group):
234 """takes the group of opcodes and generates a unified diff line by line""" 235 i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] 236 yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm) 237 for tag, i1, i2, j1, j2 in group: 238 if tag == 'equal': 239 for line in self.from_lines[i1:i2]: 240 yield ' ' + line 241 continue 242 if tag == 'replace' or tag == 'delete': 243 for line in self.from_lines[i1:i2]: 244 yield '-' + line 245 if tag == 'replace' or tag == 'insert': 246 for line in self.to_lines[j1:j2]: 247 yield '+' + line
248 249 if __name__ == "__main__": 250 main() 251