eqi = None # a[best_i] very similar to b[best_j]; eqi is None iff they're not # identical # pump out diffs from before the synch point for line in self._fancy_helper(a, alo, best_i, b, blo, best_j): yield line # do intraline marking on the synch pair aelt, belt = a[best_i], b[best_j] if eqi is None: # pump out a '-', '?', '+', '?' quad for the synched lines atags = btags = "" cruncher.set_seqs(aelt, belt) for tag, ai1, ai2, bj1, bj2 in cruncher.get_opcodes(): la, lb = ai2 - ai1, bj2 - bj1 if tag == 'replace': atags += '^' * la btags += '^' * lb elif tag == 'delete': atags += '-' * la elif tag == 'insert': btags += '+' * lb elif tag == 'equal': atags += ' ' * la btags += ' ' * lb else: raise ValueError, 'unknown tag %r' % (tag,) for line in self._qformat(aelt, belt, atags, btags): yield line else: # the synch pair is identical yield ' ' + aelt # pump out diffs from after the synch point for line in self._fancy_helper(a, best_i+1, ahi, b, best_j+1, bhi): yield line def _fancy_helper(self, a, alo, ahi, b, blo, bhi): g = [] if alo < ahi: if blo < bhi: g = self._fancy_replace(a, alo, ahi, b, blo, bhi) else: g = self._dump('-', a, alo, ahi) elif blo < bhi: g = self._dump('+', b, blo, bhi) for line in g: yield line def _qformat(self, aline, bline, atags, btags): r""" Format "?" output and deal with leading tabs. Example: >>> d = Differ() >>> results = d._qformat('\tabcDefghiJkl\n', '\t\tabcdefGhijkl\n', ... ' ^ ^ ^ ', '+ ^ ^ ^ ') >>> for line in results: print repr(line) ... '- \tabcDefghiJkl\n' '? \t ^ ^ ^\n' '+ \t\tabcdefGhijkl\n' '? \t ^ ^ ^\n' """ # Can hurt, but will probably help most of the time. common = min(_count_leading(aline, "\t"), _count_leading(bline, "\t")) common = min(common, _count_leading(atags[:common], " ")) atags = atags[common:].rstrip() btags = btags[common:].rstrip() yield "- " + aline if atags: yield "? %s%s\n" % ("\t" * common, atags) yield "+ " + bline if btags: yield "? %s%s\n" % ("\t" * common, btags) # With respect to junk, an earlier version of ndiff simply refused to# *start* a match with a junk element. The result was cases like this:# before: private Thread currentThread;# after: private volatile Thread currentThread;# If you consider whitespace to be junk, the longest contiguous match# not starting with junk is "e Thread currentThread". So ndiff reported# that "e volatil" was inserted between the 't' and the 'e' in "private".# While an accurate view, to people that's absurd. The current version# looks for matching blocks that are entirely junk-free, then extends the# longest one of those as far as possible but only with matching junk.# So now "currentThread" is matched, then extended to suck up the# preceding blank; then "private" is matched, and extended to suck up the# following blank; then "Thread" is matched; and finally ndiff reports# that "volatile " was inserted before "Thread". The only quibble# remaining is that perhaps it was really the case that " volatile"# was inserted after "private". I can live with that <wink>. import re def IS_LINE_JUNK(line, pat=re.compile(r"\s*#?\s*$").match): r""" Return 1 for ignorable line: iff `line` is blank or contains a single '#'. Examples: >>> IS_LINE_JUNK('\n') True >>> IS_LINE_JUNK(' # \n') True >>> IS_LINE_JUNK('hello\n') False """ return pat(line) is not None def IS_CHARACTER_JUNK(ch, ws=" \t"): r""" Return 1 for ignorable character: iff `ch` is a space or tab. Examples: >>> IS_CHARACTER_JUNK(' ') True >>> IS_CHARACTER_JUNK('\t') True >>> IS_CHARACTER_JUNK('\n') False >>> IS_CHARACTER_JUNK('x') False """ return ch in ws def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n'): r""" Compare two sequences of lines; generate the delta as a unified diff. Unified diffs are a compact way of showing line changes and a few lines of context. The number of context lines is set by 'n' which defaults to three. By default, the diff control lines (those with ---, +++, or @@) are created with a trailing newline. This is helpful so that inputs created from file.readlines() result in diffs that are suitable for file.writelines() since both the inputs and outputs have trailing newlines. For inputs that do not have trailing newlines, set the lineterm argument to "" so that the output will be uniformly newline free. The unidiff format normally has a header for filenames and modification times. Any or all of these may be specified using strings for 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. The modification times are normally expressed in the format returned by time.ctime(). Example: >>> for line in unified_diff('one two three four'.split(), ... 'zero one tree four'.split(), 'Original', 'Current', ... 'Sat Jan 26 23:30:50 1991', 'Fri Jun 06 10:20:52 2003', ... lineterm=''): ... print line --- Original Sat Jan 26 23:30:50 1991 +++ Current Fri Jun 06 10:20:52 2003 @@ -1,4 +1,4 @@ +zero one -two -three +tree four """ started = False for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n): if not started: yield '--- %s %s%s' % (fromfile, fromfiledate, lineterm) yield '+++ %s %s%s' % (tofile, tofiledate, lineterm) started = True i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm) for tag, i1, i2, j1, j2 in group: if tag == 'equal': for line in a[i1:i2]: yield ' ' + line continue if tag == 'replace' or tag == 'delete': for line in a[i1:i2]: yield '-' + line if tag == 'replace' or tag == 'insert': for line in b[j1:j2]: yield '+' + line # See http://www.unix.org/single_unix_specification/def context_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n'): r""" Compare two sequences of lines; generate the delta as a context diff. Context diffs are a compact way of showing line changes and a few lines of context. The number of context lines is set by 'n' which defaults to three. By default, the diff control lines (those with *** or ---) are created with a trailing newline. This is helpful so that inputs created from file.readlines() result in diffs that are suitable for file.writelines() since both the inputs and outputs have trailing newlines. For inputs that do not have trailing newlines, set the lineterm argument to "" so that the output will be uniformly newline free. The context diff format normally has a header for filenames and modification times. Any or all of these may be specified using strings for 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. The modification times are normally expressed in the format returned by time.ctime(). If not specified, the strings default to blanks. Example: >>> print ''.join(context_diff('one\ntwo\nthree\nfour\n'.splitlines(1), ... 'zero\none\ntree\nfour\n'.splitlines(1), 'Original', 'Current', ... 'Sat Jan 26 23:30:50 1991', 'Fri Jun 06 10:22:46 2003')), *** Original Sat Jan 26 23:30:50 1991 --- Current Fri Jun 06 10:22:46 2003 *************** *** 1,4 **** one ! two ! three four --- 1,4 ---- + zero one ! tree four """ started = False prefixmap = {'insert':'+ ', 'delete':'- ', 'replace':'! ', 'equal':' '} for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n): if not started: yield '*** %s %s%s' % (fromfile, fromfiledate, lineterm) yield '--- %s %s%s' % (tofile, tofiledate, lineterm) started = True yield '***************%s' % (lineterm,) if group[-1][2] - group[0][1] >= 2: yield '*** %d,%d ****%s' % (group[0][1]+1, group[-1][2], lineterm) else: yield '*** %d ****%s' % (group[-1][2], lineterm) visiblechanges = [e for e in group if e[0] in ('replace', 'delete')] if visiblechanges: for tag, i1, i2, _, _ in group: if tag != 'insert': for line in a[i1:i2]: yield prefixmap[tag] + line if group[-1][4] - group[0][3] >= 2: yield '--- %d,%d ----%s' % (group[0][3]+1, group[-1][4], lineterm) else: yield '--- %d ----%s' % (group[-1][4], lineterm) visiblechanges = [e for e in group if e[0] in ('replace', 'insert')] if visiblechanges: for tag, _, _, j1, j2 in group: if tag != 'delete': for line in b[j1:j2]: yield prefixmap[tag] + line def ndiff(a, b, linejunk=None, charjunk=IS_CHARACTER_JUNK): r""" Compare `a` and `b` (lists of strings); return a `Differ`-style delta. Optional keyword parameters `linejunk` and `charjunk` are for filter functions (or None): - linejunk: A function that should accept a single string argument, and return true iff the string is junk. The default is None, and is recommended; as of Python 2.3, an adaptive notion of "noise" lines is used that does a good job on its own. - charjunk: A function that should accept a string of length 1. The default is module-level function IS_CHARACTER_JUNK, which filters out whitespace characters (a blank or tab; note: bad idea to include newline in this!). Tools/scripts/ndiff.py is a command-line front-end to this function. Example: >>> diff = ndiff('one\ntwo\nthree\n'.splitlines(1), ... 'ore\ntree\nemu\n'.splitlines(1)) >>> print ''.join(diff), - one ? ^ + ore ? ^ - two - three ? - + tree + emu """ return Differ(linejunk, charjunk).compare(a, b) def _mdiff(fromlines, tolines, context=None, linejunk=None, charjunk=IS_CHARACTER_JUNK): r"""Returns generator yielding marked up from/to side by side differences. Arguments: fromlines -- list of text lines to compared to tolines tolines -- list of text lines to be compared to fromlines context -- number of context lines to display on each side of difference, if None, all from/to text lines will be generated. linejunk -- passed on to ndiff (see ndiff documentation) charjunk -- passed on to ndiff (see ndiff documentation) This function returns an interator which returns a tuple: (from line tuple, to line tuple, boolean flag) from/to line tuple -- (line num, line text) line num -- integer or None (to indicate a context seperation) line text -- original line text with following markers inserted: '\0+' -- marks start of added text '\0-' -- marks start of deleted text '\0^' -- marks start of changed text '\1' -- marks end of added/deleted/changed text boolean flag -- None indicates context separation, True indicates either "from" or "to" line contains a change, otherwise False. This function/iterator was originally developed to generate side by side file difference for making HTML pages (see HtmlDiff class for example usage). Note, this function utilizes the ndiff function to generate the side by side difference markup. Optional ndiff arguments may be passed to this function and they in turn will be passed to ndiff. """ import re # regular expression for finding intraline change indices change_re = re.compile('(\++|\-+|\^+)') # create the difference iterator to generate the differences diff_lines_iterator = ndiff(fromlines,tolines,linejunk,charjunk) def _make_line(lines, format_key, side, num_lines=[0,0]): """Returns line of text with user's change markup and line formatting. lines -- list of lines from the ndiff generator to produce a line of text from. When producing the line of text to return, the lines used are removed from this list. format_key -- '+' return first line in list with "add" markup around the entire line. '-' return first line in list with "delete" markup around the entire line. '?' return first line in list with add/delete/change intraline markup (indices obtained from second line) None return first line in list with no markup side -- indice into the num_lines list (0=from,1=to) num_lines -- from/to current line number. This is NOT intended to be a passed parameter. It is present as a keyword argument to maintain memory of the current line numbers between calls of this function. Note, this function is purposefully not defined at the module scope so that data it needs from its parent function (within whose context it is defined) does not need to be of module scope. """ num_lines[side] += 1 # Handle case where no user markup is to be added, just return line of # text with user's line format to allow for usage of the line number. if format_key is None: return (num_lines[side],lines.pop(0)[2:]) # Handle case of intraline changes if format_key == '?': text, markers = lines.pop(0), lines.pop(0) # find intraline changes (store change type and indices in tuples) sub_info = [] def record_sub_info(match_object,sub_info=sub_info): sub_info.append([match_object.group(1)[0],match_object.span()]) return match_object.group(1) change_re.sub(record_sub_info,markers) # process each tuple inserting our special marks that won't be # noticed by an xml/html escaper. for key,(begin,end) in sub_info[::-1]: text = text[0:begin]+'\0'+key+text[begin:end]+'\1'+text[end:] text = text[2:] # Handle case of add/delete entire line else: text = lines.pop(0)[2:] # if line of text is just a newline, insert a space so there is # something for the user to highlight and see. if not text: text = ' ' # insert marks that won't be noticed by an xml/html escaper. text = '\0' + format_key + text + '\1' # Return line of text, first allow user's line formatter to do its # thing (such as adding the line number) then replace the special # marks with what the user's change markup. return (num_lines[side],text) def _line_iterator(): """Yields from/to lines of text with a change indication. This function is an iterator. It itself pulls lines from a differencing iterator, processes them and yields them. When it can it yields both a "from" and a "to" line, otherwise it will yield one or the other. In addition to yielding the lines of from/to text, a boolean flag is yielded to indicate if the text line(s) have differences in them. Note, this function is purposefully not defined at the module scope so that data it needs from its parent function (within whose context it is defined) does not need to be of module scope. """ lines = [] num_blanks_pending, num_blanks_to_yield = 0, 0 while True: # Load up next 4 lines so we can look ahead, create strings which # are a concatenation of the first character of each of the 4 lines # so we can do some very readable comparisons. while len(lines) < 4: try: lines.append(diff_lines_iterator.next()) except StopIteration: lines.append('X') s = ''.join([line[0] for line in lines]) if s.startswith('X'): # When no more lines, pump out any remaining blank lines so the # corresponding add/delete lines get a matching blank line so # all line pairs get yielded at the next level. num_blanks_to_yield = num_blanks_pending elif s.startswith('-?+?'): # simple intraline change yield _make_line(lines,'?',0), _make_line(lines,'?',1), True continue elif s.startswith('--++'): # in delete block, add block coming: we do NOT want to get # caught up on blank lines yet, just process the delete line num_blanks_pending -= 1 yield _make_line(lines,'-',0), None, True continue elif s.startswith(('--?+', '--+', '- ')): # in delete block and see a intraline change or unchanged line # coming: yield the delete line and then blanks from_line,to_line = _make_line(lines,'-',0), None num_blanks_to_yield,num_blanks_pending = num_blanks_pending-1,0 elif s.startswith('-+?'): # intraline change yield _make_line(lines,None,0), _make_line(lines,'?',1), True continue elif s.startswith('-?+'): # intraline change yield _make_line(lines,'?',0), _make_line(lines,None,1), True continue elif s.startswith('-'): # delete FROM line num_blanks_pending -= 1 yield _make_line(lines,'-',0), None, True continue elif s.startswith('+--'): # in add block, delete block coming: we do NOT want to get # caught up on blank lines yet, just process the add line num_blanks_pending += 1 yield None, _make_line(lines,'+',1), True continue elif s.startswith(('+ ', '+-')): # will be leaving an add block: yield blanks then add line from_line, to_line = None, _make_line(lines,'+',1) num_blanks_to_yield,num_blanks_pending = num_blanks_pending+1,0 elif s.startswith('+'): # inside an add block, yield the add line num_blanks_pending += 1 yield None, _make_line(lines,'+',1), True continue elif s.startswith(' '): # unchanged text, yield it to both sides yield _make_line(lines[:],None,0),_make_line(lines,None,1),False continue # Catch up on the blank lines so when we yield the next from/to # pair, they are lined up. while(num_blanks_to_yield < 0): num_blanks_to_yield += 1 yield None,('','\n'),True while(num_blanks_to_yield > 0): num_blanks_to_yield -= 1 yield ('','\n'),None,True if s.startswith('X'): raise StopIteration else: yield from_line,to_line,True Lines 1001–1500 of 2019. Previous lines Load more
eqi = None # a[best_i] very similar to b[best_j]; eqi is None iff they're not # identical # pump out diffs from before the synch point for line in self._fancy_helper(a, alo, best_i, b, blo, best_j): yield line # do intraline marking on the synch pair aelt, belt = a[best_i], b[best_j] if eqi is None: # pump out a '-', '?', '+', '?' quad for the synched lines atags = btags = "" cruncher.set_seqs(aelt, belt) for tag, ai1, ai2, bj1, bj2 in cruncher.get_opcodes(): la, lb = ai2 - ai1, bj2 - bj1 if tag == 'replace': atags += '^' * la btags += '^' * lb elif tag == 'delete': atags += '-' * la elif tag == 'insert': btags += '+' * lb elif tag == 'equal': atags += ' ' * la btags += ' ' * lb else: raise ValueError, 'unknown tag %r' % (tag,) for line in self._qformat(aelt, belt, atags, btags): yield line else: # the synch pair is identical yield ' ' + aelt # pump out diffs from after the synch point for line in self._fancy_helper(a, best_i+1, ahi, b, best_j+1, bhi): yield line def _fancy_helper(self, a, alo, ahi, b, blo, bhi): g = [] if alo < ahi: if blo < bhi: g = self._fancy_replace(a, alo, ahi, b, blo, bhi) else: g = self._dump('-', a, alo, ahi) elif blo < bhi: g = self._dump('+', b, blo, bhi) for line in g: yield line def _qformat(self, aline, bline, atags, btags): r""" Format "?" output and deal with leading tabs. Example: >>> d = Differ() >>> results = d._qformat('\tabcDefghiJkl\n', '\t\tabcdefGhijkl\n', ... ' ^ ^ ^ ', '+ ^ ^ ^ ') >>> for line in results: print repr(line) ... '- \tabcDefghiJkl\n' '? \t ^ ^ ^\n' '+ \t\tabcdefGhijkl\n' '? \t ^ ^ ^\n' """ # Can hurt, but will probably help most of the time. common = min(_count_leading(aline, "\t"), _count_leading(bline, "\t")) common = min(common, _count_leading(atags[:common], " ")) atags = atags[common:].rstrip() btags = btags[common:].rstrip() yield "- " + aline if atags: yield "? %s%s\n" % ("\t" * common, atags) yield "+ " + bline if btags: yield "? %s%s\n" % ("\t" * common, btags) # With respect to junk, an earlier version of ndiff simply refused to # *start* a match with a junk element. The result was cases like this: # before: private Thread currentThread; # after: private volatile Thread currentThread; # If you consider whitespace to be junk, the longest contiguous match # not starting with junk is "e Thread currentThread". So ndiff reported # that "e volatil" was inserted between the 't' and the 'e' in "private". # While an accurate view, to people that's absurd. The current version # looks for matching blocks that are entirely junk-free, then extends the # longest one of those as far as possible but only with matching junk. # So now "currentThread" is matched, then extended to suck up the # preceding blank; then "private" is matched, and extended to suck up the # following blank; then "Thread" is matched; and finally ndiff reports # that "volatile " was inserted before "Thread". The only quibble # remaining is that perhaps it was really the case that " volatile" # was inserted after "private". I can live with that <wink>. import re def IS_LINE_JUNK(line, pat=re.compile(r"\s*#?\s*$").match): r""" Return 1 for ignorable line: iff `line` is blank or contains a single '#'. Examples: >>> IS_LINE_JUNK('\n') True >>> IS_LINE_JUNK(' # \n') True >>> IS_LINE_JUNK('hello\n') False """ return pat(line) is not None def IS_CHARACTER_JUNK(ch, ws=" \t"): r""" Return 1 for ignorable character: iff `ch` is a space or tab. Examples: >>> IS_CHARACTER_JUNK(' ') True >>> IS_CHARACTER_JUNK('\t') True >>> IS_CHARACTER_JUNK('\n') False >>> IS_CHARACTER_JUNK('x') False """ return ch in ws def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n'): r""" Compare two sequences of lines; generate the delta as a unified diff. Unified diffs are a compact way of showing line changes and a few lines of context. The number of context lines is set by 'n' which defaults to three. By default, the diff control lines (those with ---, +++, or @@) are created with a trailing newline. This is helpful so that inputs created from file.readlines() result in diffs that are suitable for file.writelines() since both the inputs and outputs have trailing newlines. For inputs that do not have trailing newlines, set the lineterm argument to "" so that the output will be uniformly newline free. The unidiff format normally has a header for filenames and modification times. Any or all of these may be specified using strings for 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. The modification times are normally expressed in the format returned by time.ctime(). Example: >>> for line in unified_diff('one two three four'.split(), ... 'zero one tree four'.split(), 'Original', 'Current', ... 'Sat Jan 26 23:30:50 1991', 'Fri Jun 06 10:20:52 2003', ... lineterm=''): ... print line --- Original Sat Jan 26 23:30:50 1991 +++ Current Fri Jun 06 10:20:52 2003 @@ -1,4 +1,4 @@ +zero one -two -three +tree four """ started = False for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n): if not started: yield '--- %s %s%s' % (fromfile, fromfiledate, lineterm) yield '+++ %s %s%s' % (tofile, tofiledate, lineterm) started = True i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm) for tag, i1, i2, j1, j2 in group: if tag == 'equal': for line in a[i1:i2]: yield ' ' + line continue if tag == 'replace' or tag == 'delete': for line in a[i1:i2]: yield '-' + line if tag == 'replace' or tag == 'insert': for line in b[j1:j2]: yield '+' + line # See http://www.unix.org/single_unix_specification/ def context_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n'): r""" Compare two sequences of lines; generate the delta as a context diff. Context diffs are a compact way of showing line changes and a few lines of context. The number of context lines is set by 'n' which defaults to three. By default, the diff control lines (those with *** or ---) are created with a trailing newline. This is helpful so that inputs created from file.readlines() result in diffs that are suitable for file.writelines() since both the inputs and outputs have trailing newlines. For inputs that do not have trailing newlines, set the lineterm argument to "" so that the output will be uniformly newline free. The context diff format normally has a header for filenames and modification times. Any or all of these may be specified using strings for 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. The modification times are normally expressed in the format returned by time.ctime(). If not specified, the strings default to blanks. Example: >>> print ''.join(context_diff('one\ntwo\nthree\nfour\n'.splitlines(1), ... 'zero\none\ntree\nfour\n'.splitlines(1), 'Original', 'Current', ... 'Sat Jan 26 23:30:50 1991', 'Fri Jun 06 10:22:46 2003')), *** Original Sat Jan 26 23:30:50 1991 --- Current Fri Jun 06 10:22:46 2003 *************** *** 1,4 **** one ! two ! three four --- 1,4 ---- + zero one ! tree four """ started = False prefixmap = {'insert':'+ ', 'delete':'- ', 'replace':'! ', 'equal':' '} for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n): if not started: yield '*** %s %s%s' % (fromfile, fromfiledate, lineterm) yield '--- %s %s%s' % (tofile, tofiledate, lineterm) started = True yield '***************%s' % (lineterm,) if group[-1][2] - group[0][1] >= 2: yield '*** %d,%d ****%s' % (group[0][1]+1, group[-1][2], lineterm) else: yield '*** %d ****%s' % (group[-1][2], lineterm) visiblechanges = [e for e in group if e[0] in ('replace', 'delete')] if visiblechanges: for tag, i1, i2, _, _ in group: if tag != 'insert': for line in a[i1:i2]: yield prefixmap[tag] + line if group[-1][4] - group[0][3] >= 2: yield '--- %d,%d ----%s' % (group[0][3]+1, group[-1][4], lineterm) else: yield '--- %d ----%s' % (group[-1][4], lineterm) visiblechanges = [e for e in group if e[0] in ('replace', 'insert')] if visiblechanges: for tag, _, _, j1, j2 in group: if tag != 'delete': for line in b[j1:j2]: yield prefixmap[tag] + line def ndiff(a, b, linejunk=None, charjunk=IS_CHARACTER_JUNK): r""" Compare `a` and `b` (lists of strings); return a `Differ`-style delta. Optional keyword parameters `linejunk` and `charjunk` are for filter functions (or None): - linejunk: A function that should accept a single string argument, and return true iff the string is junk. The default is None, and is recommended; as of Python 2.3, an adaptive notion of "noise" lines is used that does a good job on its own. - charjunk: A function that should accept a string of length 1. The default is module-level function IS_CHARACTER_JUNK, which filters out whitespace characters (a blank or tab; note: bad idea to include newline in this!). Tools/scripts/ndiff.py is a command-line front-end to this function. Example: >>> diff = ndiff('one\ntwo\nthree\n'.splitlines(1), ... 'ore\ntree\nemu\n'.splitlines(1)) >>> print ''.join(diff), - one ? ^ + ore ? ^ - two - three ? - + tree + emu """ return Differ(linejunk, charjunk).compare(a, b) def _mdiff(fromlines, tolines, context=None, linejunk=None, charjunk=IS_CHARACTER_JUNK): r"""Returns generator yielding marked up from/to side by side differences. Arguments: fromlines -- list of text lines to compared to tolines tolines -- list of text lines to be compared to fromlines context -- number of context lines to display on each side of difference, if None, all from/to text lines will be generated. linejunk -- passed on to ndiff (see ndiff documentation) charjunk -- passed on to ndiff (see ndiff documentation) This function returns an interator which returns a tuple: (from line tuple, to line tuple, boolean flag) from/to line tuple -- (line num, line text) line num -- integer or None (to indicate a context seperation) line text -- original line text with following markers inserted: '\0+' -- marks start of added text '\0-' -- marks start of deleted text '\0^' -- marks start of changed text '\1' -- marks end of added/deleted/changed text boolean flag -- None indicates context separation, True indicates either "from" or "to" line contains a change, otherwise False. This function/iterator was originally developed to generate side by side file difference for making HTML pages (see HtmlDiff class for example usage). Note, this function utilizes the ndiff function to generate the side by side difference markup. Optional ndiff arguments may be passed to this function and they in turn will be passed to ndiff. """ import re # regular expression for finding intraline change indices change_re = re.compile('(\++|\-+|\^+)') # create the difference iterator to generate the differences diff_lines_iterator = ndiff(fromlines,tolines,linejunk,charjunk) def _make_line(lines, format_key, side, num_lines=[0,0]): """Returns line of text with user's change markup and line formatting. lines -- list of lines from the ndiff generator to produce a line of text from. When producing the line of text to return, the lines used are removed from this list. format_key -- '+' return first line in list with "add" markup around the entire line. '-' return first line in list with "delete" markup around the entire line. '?' return first line in list with add/delete/change intraline markup (indices obtained from second line) None return first line in list with no markup side -- indice into the num_lines list (0=from,1=to) num_lines -- from/to current line number. This is NOT intended to be a passed parameter. It is present as a keyword argument to maintain memory of the current line numbers between calls of this function. Note, this function is purposefully not defined at the module scope so that data it needs from its parent function (within whose context it is defined) does not need to be of module scope. """ num_lines[side] += 1 # Handle case where no user markup is to be added, just return line of # text with user's line format to allow for usage of the line number. if format_key is None: return (num_lines[side],lines.pop(0)[2:]) # Handle case of intraline changes if format_key == '?': text, markers = lines.pop(0), lines.pop(0) # find intraline changes (store change type and indices in tuples) sub_info = [] def record_sub_info(match_object,sub_info=sub_info): sub_info.append([match_object.group(1)[0],match_object.span()]) return match_object.group(1) change_re.sub(record_sub_info,markers) # process each tuple inserting our special marks that won't be # noticed by an xml/html escaper. for key,(begin,end) in sub_info[::-1]: text = text[0:begin]+'\0'+key+text[begin:end]+'\1'+text[end:] text = text[2:] # Handle case of add/delete entire line else: text = lines.pop(0)[2:] # if line of text is just a newline, insert a space so there is # something for the user to highlight and see. if not text: text = ' ' # insert marks that won't be noticed by an xml/html escaper. text = '\0' + format_key + text + '\1' # Return line of text, first allow user's line formatter to do its # thing (such as adding the line number) then replace the special # marks with what the user's change markup. return (num_lines[side],text) def _line_iterator(): """Yields from/to lines of text with a change indication. This function is an iterator. It itself pulls lines from a differencing iterator, processes them and yields them. When it can it yields both a "from" and a "to" line, otherwise it will yield one or the other. In addition to yielding the lines of from/to text, a boolean flag is yielded to indicate if the text line(s) have differences in them. Note, this function is purposefully not defined at the module scope so that data it needs from its parent function (within whose context it is defined) does not need to be of module scope. """ lines = [] num_blanks_pending, num_blanks_to_yield = 0, 0 while True: # Load up next 4 lines so we can look ahead, create strings which # are a concatenation of the first character of each of the 4 lines # so we can do some very readable comparisons. while len(lines) < 4: try: lines.append(diff_lines_iterator.next()) except StopIteration: lines.append('X') s = ''.join([line[0] for line in lines]) if s.startswith('X'): # When no more lines, pump out any remaining blank lines so the # corresponding add/delete lines get a matching blank line so # all line pairs get yielded at the next level. num_blanks_to_yield = num_blanks_pending elif s.startswith('-?+?'): # simple intraline change yield _make_line(lines,'?',0), _make_line(lines,'?',1), True continue elif s.startswith('--++'): # in delete block, add block coming: we do NOT want to get # caught up on blank lines yet, just process the delete line num_blanks_pending -= 1 yield _make_line(lines,'-',0), None, True continue elif s.startswith(('--?+', '--+', '- ')): # in delete block and see a intraline change or unchanged line # coming: yield the delete line and then blanks from_line,to_line = _make_line(lines,'-',0), None num_blanks_to_yield,num_blanks_pending = num_blanks_pending-1,0 elif s.startswith('-+?'): # intraline change yield _make_line(lines,None,0), _make_line(lines,'?',1), True continue elif s.startswith('-?+'): # intraline change yield _make_line(lines,'?',0), _make_line(lines,None,1), True continue elif s.startswith('-'): # delete FROM line num_blanks_pending -= 1 yield _make_line(lines,'-',0), None, True continue elif s.startswith('+--'): # in add block, delete block coming: we do NOT want to get # caught up on blank lines yet, just process the add line num_blanks_pending += 1 yield None, _make_line(lines,'+',1), True continue elif s.startswith(('+ ', '+-')): # will be leaving an add block: yield blanks then add line from_line, to_line = None, _make_line(lines,'+',1) num_blanks_to_yield,num_blanks_pending = num_blanks_pending+1,0 elif s.startswith('+'): # inside an add block, yield the add line num_blanks_pending += 1 yield None, _make_line(lines,'+',1), True continue elif s.startswith(' '): # unchanged text, yield it to both sides yield _make_line(lines[:],None,0),_make_line(lines,None,1),False continue # Catch up on the blank lines so when we yield the next from/to # pair, they are lined up. while(num_blanks_to_yield < 0): num_blanks_to_yield += 1 yield None,('','\n'),True while(num_blanks_to_yield > 0): num_blanks_to_yield -= 1 yield ('','\n'),None,True if s.startswith('X'): raise StopIteration else: yield from_line,to_line,True