Skip to content

Commit 04eefad

Browse files
committed
Added obj diff code from unittest
...i.e. the code that's used to display the difference between the actual and expected values for most of the `assert[...]` methods of `TestCase` when the assertion fails. This will be used to better display the diffs on the admin history page in an upcoming commit - with some modifications. The code was copied from https://github.com/python/cpython/blob/v3.12.0/Lib/unittest/util.py#L8-L52, with the following changes: * Placed the code inside a class, to group the functions and their "setting" variables from other code - which also lets them easily be overridden by users * Removed the `_` prefix from the functions and variables * Added type hints * Formatted with Black Lastly, the code was copied instead of simply imported from `unittest`, because the functions are undocumented and underscore-prefixed, which means that they're prone to being changed (drastically) or even removed, and so I think maintaining it will be easier and more stable by copy-pasting it - which additionally facilitates modification.
1 parent b5e627c commit 04eefad

File tree

1 file changed

+86
-0
lines changed

1 file changed

+86
-0
lines changed

simple_history/template_utils.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from os.path import commonprefix
2+
from typing import Any, Tuple
3+
4+
5+
class ObjDiffDisplay:
6+
"""
7+
A class grouping functions and settings related to displaying the textual
8+
difference between two (or more) objects.
9+
``common_shorten_repr()`` is the main method for this.
10+
11+
The code is based on
12+
https://github.com/python/cpython/blob/v3.12.0/Lib/unittest/util.py#L8-L52.
13+
"""
14+
15+
def __init__(
16+
self,
17+
*,
18+
max_length=80,
19+
placeholder_len=12,
20+
min_begin_len=5,
21+
min_end_len=5,
22+
min_common_len=5,
23+
):
24+
self.max_length = max_length
25+
self.placeholder_len = placeholder_len
26+
self.min_begin_len = min_begin_len
27+
self.min_end_len = min_end_len
28+
self.min_common_len = min_common_len
29+
self.min_diff_len = max_length - (
30+
min_begin_len
31+
+ placeholder_len
32+
+ min_common_len
33+
+ placeholder_len
34+
+ min_end_len
35+
)
36+
assert self.min_diff_len >= 0 # nosec
37+
38+
def common_shorten_repr(self, *args: Any) -> Tuple[str, ...]:
39+
"""
40+
Returns ``args`` with each element converted into a string representation.
41+
If any of the strings are longer than ``self.max_length``, they're all shortened
42+
so that the first differences between the strings (after a potential common
43+
prefix in all of them) are lined up.
44+
"""
45+
args = tuple(map(self.safe_repr, args))
46+
maxlen = max(map(len, args))
47+
if maxlen <= self.max_length:
48+
return args
49+
50+
prefix = commonprefix(args)
51+
prefixlen = len(prefix)
52+
53+
common_len = self.max_length - (
54+
maxlen - prefixlen + self.min_begin_len + self.placeholder_len
55+
)
56+
if common_len > self.min_common_len:
57+
assert (
58+
self.min_begin_len
59+
+ self.placeholder_len
60+
+ self.min_common_len
61+
+ (maxlen - prefixlen)
62+
< self.max_length
63+
) # nosec
64+
prefix = self.shorten(prefix, self.min_begin_len, common_len)
65+
return tuple(prefix + s[prefixlen:] for s in args)
66+
67+
prefix = self.shorten(prefix, self.min_begin_len, self.min_common_len)
68+
return tuple(
69+
prefix + self.shorten(s[prefixlen:], self.min_diff_len, self.min_end_len)
70+
for s in args
71+
)
72+
73+
def safe_repr(self, obj: Any, short=False) -> str:
74+
try:
75+
result = repr(obj)
76+
except Exception:
77+
result = object.__repr__(obj)
78+
if not short or len(result) < self.max_length:
79+
return result
80+
return result[: self.max_length] + " [truncated]..."
81+
82+
def shorten(self, s: str, prefixlen: int, suffixlen: int) -> str:
83+
skip = len(s) - prefixlen - suffixlen
84+
if skip > self.placeholder_len:
85+
s = "%s[%d chars]%s" % (s[:prefixlen], skip, s[len(s) - suffixlen:])
86+
return s

0 commit comments

Comments
 (0)