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