Skip to content

Commit

Permalink
Added obj diff code from unittest
Browse files Browse the repository at this point in the history
...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.
  • Loading branch information
ddabble committed May 2, 2024
1 parent bc880cf commit fd72e7a
Showing 1 changed file with 86 additions and 1 deletion.
87 changes: 86 additions & 1 deletion simple_history/template_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dataclasses
from typing import Any, Dict, Final, List, Type, Union
from os.path import commonprefix
from typing import Any, Dict, Final, List, Tuple, Type, Union

from django.db.models import ManyToManyField, Model
from django.template.defaultfilters import truncatechars_html
Expand Down Expand Up @@ -115,3 +116,87 @@ def stringify_delta_change_value(self, change: ModelChange, value: Any) -> str:
value = conditional_escape(value)
value = truncatechars_html(value, self.max_displayed_delta_change_chars)
return value


class ObjDiffDisplay:
"""
A class grouping functions and settings related to displaying the textual
difference between two (or more) objects.
``common_shorten_repr()`` is the main method for this.
The code is based on
https://github.com/python/cpython/blob/v3.12.0/Lib/unittest/util.py#L8-L52.
"""

def __init__(
self,
*,
max_length=80,
placeholder_len=12,
min_begin_len=5,
min_end_len=5,
min_common_len=5,
):
self.max_length = max_length
self.placeholder_len = placeholder_len
self.min_begin_len = min_begin_len
self.min_end_len = min_end_len
self.min_common_len = min_common_len
self.min_diff_len = max_length - (
min_begin_len
+ placeholder_len
+ min_common_len
+ placeholder_len
+ min_end_len
)
assert self.min_diff_len >= 0 # nosec

def common_shorten_repr(self, *args: Any) -> Tuple[str, ...]:
"""
Returns ``args`` with each element converted into a string representation.
If any of the strings are longer than ``self.max_length``, they're all shortened
so that the first differences between the strings (after a potential common
prefix in all of them) are lined up.
"""
args = tuple(map(self.safe_repr, args))
maxlen = max(map(len, args))
if maxlen <= self.max_length:
return args

prefix = commonprefix(args)
prefixlen = len(prefix)

common_len = self.max_length - (
maxlen - prefixlen + self.min_begin_len + self.placeholder_len
)
if common_len > self.min_common_len:
assert (
self.min_begin_len
+ self.placeholder_len
+ self.min_common_len
+ (maxlen - prefixlen)
< self.max_length
) # nosec
prefix = self.shorten(prefix, self.min_begin_len, common_len)
return tuple(prefix + s[prefixlen:] for s in args)

prefix = self.shorten(prefix, self.min_begin_len, self.min_common_len)
return tuple(
prefix + self.shorten(s[prefixlen:], self.min_diff_len, self.min_end_len)
for s in args
)

def safe_repr(self, obj: Any, short=False) -> str:
try:
result = repr(obj)
except Exception:
result = object.__repr__(obj)
if not short or len(result) < self.max_length:
return result
return result[: self.max_length] + " [truncated]..."

def shorten(self, s: str, prefixlen: int, suffixlen: int) -> str:
skip = len(s) - prefixlen - suffixlen
if skip > self.placeholder_len:
s = "%s[%d chars]%s" % (s[:prefixlen], skip, s[len(s) - suffixlen :])
return s

0 comments on commit fd72e7a

Please sign in to comment.