|
"""Various utility functions.""" |
|
|
|
from collections import namedtuple, Counter |
|
from os.path import commonprefix |
|
|
|
__unittest = True |
|
|
|
_MAX_LENGTH = 80 |
|
_PLACEHOLDER_LEN = 12 |
|
_MIN_BEGIN_LEN = 5 |
|
_MIN_END_LEN = 5 |
|
_MIN_COMMON_LEN = 5 |
|
_MIN_DIFF_LEN = _MAX_LENGTH - \ |
|
(_MIN_BEGIN_LEN + _PLACEHOLDER_LEN + _MIN_COMMON_LEN + |
|
_PLACEHOLDER_LEN + _MIN_END_LEN) |
|
assert _MIN_DIFF_LEN >= 0 |
|
|
|
def _shorten(s, prefixlen, suffixlen): |
|
skip = len(s) - prefixlen - suffixlen |
|
if skip > _PLACEHOLDER_LEN: |
|
s = '%s[%d chars]%s' % (s[:prefixlen], skip, s[len(s) - suffixlen:]) |
|
return s |
|
|
|
def _common_shorten_repr(*args): |
|
args = tuple(map(safe_repr, args)) |
|
maxlen = max(map(len, args)) |
|
if maxlen <= _MAX_LENGTH: |
|
return args |
|
|
|
prefix = commonprefix(args) |
|
prefixlen = len(prefix) |
|
|
|
common_len = _MAX_LENGTH - \ |
|
(maxlen - prefixlen + _MIN_BEGIN_LEN + _PLACEHOLDER_LEN) |
|
if common_len > _MIN_COMMON_LEN: |
|
assert _MIN_BEGIN_LEN + _PLACEHOLDER_LEN + _MIN_COMMON_LEN + \ |
|
(maxlen - prefixlen) < _MAX_LENGTH |
|
prefix = _shorten(prefix, _MIN_BEGIN_LEN, common_len) |
|
return tuple(prefix + s[prefixlen:] for s in args) |
|
|
|
prefix = _shorten(prefix, _MIN_BEGIN_LEN, _MIN_COMMON_LEN) |
|
return tuple(prefix + _shorten(s[prefixlen:], _MIN_DIFF_LEN, _MIN_END_LEN) |
|
for s in args) |
|
|
|
def safe_repr(obj, short=False): |
|
try: |
|
result = repr(obj) |
|
except Exception: |
|
result = object.__repr__(obj) |
|
if not short or len(result) < _MAX_LENGTH: |
|
return result |
|
return result[:_MAX_LENGTH] + ' [truncated]...' |
|
|
|
def strclass(cls): |
|
return "%s.%s" % (cls.__module__, cls.__qualname__) |
|
|
|
def sorted_list_difference(expected, actual): |
|
"""Finds elements in only one or the other of two, sorted input lists. |
|
|
|
Returns a two-element tuple of lists. The first list contains those |
|
elements in the "expected" list but not in the "actual" list, and the |
|
second contains those elements in the "actual" list but not in the |
|
"expected" list. Duplicate elements in either input list are ignored. |
|
""" |
|
i = j = 0 |
|
missing = [] |
|
unexpected = [] |
|
while True: |
|
try: |
|
e = expected[i] |
|
a = actual[j] |
|
if e < a: |
|
missing.append(e) |
|
i += 1 |
|
while expected[i] == e: |
|
i += 1 |
|
elif e > a: |
|
unexpected.append(a) |
|
j += 1 |
|
while actual[j] == a: |
|
j += 1 |
|
else: |
|
i += 1 |
|
try: |
|
while expected[i] == e: |
|
i += 1 |
|
finally: |
|
j += 1 |
|
while actual[j] == a: |
|
j += 1 |
|
except IndexError: |
|
missing.extend(expected[i:]) |
|
unexpected.extend(actual[j:]) |
|
break |
|
return missing, unexpected |
|
|
|
|
|
def unorderable_list_difference(expected, actual): |
|
"""Same behavior as sorted_list_difference but |
|
for lists of unorderable items (like dicts). |
|
|
|
As it does a linear search per item (remove) it |
|
has O(n*n) performance.""" |
|
missing = [] |
|
while expected: |
|
item = expected.pop() |
|
try: |
|
actual.remove(item) |
|
except ValueError: |
|
missing.append(item) |
|
|
|
|
|
return missing, actual |
|
|
|
def three_way_cmp(x, y): |
|
"""Return -1 if x < y, 0 if x == y and 1 if x > y""" |
|
return (x > y) - (x < y) |
|
|
|
_Mismatch = namedtuple('Mismatch', 'actual expected value') |
|
|
|
def _count_diff_all_purpose(actual, expected): |
|
'Returns list of (cnt_act, cnt_exp, elem) triples where the counts differ' |
|
|
|
s, t = list(actual), list(expected) |
|
m, n = len(s), len(t) |
|
NULL = object() |
|
result = [] |
|
for i, elem in enumerate(s): |
|
if elem is NULL: |
|
continue |
|
cnt_s = cnt_t = 0 |
|
for j in range(i, m): |
|
if s[j] == elem: |
|
cnt_s += 1 |
|
s[j] = NULL |
|
for j, other_elem in enumerate(t): |
|
if other_elem == elem: |
|
cnt_t += 1 |
|
t[j] = NULL |
|
if cnt_s != cnt_t: |
|
diff = _Mismatch(cnt_s, cnt_t, elem) |
|
result.append(diff) |
|
|
|
for i, elem in enumerate(t): |
|
if elem is NULL: |
|
continue |
|
cnt_t = 0 |
|
for j in range(i, n): |
|
if t[j] == elem: |
|
cnt_t += 1 |
|
t[j] = NULL |
|
diff = _Mismatch(0, cnt_t, elem) |
|
result.append(diff) |
|
return result |
|
|
|
def _count_diff_hashable(actual, expected): |
|
'Returns list of (cnt_act, cnt_exp, elem) triples where the counts differ' |
|
|
|
s, t = Counter(actual), Counter(expected) |
|
result = [] |
|
for elem, cnt_s in s.items(): |
|
cnt_t = t.get(elem, 0) |
|
if cnt_s != cnt_t: |
|
diff = _Mismatch(cnt_s, cnt_t, elem) |
|
result.append(diff) |
|
for elem, cnt_t in t.items(): |
|
if elem not in s: |
|
diff = _Mismatch(0, cnt_t, elem) |
|
result.append(diff) |
|
return result |
|
|