cinap_lenrekadd hg and python4bb9e2215 years agoHistory
2019 lines · 78.9 KB · Python
RawDownload
         # It's possible that we have adjacent equal blocks in the        # matching_blocks list now.  Starting with 2.5, this code was added        # to collapse them.        i1 = j1 = k1 = 0        non_adjacent = []        for i2, j2, k2 in matching_blocks:            # Is this block adjacent to i1, j1, k1?            if i1 + k1 == i2 and j1 + k1 == j2:                # Yes, so collapse them -- this just increases the length of                # the first block by the length of the second, and the first                # block so lengthened remains the block to compare against.                k1 += k2            else:                # Not adjacent.  Remember the first block (k1==0 means it's                # the dummy we started with), and make the second block the                # new block to compare against.                if k1:                    non_adjacent.append((i1, j1, k1))                i1, j1, k1 = i2, j2, k2        if k1:            non_adjacent.append((i1, j1, k1))         non_adjacent.append( (la, lb, 0) )        self.matching_blocks = non_adjacent        return self.matching_blocks     def get_opcodes(self):        """Return list of 5-tuples describing how to turn a into b.         Each tuple is of the form (tag, i1, i2, j1, j2).  The first tuple        has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the        tuple preceding it, and likewise for j1 == the previous j2.         The tags are strings, with these meanings:         'replace':  a[i1:i2] should be replaced by b[j1:j2]        'delete':   a[i1:i2] should be deleted.                    Note that j1==j2 in this case.        'insert':   b[j1:j2] should be inserted at a[i1:i1].                    Note that i1==i2 in this case.        'equal':    a[i1:i2] == b[j1:j2]         >>> a = "qabxcd"        >>> b = "abycdf"        >>> s = SequenceMatcher(None, a, b)        >>> for tag, i1, i2, j1, j2 in s.get_opcodes():        ...    print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" %        ...           (tag, i1, i2, a[i1:i2], j1, j2, b[j1:j2]))         delete a[0:1] (q) b[0:0] ()          equal a[1:3] (ab) b[0:2] (ab)        replace a[3:4] (x) b[2:3] (y)          equal a[4:6] (cd) b[3:5] (cd)         insert a[6:6] () b[5:6] (f)        """         if self.opcodes is not None:            return self.opcodes        i = j = 0        self.opcodes = answer = []        for ai, bj, size in self.get_matching_blocks():            # invariant:  we've pumped out correct diffs to change            # a[:i] into b[:j], and the next matching block is            # a[ai:ai+size] == b[bj:bj+size].  So we need to pump            # out a diff to change a[i:ai] into b[j:bj], pump out            # the matching block, and move (i,j) beyond the match            tag = ''            if i < ai and j < bj:                tag = 'replace'            elif i < ai:                tag = 'delete'            elif j < bj:                tag = 'insert'            if tag:                answer.append( (tag, i, ai, j, bj) )            i, j = ai+size, bj+size            # the list of matching blocks is terminated by a            # sentinel with size 0            if size:                answer.append( ('equal', ai, i, bj, j) )        return answer     def get_grouped_opcodes(self, n=3):        """ Isolate change clusters by eliminating ranges with no changes.         Return a generator of groups with upto n lines of context.        Each group is in the same format as returned by get_opcodes().         >>> from pprint import pprint        >>> a = map(str, range(1,40))        >>> b = a[:]        >>> b[8:8] = ['i']     # Make an insertion        >>> b[20] += 'x'       # Make a replacement        >>> b[23:28] = []      # Make a deletion        >>> b[30] += 'y'       # Make another replacement        >>> pprint(list(SequenceMatcher(None,a,b).get_grouped_opcodes()))        [[('equal', 5, 8, 5, 8), ('insert', 8, 8, 8, 9), ('equal', 8, 11, 9, 12)],         [('equal', 16, 19, 17, 20),          ('replace', 19, 20, 20, 21),          ('equal', 20, 22, 21, 23),          ('delete', 22, 27, 23, 23),          ('equal', 27, 30, 23, 26)],         [('equal', 31, 34, 27, 30),          ('replace', 34, 35, 30, 31),          ('equal', 35, 38, 31, 34)]]        """         codes = self.get_opcodes()        if not codes:            codes = [("equal", 0, 1, 0, 1)]        # Fixup leading and trailing groups if they show no changes.        if codes[0][0] == 'equal':            tag, i1, i2, j1, j2 = codes[0]            codes[0] = tag, max(i1, i2-n), i2, max(j1, j2-n), j2        if codes[-1][0] == 'equal':            tag, i1, i2, j1, j2 = codes[-1]            codes[-1] = tag, i1, min(i2, i1+n), j1, min(j2, j1+n)         nn = n + n        group = []        for tag, i1, i2, j1, j2 in codes:            # End the current group and start a new one whenever            # there is a large range with no changes.            if tag == 'equal' and i2-i1 > nn:                group.append((tag, i1, min(i2, i1+n), j1, min(j2, j1+n)))                yield group                group = []                i1, j1 = max(i1, i2-n), max(j1, j2-n)            group.append((tag, i1, i2, j1 ,j2))        if group and not (len(group)==1 and group[0][0] == 'equal'):            yield group     def ratio(self):        """Return a measure of the sequences' similarity (float in [0,1]).         Where T is the total number of elements in both sequences, and        M is the number of matches, this is 2.0*M / T.        Note that this is 1 if the sequences are identical, and 0 if        they have nothing in common.         .ratio() is expensive to compute if you haven't already computed        .get_matching_blocks() or .get_opcodes(), in which case you may        want to try .quick_ratio() or .real_quick_ratio() first to get an        upper bound.         >>> s = SequenceMatcher(None, "abcd", "bcde")        >>> s.ratio()        0.75        >>> s.quick_ratio()        0.75        >>> s.real_quick_ratio()        1.0        """         matches = reduce(lambda sum, triple: sum + triple[-1],                         self.get_matching_blocks(), 0)        return _calculate_ratio(matches, len(self.a) + len(self.b))     def quick_ratio(self):        """Return an upper bound on ratio() relatively quickly.         This isn't defined beyond that it is an upper bound on .ratio(), and        is faster to compute.        """         # viewing a and b as multisets, set matches to the cardinality        # of their intersection; this counts the number of matches        # without regard to order, so is clearly an upper bound        if self.fullbcount is None:            self.fullbcount = fullbcount = {}            for elt in self.b:                fullbcount[elt] = fullbcount.get(elt, 0) + 1        fullbcount = self.fullbcount        # avail[x] is the number of times x appears in 'b' less the        # number of times we've seen it in 'a' so far ... kinda        avail = {}        availhas, matches = avail.has_key, 0        for elt in self.a:            if availhas(elt):                numb = avail[elt]            else:                numb = fullbcount.get(elt, 0)            avail[elt] = numb - 1            if numb > 0:                matches = matches + 1        return _calculate_ratio(matches, len(self.a) + len(self.b))     def real_quick_ratio(self):        """Return an upper bound on ratio() very quickly.         This isn't defined beyond that it is an upper bound on .ratio(), and        is faster to compute than either .ratio() or .quick_ratio().        """         la, lb = len(self.a), len(self.b)        # can't have more matches than the number of elements in the        # shorter sequence        return _calculate_ratio(min(la, lb), la + lb) def get_close_matches(word, possibilities, n=3, cutoff=0.6):    """Use SequenceMatcher to return list of the best "good enough" matches.     word is a sequence for which close matches are desired (typically a    string).     possibilities is a list of sequences against which to match word    (typically a list of strings).     Optional arg n (default 3) is the maximum number of close matches to    return.  n must be > 0.     Optional arg cutoff (default 0.6) is a float in [0, 1].  Possibilities    that don't score at least that similar to word are ignored.     The best (no more than n) matches among the possibilities are returned    in a list, sorted by similarity score, most similar first.     >>> get_close_matches("appel", ["ape", "apple", "peach", "puppy"])    ['apple', 'ape']    >>> import keyword as _keyword    >>> get_close_matches("wheel", _keyword.kwlist)    ['while']    >>> get_close_matches("apple", _keyword.kwlist)    []    >>> get_close_matches("accept", _keyword.kwlist)    ['except']    """     if not n >  0:        raise ValueError("n must be > 0: %r" % (n,))    if not 0.0 <= cutoff <= 1.0:        raise ValueError("cutoff must be in [0.0, 1.0]: %r" % (cutoff,))    result = []    s = SequenceMatcher()    s.set_seq2(word)    for x in possibilities:        s.set_seq1(x)        if s.real_quick_ratio() >= cutoff and \           s.quick_ratio() >= cutoff and \           s.ratio() >= cutoff:            result.append((s.ratio(), x))     # Move the best scorers to head of list    result = heapq.nlargest(n, result)    # Strip scores for the best n matches    return [x for score, x in result] def _count_leading(line, ch):    """    Return number of `ch` characters at the start of `line`.     Example:     >>> _count_leading('   abc', ' ')    3    """     i, n = 0, len(line)    while i < n and line[i] == ch:        i += 1    return i class Differ:    r"""    Differ is a class for comparing sequences of lines of text, and    producing human-readable differences or deltas.  Differ uses    SequenceMatcher both to compare sequences of lines, and to compare    sequences of characters within similar (near-matching) lines.     Each line of a Differ delta begins with a two-letter code:         '- '    line unique to sequence 1        '+ '    line unique to sequence 2        '  '    line common to both sequences        '? '    line not present in either input sequence     Lines beginning with '? ' attempt to guide the eye to intraline    differences, and were not present in either input sequence.  These lines    can be confusing if the sequences contain tab characters.     Note that Differ makes no claim to produce a *minimal* diff.  To the    contrary, minimal diffs are often counter-intuitive, because they synch    up anywhere possible, sometimes accidental matches 100 pages apart.    Restricting synch points to contiguous matches preserves some notion of    locality, at the occasional cost of producing a longer diff.     Example: Comparing two texts.     First we set up the texts, sequences of individual single-line strings    ending with newlines (such sequences can also be obtained from the    `readlines()` method of file-like objects):     >>> text1 = '''  1. Beautiful is better than ugly.    ...   2. Explicit is better than implicit.    ...   3. Simple is better than complex.    ...   4. Complex is better than complicated.    ... '''.splitlines(1)    >>> len(text1)    4    >>> text1[0][-1]    '\n'    >>> text2 = '''  1. Beautiful is better than ugly.    ...   3.   Simple is better than complex.    ...   4. Complicated is better than complex.    ...   5. Flat is better than nested.    ... '''.splitlines(1)     Next we instantiate a Differ object:     >>> d = Differ()     Note that when instantiating a Differ object we may pass functions to    filter out line and character 'junk'.  See Differ.__init__ for details.     Finally, we compare the two:     >>> result = list(d.compare(text1, text2))     'result' is a list of strings, so let's pretty-print it:     >>> from pprint import pprint as _pprint    >>> _pprint(result)    ['    1. Beautiful is better than ugly.\n',     '-   2. Explicit is better than implicit.\n',     '-   3. Simple is better than complex.\n',     '+   3.   Simple is better than complex.\n',     '?     ++\n',     '-   4. Complex is better than complicated.\n',     '?            ^                     ---- ^\n',     '+   4. Complicated is better than complex.\n',     '?           ++++ ^                      ^\n',     '+   5. Flat is better than nested.\n']     As a single multi-line string it looks like this:     >>> print ''.join(result),        1. Beautiful is better than ugly.    -   2. Explicit is better than implicit.    -   3. Simple is better than complex.    +   3.   Simple is better than complex.    ?     ++    -   4. Complex is better than complicated.    ?            ^                     ---- ^    +   4. Complicated is better than complex.    ?           ++++ ^                      ^    +   5. Flat is better than nested.     Methods:     __init__(linejunk=None, charjunk=None)        Construct a text differencer, with optional filters.     compare(a, b)        Compare two sequences of lines; generate the resulting delta.    """     def __init__(self, linejunk=None, charjunk=None):        """        Construct a text differencer, with optional filters.         The two optional keyword parameters are for filter functions:         - `linejunk`: A function that should accept a single string argument,          and return true iff the string is junk. The module-level function          `IS_LINE_JUNK` may be used to filter out lines without visible          characters, except for at most one splat ('#').  It is recommended          to leave linejunk None; as of Python 2.3, the underlying          SequenceMatcher class has grown an adaptive notion of "noise" lines          that's better than any static definition the author has ever been          able to craft.         - `charjunk`: A function that should accept a string of length 1. The          module-level function `IS_CHARACTER_JUNK` may be used to filter out          whitespace characters (a blank or tab; **note**: bad idea to include          newline in this!).  Use of IS_CHARACTER_JUNK is recommended.        """         self.linejunk = linejunk        self.charjunk = charjunk     def compare(self, a, b):        r"""        Compare two sequences of lines; generate the resulting delta.         Each sequence must contain individual single-line strings ending with        newlines. Such sequences can be obtained from the `readlines()` method        of file-like objects.  The delta generated also consists of newline-        terminated strings, ready to be printed as-is via the writeline()        method of a file-like object.         Example:         >>> print ''.join(Differ().compare('one\ntwo\nthree\n'.splitlines(1),        ...                                'ore\ntree\nemu\n'.splitlines(1))),        - one        ?  ^        + ore        ?  ^        - two        - three        ?  -        + tree        + emu        """         cruncher = SequenceMatcher(self.linejunk, a, b)        for tag, alo, ahi, blo, bhi in cruncher.get_opcodes():            if tag == 'replace':                g = self._fancy_replace(a, alo, ahi, b, blo, bhi)            elif tag == 'delete':                g = self._dump('-', a, alo, ahi)            elif tag == 'insert':                g = self._dump('+', b, blo, bhi)            elif tag == 'equal':                g = self._dump(' ', a, alo, ahi)            else:                raise ValueError, 'unknown tag %r' % (tag,)             for line in g:                yield line     def _dump(self, tag, x, lo, hi):        """Generate comparison results for a same-tagged range."""        for i in xrange(lo, hi):            yield '%s %s' % (tag, x[i])     def _plain_replace(self, a, alo, ahi, b, blo, bhi):        assert alo < ahi and blo < bhi        # dump the shorter block first -- reduces the burden on short-term        # memory if the blocks are of very different sizes        if bhi - blo < ahi - alo:            first  = self._dump('+', b, blo, bhi)            second = self._dump('-', a, alo, ahi)        else:            first  = self._dump('-', a, alo, ahi)            second = self._dump('+', b, blo, bhi)         for g in first, second:            for line in g:                yield line     def _fancy_replace(self, a, alo, ahi, b, blo, bhi):        r"""        When replacing one block of lines with another, search the blocks        for *similar* lines; the best-matching pair (if any) is used as a        synch point, and intraline difference marking is done on the        similar pair. Lots of work, but often worth it.         Example:         >>> d = Differ()        >>> results = d._fancy_replace(['abcDefghiJkl\n'], 0, 1,        ...                            ['abcdefGhijkl\n'], 0, 1)        >>> print ''.join(results),        - abcDefghiJkl        ?    ^  ^  ^        + abcdefGhijkl        ?    ^  ^  ^        """         # don't synch up unless the lines have a similarity score of at        # least cutoff; best_ratio tracks the best score seen so far        best_ratio, cutoff = 0.74, 0.75        cruncher = SequenceMatcher(self.charjunk)        eqi, eqj = None, None   # 1st indices of equal lines (if any)         # search for the pair that matches best without being identical        # (identical lines must be junk lines, & we don't want to synch up        # on junk -- unless we have to)        for j in xrange(blo, bhi):            bj = b[j]            cruncher.set_seq2(bj)            for i in xrange(alo, ahi):                ai = a[i]                if ai == bj:                    if eqi is None:                        eqi, eqj = i, j                    continue                cruncher.set_seq1(ai)                # computing similarity is expensive, so use the quick                # upper bounds first -- have seen this speed up messy                # compares by a factor of 3.                # note that ratio() is only expensive to compute the first                # time it's called on a sequence pair; the expensive part                # of the computation is cached by cruncher                if cruncher.real_quick_ratio() > best_ratio and \                      cruncher.quick_ratio() > best_ratio and \                      cruncher.ratio() > best_ratio:                    best_ratio, best_i, best_j = cruncher.ratio(), i, j        if best_ratio < cutoff:            # no non-identical "pretty close" pair            if eqi is None:                # no identical pair either -- treat it as a straight replace                for line in self._plain_replace(a, alo, ahi, b, blo, bhi):                    yield line                return            # no close pair, but an identical pair -- synch up on that            best_i, best_j, best_ratio = eqi, eqj, 1.0        else:            # there's a close pair, so forget the identical pair (if any)

Lines 501–1000 of 2019. Previous lines