mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
Update difflib.py from 3.14.3
This commit is contained in:
1 parent
32b57785c3
commit
d54cf8f12e
3 files changed
+148
-93
No files matched your search
Vendored
+94
-86
@@ -78,8 +78,8 @@ class SequenceMatcher:
|
||||
sequences. As a rule of thumb, a .ratio() value over 0.6 means the
|
||||
sequences are close matches:
|
||||
|
||||
>>> print(round(s.ratio(), 3))
|
||||
0.866
|
||||
>>> print(round(s.ratio(), 2))
|
||||
0.87
|
||||
>>>
|
||||
|
||||
If you're only interested in where the sequences match,
|
||||
@@ -908,87 +908,85 @@ class Differ:
|
||||
+ 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
|
||||
# Don't synch up unless the lines have a similarity score above
|
||||
# cutoff. Previously only the smallest pair was handled here,
|
||||
# and if there are many pairs with the best ratio, recursion
|
||||
# could grow very deep, and runtime cubic. See:
|
||||
# https://github.com/python/cpython/issues/119105
|
||||
#
|
||||
# Later, more pathological cases prompted removing recursion
|
||||
# entirely.
|
||||
cutoff = 0.74999
|
||||
cruncher = SequenceMatcher(self.charjunk)
|
||||
eqi, eqj = None, None # 1st indices of equal lines (if any)
|
||||
crqr = cruncher.real_quick_ratio
|
||||
cqr = cruncher.quick_ratio
|
||||
cr = cruncher.ratio
|
||||
|
||||
# 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)
|
||||
WINDOW = 10
|
||||
best_i = best_j = None
|
||||
dump_i, dump_j = alo, blo # smallest indices not yet resolved
|
||||
for j in range(blo, bhi):
|
||||
bj = b[j]
|
||||
cruncher.set_seq2(bj)
|
||||
for i in range(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
|
||||
yield from self._plain_replace(a, alo, ahi, b, blo, bhi)
|
||||
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)
|
||||
eqi = None
|
||||
cruncher.set_seq2(b[j])
|
||||
# Search the corresponding i's within WINDOW for rhe highest
|
||||
# ratio greater than `cutoff`.
|
||||
aequiv = alo + (j - blo)
|
||||
arange = range(max(aequiv - WINDOW, dump_i),
|
||||
min(aequiv + WINDOW + 1, ahi))
|
||||
if not arange: # likely exit if `a` is shorter than `b`
|
||||
break
|
||||
best_ratio = cutoff
|
||||
for i in arange:
|
||||
cruncher.set_seq1(a[i])
|
||||
# Ordering by cheapest to most expensive ratio is very
|
||||
# valuable, most often getting out early.
|
||||
if (crqr() > best_ratio
|
||||
and cqr() > best_ratio
|
||||
and cr() > best_ratio):
|
||||
best_i, best_j, best_ratio = i, j, cr()
|
||||
|
||||
# a[best_i] very similar to b[best_j]; eqi is None iff they're not
|
||||
# identical
|
||||
if best_i is None:
|
||||
# found nothing to synch on yet - move to next j
|
||||
continue
|
||||
|
||||
# pump out diffs from before the synch point
|
||||
yield from self._fancy_helper(a, alo, best_i, b, blo, best_j)
|
||||
# pump out straight replace from before this synch pair
|
||||
yield from self._fancy_helper(a, dump_i, best_i,
|
||||
b, dump_j, best_j)
|
||||
# do intraline marking on the synch pair
|
||||
aelt, belt = a[best_i], b[best_j]
|
||||
if aelt != belt:
|
||||
# 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,))
|
||||
yield from self._qformat(aelt, belt, atags, btags)
|
||||
else:
|
||||
# the synch pair is identical
|
||||
yield ' ' + aelt
|
||||
dump_i, dump_j = best_i + 1, best_j + 1
|
||||
best_i = best_j = None
|
||||
|
||||
# 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,))
|
||||
yield from self._qformat(aelt, belt, atags, btags)
|
||||
else:
|
||||
# the synch pair is identical
|
||||
yield ' ' + aelt
|
||||
|
||||
# pump out diffs from after the synch point
|
||||
yield from self._fancy_helper(a, best_i+1, ahi, b, best_j+1, bhi)
|
||||
# pump out straight replace from after the last synch pair
|
||||
yield from self._fancy_helper(a, dump_i, ahi,
|
||||
b, dump_j, bhi)
|
||||
|
||||
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)
|
||||
g = self._plain_replace(a, alo, ahi, b, blo, bhi)
|
||||
else:
|
||||
g = self._dump('-', a, alo, ahi)
|
||||
elif blo < bhi:
|
||||
@@ -1040,11 +1038,9 @@ class Differ:
|
||||
# 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):
|
||||
def IS_LINE_JUNK(line, pat=None):
|
||||
r"""
|
||||
Return True for ignorable line: iff `line` is blank or contains a single '#'.
|
||||
Return True for ignorable line: if `line` is blank or contains a single '#'.
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -1056,6 +1052,11 @@ def IS_LINE_JUNK(line, pat=re.compile(r"\s*(?:#\s*)?$").match):
|
||||
False
|
||||
"""
|
||||
|
||||
if pat is None:
|
||||
# Default: match '#' or the empty string
|
||||
return line.strip() in '#'
|
||||
# Previous versions used the undocumented parameter 'pat' as a
|
||||
# match function. Retain this behaviour for compatibility.
|
||||
return pat(line) is not None
|
||||
|
||||
def IS_CHARACTER_JUNK(ch, ws=" \t"):
|
||||
@@ -1266,6 +1267,12 @@ def _check_types(a, b, *args):
|
||||
if b and not isinstance(b[0], str):
|
||||
raise TypeError('lines to compare must be str, not %s (%r)' %
|
||||
(type(b[0]).__name__, b[0]))
|
||||
if isinstance(a, str):
|
||||
raise TypeError('input must be a sequence of strings, not %s' %
|
||||
type(a).__name__)
|
||||
if isinstance(b, str):
|
||||
raise TypeError('input must be a sequence of strings, not %s' %
|
||||
type(b).__name__)
|
||||
for arg in args:
|
||||
if not isinstance(arg, str):
|
||||
raise TypeError('all arguments must be str, not: %r' % (arg,))
|
||||
@@ -1628,13 +1635,22 @@ _file_template = """
|
||||
</html>"""
|
||||
|
||||
_styles = """
|
||||
:root {color-scheme: light dark}
|
||||
table.diff {font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; border:medium}
|
||||
.diff_header {background-color:#e0e0e0}
|
||||
td.diff_header {text-align:right}
|
||||
.diff_next {background-color:#c0c0c0}
|
||||
.diff_add {background-color:#aaffaa}
|
||||
.diff_add {background-color:palegreen}
|
||||
.diff_chg {background-color:#ffff77}
|
||||
.diff_sub {background-color:#ffaaaa}"""
|
||||
.diff_sub {background-color:#ffaaaa}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.diff_header {background-color:#666}
|
||||
.diff_next {background-color:#393939}
|
||||
.diff_add {background-color:darkgreen}
|
||||
.diff_chg {background-color:#847415}
|
||||
.diff_sub {background-color:darkred}
|
||||
}"""
|
||||
|
||||
_table_template = """
|
||||
<table class="diff" id="difflib_chg_%(prefix)s_top"
|
||||
@@ -2014,7 +2030,6 @@ class HtmlDiff(object):
|
||||
replace('\1','</span>'). \
|
||||
replace('\t',' ')
|
||||
|
||||
del re
|
||||
|
||||
def restore(delta, which):
|
||||
r"""
|
||||
@@ -2047,10 +2062,3 @@ def restore(delta, which):
|
||||
for line in delta:
|
||||
if line[:2] in prefixes:
|
||||
yield line[2:]
|
||||
|
||||
def _test():
|
||||
import doctest, difflib
|
||||
return doctest.testmod(difflib)
|
||||
|
||||
if __name__ == "__main__":
|
||||
_test()
|
||||
Vendored
+44
-6
@@ -282,6 +282,26 @@ class TestSFpatches(unittest.TestCase):
|
||||
self.assertIn('content="text/html; charset=us-ascii"', output)
|
||||
self.assertIn('ımplıcıt', output)
|
||||
|
||||
class TestDiffer(unittest.TestCase):
|
||||
def test_close_matches_aligned(self):
|
||||
# Of the 4 closely matching pairs, we want 1 to match with 3,
|
||||
# and 2 with 4, to align with a "top to bottom" mental model.
|
||||
a = ["cat\n", "dog\n", "close match 1\n", "close match 2\n"]
|
||||
b = ["close match 3\n", "close match 4\n", "kitten\n", "puppy\n"]
|
||||
m = difflib.Differ().compare(a, b)
|
||||
self.assertEqual(list(m),
|
||||
['- cat\n',
|
||||
'- dog\n',
|
||||
'- close match 1\n',
|
||||
'? ^\n',
|
||||
'+ close match 3\n',
|
||||
'? ^\n',
|
||||
'- close match 2\n',
|
||||
'? ^\n',
|
||||
'+ close match 4\n',
|
||||
'? ^\n',
|
||||
'+ kitten\n',
|
||||
'+ puppy\n'])
|
||||
|
||||
def test_one_insert(self):
|
||||
m = difflib.Differ().compare('b' * 2, 'a' + 'b' * 2)
|
||||
@@ -294,7 +314,7 @@ class TestSFpatches(unittest.TestCase):
|
||||
|
||||
class TestOutputFormat(unittest.TestCase):
|
||||
def test_tab_delimiter(self):
|
||||
args = ['one', 'two', 'Original', 'Current',
|
||||
args = [['one'], ['two'], 'Original', 'Current',
|
||||
'2005-01-26 23:30:50', '2010-04-02 10:20:52']
|
||||
ud = difflib.unified_diff(*args, lineterm='')
|
||||
self.assertEqual(list(ud)[0:2], [
|
||||
@@ -306,7 +326,7 @@ class TestOutputFormat(unittest.TestCase):
|
||||
"--- Current\t2010-04-02 10:20:52"])
|
||||
|
||||
def test_no_trailing_tab_on_empty_filedate(self):
|
||||
args = ['one', 'two', 'Original', 'Current']
|
||||
args = [['one'], ['two'], 'Original', 'Current']
|
||||
ud = difflib.unified_diff(*args, lineterm='')
|
||||
self.assertEqual(list(ud)[0:2], ["--- Original", "+++ Current"])
|
||||
|
||||
@@ -446,6 +466,28 @@ class TestBytes(unittest.TestCase):
|
||||
lineterm=b'')
|
||||
assertDiff(expect, actual)
|
||||
|
||||
|
||||
class TestInputTypes(unittest.TestCase):
|
||||
def _assert_type_error(self, msg, generator, *args):
|
||||
with self.assertRaises(TypeError) as ctx:
|
||||
list(generator(*args))
|
||||
self.assertEqual(msg, str(ctx.exception))
|
||||
|
||||
def test_input_type_checks(self):
|
||||
unified = difflib.unified_diff
|
||||
context = difflib.context_diff
|
||||
|
||||
expect = "input must be a sequence of strings, not str"
|
||||
self._assert_type_error(expect, unified, 'a', ['b'])
|
||||
self._assert_type_error(expect, context, 'a', ['b'])
|
||||
|
||||
self._assert_type_error(expect, unified, ['a'], 'b')
|
||||
self._assert_type_error(expect, context, ['a'], 'b')
|
||||
|
||||
expect = "lines to compare must be str, not NoneType (None)"
|
||||
self._assert_type_error(expect, unified, ['a'], [None])
|
||||
self._assert_type_error(expect, context, ['a'], [None])
|
||||
|
||||
def test_mixed_types_content(self):
|
||||
# type of input content must be consistent: all str or all bytes
|
||||
a = [b'hello']
|
||||
@@ -494,10 +536,6 @@ class TestBytes(unittest.TestCase):
|
||||
b = ['bar\n']
|
||||
list(difflib.unified_diff(a, b, 'a', 'b', datea, dateb))
|
||||
|
||||
def _assert_type_error(self, msg, generator, *args):
|
||||
with self.assertRaises(TypeError) as ctx:
|
||||
list(generator(*args))
|
||||
self.assertEqual(msg, str(ctx.exception))
|
||||
|
||||
class TestJunkAPIs(unittest.TestCase):
|
||||
def test_is_line_junk_true(self):
|
||||
|
||||
Vendored
+10
-1
@@ -9,13 +9,22 @@
|
||||
content="text/html; charset=utf-8" />
|
||||
<title></title>
|
||||
<style type="text/css">
|
||||
:root {color-scheme: light dark}
|
||||
table.diff {font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; border:medium}
|
||||
.diff_header {background-color:#e0e0e0}
|
||||
td.diff_header {text-align:right}
|
||||
.diff_next {background-color:#c0c0c0}
|
||||
.diff_add {background-color:#aaffaa}
|
||||
.diff_add {background-color:palegreen}
|
||||
.diff_chg {background-color:#ffff77}
|
||||
.diff_sub {background-color:#ffaaaa}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.diff_header {background-color:#666}
|
||||
.diff_next {background-color:#393939}
|
||||
.diff_add {background-color:darkgreen}
|
||||
.diff_chg {background-color:#847415}
|
||||
.diff_sub {background-color:darkred}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
||||
Reference in new issue
Block a user