Skip to content

Commit 509cbdc

Browse files
committed
Move sanitize_pairs/choices from plugins to util module
1 parent 0f76312 commit 509cbdc

File tree

6 files changed

+85
-72
lines changed

6 files changed

+85
-72
lines changed

beets/plugins.py

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -654,66 +654,6 @@ def feat_tokens(for_artist: bool = True) -> str:
654654
)
655655

656656

657-
def sanitize_choices(
658-
choices: Sequence[str], choices_all: Sequence[str]
659-
) -> list[str]:
660-
"""Clean up a stringlist configuration attribute: keep only choices
661-
elements present in choices_all, remove duplicate elements, expand '*'
662-
wildcard while keeping original stringlist order.
663-
"""
664-
seen: set[str] = set()
665-
others = [x for x in choices_all if x not in choices]
666-
res: list[str] = []
667-
for s in choices:
668-
if s not in seen:
669-
if s in list(choices_all):
670-
res.append(s)
671-
elif s == "*":
672-
res.extend(others)
673-
seen.add(s)
674-
return res
675-
676-
677-
def sanitize_pairs(
678-
pairs: Sequence[tuple[str, str]], pairs_all: Sequence[tuple[str, str]]
679-
) -> list[tuple[str, str]]:
680-
"""Clean up a single-element mapping configuration attribute as returned
681-
by Confuse's `Pairs` template: keep only two-element tuples present in
682-
pairs_all, remove duplicate elements, expand ('str', '*') and ('*', '*')
683-
wildcards while keeping the original order. Note that ('*', '*') and
684-
('*', 'whatever') have the same effect.
685-
686-
For example,
687-
688-
>>> sanitize_pairs(
689-
... [('foo', 'baz bar'), ('key', '*'), ('*', '*')],
690-
... [('foo', 'bar'), ('foo', 'baz'), ('foo', 'foobar'),
691-
... ('key', 'value')]
692-
... )
693-
[('foo', 'baz'), ('foo', 'bar'), ('key', 'value'), ('foo', 'foobar')]
694-
"""
695-
pairs_all = list(pairs_all)
696-
seen: set[tuple[str, str]] = set()
697-
others = [x for x in pairs_all if x not in pairs]
698-
res: list[tuple[str, str]] = []
699-
for k, values in pairs:
700-
for v in values.split():
701-
x = (k, v)
702-
if x in pairs_all:
703-
if x not in seen:
704-
seen.add(x)
705-
res.append(x)
706-
elif k == "*":
707-
new = [o for o in others if o not in seen]
708-
seen.update(new)
709-
res.extend(new)
710-
elif v == "*":
711-
new = [o for o in others if o not in seen and o[0] == k]
712-
seen.update(new)
713-
res.extend(new)
714-
return res
715-
716-
717657
def get_distance(
718658
config: ConfigView, data_source: str, info: AlbumInfo | TrackInfo
719659
) -> Distance:

beets/util/config.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
if TYPE_CHECKING:
6+
from collections.abc import Collection, Sequence
7+
8+
9+
def sanitize_choices(
10+
choices: Sequence[str], choices_all: Collection[str]
11+
) -> list[str]:
12+
"""Clean up a stringlist configuration attribute: keep only choices
13+
elements present in choices_all, remove duplicate elements, expand '*'
14+
wildcard while keeping original stringlist order.
15+
"""
16+
seen: set[str] = set()
17+
others = [x for x in choices_all if x not in choices]
18+
res: list[str] = []
19+
for s in choices:
20+
if s not in seen:
21+
if s in list(choices_all):
22+
res.append(s)
23+
elif s == "*":
24+
res.extend(others)
25+
seen.add(s)
26+
return res
27+
28+
29+
def sanitize_pairs(
30+
pairs: Sequence[tuple[str, str]], pairs_all: Sequence[tuple[str, str]]
31+
) -> list[tuple[str, str]]:
32+
"""Clean up a single-element mapping configuration attribute as returned
33+
by Confuse's `Pairs` template: keep only two-element tuples present in
34+
pairs_all, remove duplicate elements, expand ('str', '*') and ('*', '*')
35+
wildcards while keeping the original order. Note that ('*', '*') and
36+
('*', 'whatever') have the same effect.
37+
38+
For example,
39+
40+
>>> sanitize_pairs(
41+
... [('foo', 'baz bar'), ('key', '*'), ('*', '*')],
42+
... [('foo', 'bar'), ('foo', 'baz'), ('foo', 'foobar'),
43+
... ('key', 'value')]
44+
... )
45+
[('foo', 'baz'), ('foo', 'bar'), ('key', 'value'), ('foo', 'foobar')]
46+
"""
47+
pairs_all = list(pairs_all)
48+
seen: set[tuple[str, str]] = set()
49+
others = [x for x in pairs_all if x not in pairs]
50+
res: list[tuple[str, str]] = []
51+
for k, values in pairs:
52+
for v in values.split():
53+
x = (k, v)
54+
if x in pairs_all:
55+
if x not in seen:
56+
seen.add(x)
57+
res.append(x)
58+
elif k == "*":
59+
new = [o for o in others if o not in seen]
60+
seen.update(new)
61+
res.extend(new)
62+
elif v == "*":
63+
new = [o for o in others if o not in seen and o[0] == k]
64+
seen.update(new)
65+
res.extend(new)
66+
return res

beetsplug/fetchart.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from beets import config, importer, plugins, ui, util
3333
from beets.util import bytestring_path, get_temp_filename, sorted_walk, syspath
3434
from beets.util.artresizer import ArtResizer
35+
from beets.util.config import sanitize_pairs
3536

3637
if TYPE_CHECKING:
3738
from collections.abc import Iterable, Iterator, Sequence
@@ -1396,7 +1397,7 @@ def __init__(self) -> None:
13961397
if s_cls.available(self._log, self.config)
13971398
for c in s_cls.VALID_MATCHING_CRITERIA
13981399
]
1399-
sources = plugins.sanitize_pairs(
1400+
sources = sanitize_pairs(
14001401
self.config["sources"].as_pairs(default_value="*"),
14011402
available_sources,
14021403
)

beetsplug/lyrics.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import beets
4040
from beets import plugins, ui
4141
from beets.autotag.hooks import string_dist
42+
from beets.util.config import sanitize_choices
4243

4344
if TYPE_CHECKING:
4445
from logging import Logger
@@ -957,7 +958,7 @@ class LyricsPlugin(RequestHandler, plugins.BeetsPlugin):
957958
def backends(self) -> list[Backend]:
958959
user_sources = self.config["sources"].get()
959960

960-
chosen = plugins.sanitize_choices(user_sources, self.BACKEND_BY_NAME)
961+
chosen = sanitize_choices(user_sources, self.BACKEND_BY_NAME)
961962
if "google" in chosen and not self.config["google_API_key"].get():
962963
self.warn("Disabling Google source: no API key configured.")
963964
chosen.remove("google")

test/test_plugins.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import itertools
1717
import os
18-
import unittest
1918
from unittest.mock import ANY, Mock, patch
2019

2120
import pytest
@@ -215,15 +214,6 @@ def import_task_created_event(self, session, task):
215214
]
216215

217216

218-
class HelpersTest(unittest.TestCase):
219-
def test_sanitize_choices(self):
220-
assert plugins.sanitize_choices(["A", "Z"], ("A", "B")) == ["A"]
221-
assert plugins.sanitize_choices(["A", "A"], ("A")) == ["A"]
222-
assert plugins.sanitize_choices(
223-
["D", "*", "A"], ("A", "B", "C", "D")
224-
) == ["D", "B", "C", "A"]
225-
226-
227217
class ListenersTest(PluginLoaderTestCase):
228218
def test_register(self):
229219
class DummyPlugin(plugins.BeetsPlugin):

test/util/test_config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import unittest
2+
3+
from beets.util.config import sanitize_choices
4+
5+
6+
class HelpersTest(unittest.TestCase):
7+
def test_sanitize_choices(self):
8+
assert sanitize_choices(["A", "Z"], ("A", "B")) == ["A"]
9+
assert sanitize_choices(["A", "A"], ("A")) == ["A"]
10+
assert sanitize_choices(["D", "*", "A"], ("A", "B", "C", "D")) == [
11+
"D",
12+
"B",
13+
"C",
14+
"A",
15+
]

0 commit comments

Comments
 (0)