mirror of
https://github.com/beetbox/beets.git
synced 2026-03-26 15:24:05 +01:00
Move diff utils to beets/util/diff.py
This commit is contained in:
parent
a6fcb7ba0f
commit
756d5c9921
4 changed files with 83 additions and 76 deletions
|
|
@ -28,7 +28,6 @@ import sqlite3
|
|||
import sys
|
||||
import textwrap
|
||||
import traceback
|
||||
from difflib import SequenceMatcher
|
||||
from functools import cache
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
|
|
@ -47,13 +46,12 @@ from beets.util.color import (
|
|||
uncolorize,
|
||||
)
|
||||
from beets.util.deprecation import deprecate_for_maintainers
|
||||
from beets.util.diff import _field_diff
|
||||
from beets.util.functemplate import template
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
from beets.dbcore.db import FormattedMapping
|
||||
|
||||
|
||||
# On Windows platforms, use colorama to support "ANSI" terminal colors.
|
||||
if sys.platform == "win32":
|
||||
|
|
@ -444,25 +442,6 @@ def input_select_objects(prompt, objs, rep, prompt_all=None):
|
|||
return []
|
||||
|
||||
|
||||
def colordiff(a: str, b: str) -> tuple[str, str]:
|
||||
"""Intelligently highlight the differences between two strings."""
|
||||
before = ""
|
||||
after = ""
|
||||
|
||||
matcher = SequenceMatcher(lambda _: False, a, b)
|
||||
for op, a_start, a_end, b_start, b_end in matcher.get_opcodes():
|
||||
before_part, after_part = a[a_start:a_end], b[b_start:b_end]
|
||||
if op in {"delete", "replace"}:
|
||||
before_part = colorize("text_diff_removed", before_part)
|
||||
if op in {"insert", "replace"}:
|
||||
after_part = colorize("text_diff_added", after_part)
|
||||
|
||||
before += before_part
|
||||
after += after_part
|
||||
|
||||
return before, after
|
||||
|
||||
|
||||
def get_path_formats(subview=None):
|
||||
"""Get the configuration's path formats as a list of query/template
|
||||
pairs.
|
||||
|
|
@ -816,59 +795,6 @@ def print_newline_layout(
|
|||
print_(f"{indent_str * 2}{line}")
|
||||
|
||||
|
||||
FLOAT_EPSILON = 0.01
|
||||
|
||||
|
||||
def _multi_value_diff(field: str, oldset: set[str], newset: set[str]) -> str:
|
||||
added = newset - oldset
|
||||
removed = oldset - newset
|
||||
|
||||
parts = [
|
||||
f"{field}:",
|
||||
*(colorize("text_diff_removed", f" - {i}") for i in sorted(removed)),
|
||||
*(colorize("text_diff_added", f" + {i}") for i in sorted(added)),
|
||||
]
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _field_diff(
|
||||
field: str, old: FormattedMapping, new: FormattedMapping
|
||||
) -> str | None:
|
||||
"""Given two Model objects and their formatted views, format their values
|
||||
for `field` and highlight changes among them. Return a human-readable
|
||||
string. If the value has not changed, return None instead.
|
||||
"""
|
||||
# If no change, abort.
|
||||
if (oldval := old.model.get(field)) == (newval := new.model.get(field)) or (
|
||||
isinstance(oldval, float)
|
||||
and isinstance(newval, float)
|
||||
and abs(oldval - newval) < FLOAT_EPSILON
|
||||
):
|
||||
return None
|
||||
|
||||
if isinstance(oldval, list):
|
||||
if (oldset := set(oldval)) != (newset := set(newval)):
|
||||
return _multi_value_diff(field, oldset, newset)
|
||||
return None
|
||||
|
||||
# Get formatted values for output.
|
||||
oldstr, newstr = old.get(field, ""), new.get(field, "")
|
||||
if field not in new:
|
||||
return colorize("text_diff_removed", f"{field}: {oldstr}")
|
||||
|
||||
if field not in old:
|
||||
return colorize("text_diff_added", f"{field}: {newstr}")
|
||||
|
||||
# For strings, highlight changes. For others, colorize the whole thing.
|
||||
if isinstance(oldval, str):
|
||||
oldstr, newstr = colordiff(oldstr, newstr)
|
||||
else:
|
||||
oldstr = colorize("text_diff_removed", oldstr)
|
||||
newstr = colorize("text_diff_added", newstr)
|
||||
|
||||
return f"{field}: {oldstr} -> {newstr}"
|
||||
|
||||
|
||||
def show_model_changes(
|
||||
new: library.LibModel,
|
||||
old: library.LibModel | None = None,
|
||||
|
|
|
|||
81
beets/util/diff.py
Normal file
81
beets/util/diff.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from difflib import SequenceMatcher
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .color import colorize
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beets.dbcore.db import FormattedMapping
|
||||
|
||||
|
||||
def colordiff(a: str, b: str) -> tuple[str, str]:
|
||||
"""Intelligently highlight the differences between two strings."""
|
||||
before = ""
|
||||
after = ""
|
||||
|
||||
matcher = SequenceMatcher(lambda _: False, a, b)
|
||||
for op, a_start, a_end, b_start, b_end in matcher.get_opcodes():
|
||||
before_part, after_part = a[a_start:a_end], b[b_start:b_end]
|
||||
if op in {"delete", "replace"}:
|
||||
before_part = colorize("text_diff_removed", before_part)
|
||||
if op in {"insert", "replace"}:
|
||||
after_part = colorize("text_diff_added", after_part)
|
||||
|
||||
before += before_part
|
||||
after += after_part
|
||||
|
||||
return before, after
|
||||
|
||||
|
||||
FLOAT_EPSILON = 0.01
|
||||
|
||||
|
||||
def _multi_value_diff(field: str, oldset: set[str], newset: set[str]) -> str:
|
||||
added = newset - oldset
|
||||
removed = oldset - newset
|
||||
|
||||
parts = [
|
||||
f"{field}:",
|
||||
*(colorize("text_diff_removed", f" - {i}") for i in sorted(removed)),
|
||||
*(colorize("text_diff_added", f" + {i}") for i in sorted(added)),
|
||||
]
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _field_diff(
|
||||
field: str, old: FormattedMapping, new: FormattedMapping
|
||||
) -> str | None:
|
||||
"""Given two Model objects and their formatted views, format their values
|
||||
for `field` and highlight changes among them. Return a human-readable
|
||||
string. If the value has not changed, return None instead.
|
||||
"""
|
||||
# If no change, abort.
|
||||
if (oldval := old.model.get(field)) == (newval := new.model.get(field)) or (
|
||||
isinstance(oldval, float)
|
||||
and isinstance(newval, float)
|
||||
and abs(oldval - newval) < FLOAT_EPSILON
|
||||
):
|
||||
return None
|
||||
|
||||
if isinstance(oldval, list):
|
||||
if (oldset := set(oldval)) != (newset := set(newval)):
|
||||
return _multi_value_diff(field, oldset, newset)
|
||||
return None
|
||||
|
||||
# Get formatted values for output.
|
||||
oldstr, newstr = old.get(field, ""), new.get(field, "")
|
||||
if field not in new:
|
||||
return colorize("text_diff_removed", f"{field}: {oldstr}")
|
||||
|
||||
if field not in old:
|
||||
return colorize("text_diff_added", f"{field}: {newstr}")
|
||||
|
||||
# For strings, highlight changes. For others, colorize the whole thing.
|
||||
if isinstance(oldval, str):
|
||||
oldstr, newstr = colordiff(oldstr, newstr)
|
||||
else:
|
||||
oldstr = colorize("text_diff_removed", oldstr)
|
||||
newstr = colorize("text_diff_added", newstr)
|
||||
|
||||
return f"{field}: {oldstr} -> {newstr}"
|
||||
|
|
@ -329,7 +329,7 @@ ignore = [
|
|||
"beets/**" = ["PT"]
|
||||
"test/plugins/test_ftintitle.py" = ["E501"]
|
||||
"test/test_util.py" = ["E501"]
|
||||
"test/ui/test_field_diff.py" = ["E501"]
|
||||
"test/util/test_diff.py" = ["E501"]
|
||||
"test/util/test_id_extractors.py" = ["E501"]
|
||||
"test/**" = ["RUF001"] # we use Unicode characters in tests
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue