Skip to content

Instantly share code, notes, and snippets.

@jlumbroso
Created May 8, 2022 02:18
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jlumbroso/3ef433b4402b4f157728920a66cc15ed to your computer and use it in GitHub Desktop.
Save jlumbroso/3ef433b4402b4f157728920a66cc15ed to your computer and use it in GitHub Desktop.
Side-by-Side Diff Comparison in Python
# Code licensed LGPLv3 by Jérémie Lumbroso <lumbroso@cs.princeton.edu>
import difflib
import itertools
import textwrap
import typing
def side_by_side(
left: typing.List[str],
right: typing.List[str],
width: int = 78,
as_string: bool = False,
separator: typing.Optional[str] = " | ",
left_title: typing.Optional[str] = None,
right_title: typing.Optional[str] = None,
) -> typing.Union[str, typing.List[str]]:
"""Returns either the list of lines, or string of lines, that results from
merging the two lists side-by-side.
:param left: Lines of text to place on the left side
:type left: typing.List[str]
:param right: Lines of text to place on the right side
:type right: typing.List[str]
:param width: Character width of the overall output, defaults to 78
:type width: int, optional
:param as_string: Whether to return a string (as opposed to a list of strings), defaults to False
:type as_string: bool, optional
:param separator: String separating the left and right side, defaults to " | "
:type separator: typing.Optional[str], optional
:param left_title: Title to place on the left side, defaults to None
:type left_title: typing.Optional[str], optional
:param right_title: Title to place on the right side, defaults to None
:type right_title: typing.Optional[str], optional
:return: Lines or text of the merged side-by-side output.
:rtype: typing.Union[str, typing.List[str]]
"""
DEFAULT_SEPARATOR = " | "
separator = separator or DEFAULT_SEPARATOR
mid_width = (width - len(separator) - (1 - width % 2)) // 2
tw = textwrap.TextWrapper(
width=mid_width,
break_long_words=False,
replace_whitespace=False
)
def reflow(lines):
wrapped_lines = list(map(tw.wrap, lines))
wrapped_lines_with_linebreaks = [
[""] if len(wls) == 0 else wls
for wls in wrapped_lines
]
return list(itertools.chain.from_iterable(wrapped_lines_with_linebreaks))
left = reflow(left)
right = reflow(right)
zip_pairs = itertools.zip_longest(left, right)
if left_title is not None or right_title is not None:
left_title = left_title or ""
right_title = right_title or ""
zip_pairs = [
(left_title, right_title),
(mid_width * "-", mid_width * "-")
] + list(zip_pairs)
lines = []
for l, r in zip_pairs:
l = l or ""
r = r or ""
line = "{}{}{}{}".format(
l,
(" " * max(0, mid_width - len(l))),
separator,
r
)
lines.append(line)
if as_string:
return "\n".join(lines)
return lines
def better_diff(
left: typing.List[str],
right: typing.List[str],
width: int = 78,
as_string: bool = False,
separator: typing.Optional[str] = None,
left_title: typing.Optional[str] = None,
right_title: typing.Optional[str] = None,
) -> typing.Union[str, typing.List[str]]:
"""Returns a side-by-side comparison of the two provided inputs, showing
common lines between both inputs, and the lines that are unique to each.
:param left: Lines of text to place on the left side
:type left: typing.List[str]
:param right: Lines of text to place on the right side
:type right: typing.List[str]
:param width: Character width of the overall output, defaults to 78
:type width: int, optional
:param as_string: Whether to return a string (as opposed to a list of strings), defaults to False
:type as_string: bool, optional
:param separator: String separating the left and right side, defaults to " | "
:type separator: typing.Optional[str], optional
:param left_title: Title to place on the left side, defaults to None
:type left_title: typing.Optional[str], optional
:param right_title: Title to place on the right side, defaults to None
:type right_title: typing.Optional[str], optional
:return: Lines or text of the merged side-by-side diff comparison output.
:rtype: typing.Union[str, typing.List[str]]
"""
differ = difflib.Differ()
left_side = []
right_side = []
# adapted from
# LINK: https://stackoverflow.com/a/66091742/408734
difflines = list(differ.compare(left, right))
for line in difflines:
op = line[0]
tail = line[2:]
if op == " ":
# line is same in both
left_side.append(tail)
right_side.append(tail)
elif op == "-":
# line is only on the left
left_side.append(tail)
right_side.append("")
elif op == "+":
# line is only on the right
left_side.append("")
right_side.append(tail)
return side_by_side(
left=left_side,
right=right_side,
width=width,
as_string=as_string,
separator=separator,
left_title=left_title,
right_title=right_title,
)
@jlumbroso
Copy link
Author

Dear @haferburg !!

Thanks for taking the time to chime in about this — well observed, I completely agree. I am going to look into making this change tomorrow or Sunday, and I'll follow up when I am done. You rock!!

@jlumbroso giving a high five to @haferburg

@soxofaan
Copy link

soxofaan commented Apr 6, 2023

FYI:
I worked on an alternative implementation here: https://gist.github.com/soxofaan/e97112c4789ee74e1bf61532c998c0eb

The better_diff of this gist did not work well for me, e.g. using on the same data of https://gist.github.com/soxofaan/e97112c4789ee74e1bf61532c998c0eb?permalink_comment_id=4528416#gistcomment-4528416 gave me this:

{                                     | {
  "exp": 1680773344,                  |   "exp": 1680773344,
  "iat": 1680773044,                  |   "iat": 1680773044,
  "jti":                              | 
"40410c7c-8828-4755-a5b8-fa09b8736542", |   "jti":
                                      | "564abdb3-6e34-4a7c-b00e-77d1ee675847",
  "iss":                              |   "iss":
"http://localhost:8642/realms/i168realm", | "http://localhost:8642/realms/i168realm",
  "aud": "account",                   |   "aud": "account",
  "sub":                              | 
"7e7feab3-afd4-4186-a00c-c546ac51a749", |   "sub":
                                      | "d2aa0547-daaa-478b-9cb9-1ba2b1aaf47c",
  "typ": "Bearer",                    |   "typ": "Bearer",
                                      |   "azp": "service-client-L8FFS94R",
                                      |   "preferred_username": "service-
  "azp": "public-client-VQ5r6dli",    | account-service-client-l8ffs94r",
  "session_state":                    | 
"0c09bcad-45de-47ff-b645-e1212047d39a", | 
  "preferred_username":               | 
"user-c39y4yag",                      |   "email_verified": false,
  "email_verified": false,            |   "acr": "1",
  "acr": "1",                         |   "realm_access": {
  "realm_access": {                   |     "roles": [
    "roles": [                        |       "default-roles-i168realm",
      "default-roles-i168realm",      |       "offline_access",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment