From fc39ab791c45d9e996500fa894c3d58d12e7d75e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 14 Mar 2026 11:47:15 +0000 Subject: [PATCH] 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. --- beets/ui/__init__.py | 32 ++++++--------------------- beets/util/diff.py | 33 ++++++++++++++++++++++++++++ test/ui/test_ui.py | 51 -------------------------------------------- 3 files changed, 40 insertions(+), 76 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 930b918f5..dc7e35060 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -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): diff --git a/beets/util/diff.py b/beets/util/diff.py index c01a35a6e..9feb65897 100644 --- a/beets/util/diff.py +++ b/beets/util/diff.py @@ -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)) + ] diff --git a/test/ui/test_ui.py b/test/ui/test_ui.py index 577954a85..dd5a2b460 100644 --- a/test/ui/test_ui.py +++ b/test/ui/test_ui.py @@ -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()