Skip to content

Commit fd72e7a

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 upcoming commit(s) - 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 bc880cf commit fd72e7a

File tree

1 file changed

+86
-1
lines changed

1 file changed

+86
-1
lines changed

simple_history/template_utils.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dataclasses
2-
from typing import Any, Dict, Final, List, Type, Union
2+
from os.path import commonprefix
3+
from typing import Any, Dict, Final, List, Tuple, Type, Union
34

45
from django.db.models import ManyToManyField, Model
56
from django.template.defaultfilters import truncatechars_html
@@ -115,3 +116,87 @@ def stringify_delta_change_value(self, change: ModelChange, value: Any) -> str:
115116
value = conditional_escape(value)
116117
value = truncatechars_html(value, self.max_displayed_delta_change_chars)
117118
return value
119+
120+
121+
class ObjDiffDisplay:
122+
"""
123+
A class grouping functions and settings related to displaying the textual
124+
difference between two (or more) objects.
125+
``common_shorten_repr()`` is the main method for this.
126+
127+
The code is based on
128+
https://github.com/python/cpython/blob/v3.12.0/Lib/unittest/util.py#L8-L52.
129+
"""
130+
131+
def __init__(
132+
self,
133+
*,
134+
max_length=80,
135+
placeholder_len=12,
136+
min_begin_len=5,
137+
min_end_len=5,
138+
min_common_len=5,
139+
):
140+
self.max_length = max_length
141+
self.placeholder_len = placeholder_len
142+
self.min_begin_len = min_begin_len
143+
self.min_end_len = min_end_len
144+
self.min_common_len = min_common_len
145+
self.min_diff_len = max_length - (
146+
min_begin_len
147+
+ placeholder_len
148+
+ min_common_len
149+
+ placeholder_len
150+
+ min_end_len
151+
)
152+
assert self.min_diff_len >= 0 # nosec
153+
154+
def common_shorten_repr(self, *args: Any) -> Tuple[str, ...]:
155+
"""
156+
Returns ``args`` with each element converted into a string representation.
157+
If any of the strings are longer than ``self.max_length``, they're all shortened
158+
so that the first differences between the strings (after a potential common
159+
prefix in all of them) are lined up.
160+
"""
161+
args = tuple(map(self.safe_repr, args))
162+
maxlen = max(map(len, args))
163+
if maxlen <= self.max_length:
164+
return args
165+
166+
prefix = commonprefix(args)
167+
prefixlen = len(prefix)
168+
169+
common_len = self.max_length - (
170+
maxlen - prefixlen + self.min_begin_len + self.placeholder_len
171+
)
172+
if common_len > self.min_common_len:
173+
assert (
174+
self.min_begin_len
175+
+ self.placeholder_len
176+
+ self.min_common_len
177+
+ (maxlen - prefixlen)
178+
< self.max_length
179+
) # nosec
180+
prefix = self.shorten(prefix, self.min_begin_len, common_len)
181+
return tuple(prefix + s[prefixlen:] for s in args)
182+
183+
prefix = self.shorten(prefix, self.min_begin_len, self.min_common_len)
184+
return tuple(
185+
prefix + self.shorten(s[prefixlen:], self.min_diff_len, self.min_end_len)
186+
for s in args
187+
)
188+
189+
def safe_repr(self, obj: Any, short=False) -> str:
190+
try:
191+
result = repr(obj)
192+
except Exception:
193+
result = object.__repr__(obj)
194+
if not short or len(result) < self.max_length:
195+
return result
196+
return result[: self.max_length] + " [truncated]..."
197+
198+
def shorten(self, s: str, prefixlen: int, suffixlen: int) -> str:
199+
skip = len(s) - prefixlen - suffixlen
200+
if skip > self.placeholder_len:
201+
s = "%s[%d chars]%s" % (s[:prefixlen], skip, s[len(s) - suffixlen :])
202+
return s

0 commit comments

Comments
 (0)