diff --git a/beets/plugins.py b/beets/plugins.py index d87dd5d1e..6d3a8447e 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -654,66 +654,6 @@ def feat_tokens(for_artist: bool = True) -> str: ) -def sanitize_choices( - choices: Sequence[str], choices_all: Sequence[str] -) -> list[str]: - """Clean up a stringlist configuration attribute: keep only choices - elements present in choices_all, remove duplicate elements, expand '*' - wildcard while keeping original stringlist order. - """ - seen: set[str] = set() - others = [x for x in choices_all if x not in choices] - res: list[str] = [] - for s in choices: - if s not in seen: - if s in list(choices_all): - res.append(s) - elif s == "*": - res.extend(others) - seen.add(s) - return res - - -def sanitize_pairs( - pairs: Sequence[tuple[str, str]], pairs_all: Sequence[tuple[str, str]] -) -> list[tuple[str, str]]: - """Clean up a single-element mapping configuration attribute as returned - by Confuse's `Pairs` template: keep only two-element tuples present in - pairs_all, remove duplicate elements, expand ('str', '*') and ('*', '*') - wildcards while keeping the original order. Note that ('*', '*') and - ('*', 'whatever') have the same effect. - - For example, - - >>> sanitize_pairs( - ... [('foo', 'baz bar'), ('key', '*'), ('*', '*')], - ... [('foo', 'bar'), ('foo', 'baz'), ('foo', 'foobar'), - ... ('key', 'value')] - ... ) - [('foo', 'baz'), ('foo', 'bar'), ('key', 'value'), ('foo', 'foobar')] - """ - pairs_all = list(pairs_all) - seen: set[tuple[str, str]] = set() - others = [x for x in pairs_all if x not in pairs] - res: list[tuple[str, str]] = [] - for k, values in pairs: - for v in values.split(): - x = (k, v) - if x in pairs_all: - if x not in seen: - seen.add(x) - res.append(x) - elif k == "*": - new = [o for o in others if o not in seen] - seen.update(new) - res.extend(new) - elif v == "*": - new = [o for o in others if o not in seen and o[0] == k] - seen.update(new) - res.extend(new) - return res - - def get_distance( config: ConfigView, data_source: str, info: AlbumInfo | TrackInfo ) -> Distance: diff --git a/beets/util/config.py b/beets/util/config.py new file mode 100644 index 000000000..218a9d133 --- /dev/null +++ b/beets/util/config.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Collection, Sequence + + +def sanitize_choices( + choices: Sequence[str], choices_all: Collection[str] +) -> list[str]: + """Clean up a stringlist configuration attribute: keep only choices + elements present in choices_all, remove duplicate elements, expand '*' + wildcard while keeping original stringlist order. + """ + seen: set[str] = set() + others = [x for x in choices_all if x not in choices] + res: list[str] = [] + for s in choices: + if s not in seen: + if s in list(choices_all): + res.append(s) + elif s == "*": + res.extend(others) + seen.add(s) + return res + + +def sanitize_pairs( + pairs: Sequence[tuple[str, str]], pairs_all: Sequence[tuple[str, str]] +) -> list[tuple[str, str]]: + """Clean up a single-element mapping configuration attribute as returned + by Confuse's `Pairs` template: keep only two-element tuples present in + pairs_all, remove duplicate elements, expand ('str', '*') and ('*', '*') + wildcards while keeping the original order. Note that ('*', '*') and + ('*', 'whatever') have the same effect. + + For example, + + >>> sanitize_pairs( + ... [('foo', 'baz bar'), ('key', '*'), ('*', '*')], + ... [('foo', 'bar'), ('foo', 'baz'), ('foo', 'foobar'), + ... ('key', 'value')] + ... ) + [('foo', 'baz'), ('foo', 'bar'), ('key', 'value'), ('foo', 'foobar')] + """ + pairs_all = list(pairs_all) + seen: set[tuple[str, str]] = set() + others = [x for x in pairs_all if x not in pairs] + res: list[tuple[str, str]] = [] + for k, values in pairs: + for v in values.split(): + x = (k, v) + if x in pairs_all: + if x not in seen: + seen.add(x) + res.append(x) + elif k == "*": + new = [o for o in others if o not in seen] + seen.update(new) + res.extend(new) + elif v == "*": + new = [o for o in others if o not in seen and o[0] == k] + seen.update(new) + res.extend(new) + return res diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 3473fe08b..b442633da 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -32,6 +32,7 @@ from mediafile import image_mime_type from beets import config, importer, plugins, ui, util from beets.util import bytestring_path, get_temp_filename, sorted_walk, syspath from beets.util.artresizer import ArtResizer +from beets.util.config import sanitize_pairs if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Sequence @@ -1396,7 +1397,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if s_cls.available(self._log, self.config) for c in s_cls.VALID_MATCHING_CRITERIA ] - sources = plugins.sanitize_pairs( + sources = sanitize_pairs( self.config["sources"].as_pairs(default_value="*"), available_sources, ) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 3e979221c..e2c0c7fd2 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -39,6 +39,7 @@ from unidecode import unidecode import beets from beets import plugins, ui from beets.autotag.hooks import string_dist +from beets.util.config import sanitize_choices if TYPE_CHECKING: from logging import Logger @@ -957,7 +958,7 @@ class LyricsPlugin(RequestHandler, plugins.BeetsPlugin): def backends(self) -> list[Backend]: user_sources = self.config["sources"].get() - chosen = plugins.sanitize_choices(user_sources, self.BACKEND_BY_NAME) + chosen = sanitize_choices(user_sources, self.BACKEND_BY_NAME) if "google" in chosen and not self.config["google_API_key"].get(): self.warn("Disabling Google source: no API key configured.") chosen.remove("google") diff --git a/test/test_plugins.py b/test/test_plugins.py index 3e809e492..207522430 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -15,7 +15,6 @@ import itertools import os -import unittest from unittest.mock import ANY, Mock, patch import pytest @@ -215,15 +214,6 @@ class EventsTest(PluginImportTestCase): ] -class HelpersTest(unittest.TestCase): - def test_sanitize_choices(self): - assert plugins.sanitize_choices(["A", "Z"], ("A", "B")) == ["A"] - assert plugins.sanitize_choices(["A", "A"], ("A")) == ["A"] - assert plugins.sanitize_choices( - ["D", "*", "A"], ("A", "B", "C", "D") - ) == ["D", "B", "C", "A"] - - class ListenersTest(PluginLoaderTestCase): def test_register(self): class DummyPlugin(plugins.BeetsPlugin): diff --git a/test/util/test_config.py b/test/util/test_config.py new file mode 100644 index 000000000..0c49f85b1 --- /dev/null +++ b/test/util/test_config.py @@ -0,0 +1,15 @@ +import unittest + +from beets.util.config import sanitize_choices + + +class HelpersTest(unittest.TestCase): + def test_sanitize_choices(self): + assert sanitize_choices(["A", "Z"], ("A", "B")) == ["A"] + assert sanitize_choices(["A", "A"], ("A")) == ["A"] + assert sanitize_choices(["D", "*", "A"], ("A", "B", "C", "D")) == [ + "D", + "B", + "C", + "A", + ]