Create a dedicated get_model_changes function in beets/util/diff.py

- Add `get_model_changes` as the public API for computing diffs
- Remove tests for `show_model_changes` as test_diff.py fully covers them.
This commit is contained in:
Šarūnas Nejus 2026-03-14 11:47:15 +00:00
parent 1d54f2bf66
commit fc39ab791c
No known key found for this signature in database
3 changed files with 40 additions and 76 deletions

View file

@ -46,7 +46,7 @@ from beets.util.color import (
uncolorize,
)
from beets.util.deprecation import deprecate_for_maintainers
from beets.util.diff import _field_diff
from beets.util.diff import get_model_changes
from beets.util.functemplate import template
if TYPE_CHECKING:
@ -802,32 +802,14 @@ def show_model_changes(
always: bool = False,
print_obj: bool = True,
) -> bool:
"""Given a Model object, print a list of changes from its pristine
version stored in the database. Return a boolean indicating whether
any changes were found.
"""Print a diff of changes between two library model states.
`old` may be the "original" object to avoid using the pristine
version from the database. `fields` may be a list of fields to
restrict the detection to. `always` indicates whether the object is
always identified, regardless of whether any changes are present.
Optionally prints the original object label before listing field-level
changes when `print_obj` is enabled. When `always` is set, the object
label is printed even if no changes are detected. Returns whether any
changes were found.
"""
old = old or new.get_fresh_from_db()
# Keep the formatted views around instead of re-creating them in each
# iteration step
old_fmt = old.formatted()
new_fmt = new.formatted()
# Build up lines showing changed fields.
diff_fields = (set(old) | set(new)) - {"mtime"}
if allowed_fields := set(fields or {}):
diff_fields &= allowed_fields
changes = [
d
for f in sorted(diff_fields)
if (d := _field_diff(f, old_fmt, new_fmt))
]
changes = get_model_changes(new, old, fields)
# Print changes.
if print_obj and (changes or always):

View file

@ -6,7 +6,10 @@ from typing import TYPE_CHECKING
from .color import colorize
if TYPE_CHECKING:
from collections.abc import Iterable
from beets.dbcore.db import FormattedMapping
from beets.library.models import LibModel
def colordiff(a: str, b: str) -> tuple[str, str]:
@ -79,3 +82,33 @@ def _field_diff(
newstr = colorize("text_diff_added", newstr)
return f"{field}: {oldstr} -> {newstr}"
def get_model_changes(
new: LibModel,
old: LibModel | None,
fields: Iterable[str] | None,
) -> list[str]:
"""Compute human-readable diff lines for changed fields between two models.
Compares `new` against `old`, falling back to the database version of
`new` when `old` is not provided. When `fields` is given, only those
fields are considered. The `mtime` field is always excluded.
"""
old = old or new.get_fresh_from_db()
# Keep the formatted views around instead of re-creating them in each
# iteration step
old_fmt = old.formatted()
new_fmt = new.formatted()
# Build up lines showing changed fields.
diff_fields = (set(old) | set(new)) - {"mtime"}
if allowed_fields := set(fields or {}):
diff_fields &= allowed_fields
return [
d
for f in sorted(diff_fields)
if (d := _field_diff(f, old_fmt, new_fmt))
]

View file

@ -329,57 +329,6 @@ class ConfigTest(IOMixin, TestPluginTestCase):
assert config["statefile"].as_path() == self.beetsdir / "state"
class ShowModelChangeTest(IOMixin, unittest.TestCase):
def setUp(self):
super().setUp()
self.a = _common.item()
self.b = _common.item()
self.a.path = self.b.path
def _show(self, **kwargs):
change = ui.show_model_changes(self.a, self.b, **kwargs)
out = self.io.getoutput()
return change, out
def test_identical(self):
change, out = self._show()
assert not change
assert out == ""
def test_string_fixed_field_change(self):
self.b.title = "x"
change, out = self._show()
assert change
assert "title" in out
def test_int_fixed_field_change(self):
self.b.track = 9
change, out = self._show()
assert change
assert "track" in out
def test_floats_close_to_identical(self):
self.a.length = 1.00001
self.b.length = 1.00005
change, out = self._show()
assert not change
assert out == ""
def test_floats_different(self):
self.a.length = 1.00001
self.b.length = 2.00001
change, out = self._show()
assert change
assert "length" in out
def test_both_values_shown(self):
self.a.title = "foo"
self.b.title = "bar"
_, out = self._show()
assert "foo" in out
assert "bar" in out
class PathFormatTest(unittest.TestCase):
def test_custom_paths_prepend(self):
default_formats = ui.get_path_formats()