From 37e18fbb46df8e2b9c5ebc49dd8dc2ee77d8e94e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 25 Jan 2026 06:45:07 +0000 Subject: [PATCH 1/2] Adapt code to fully typed confuse library --- beets/__init__.py | 4 ++-- beets/plugins.py | 4 ++-- beets/ui/__init__.py | 23 ++++++++++++----------- beets/ui/commands/import_/session.py | 3 ++- beetsplug/discogs/__init__.py | 7 +++---- beetsplug/fetchart.py | 5 +++-- beetsplug/lastgenre/__init__.py | 22 ++++++++++------------ beetsplug/lyrics.py | 2 +- beetsplug/playlist.py | 4 ++-- beetsplug/smartplaylist.py | 5 +++-- beetsplug/titlecase.py | 2 +- 11 files changed, 41 insertions(+), 40 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index 4bde535043..750efb3b39 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -37,11 +37,11 @@ class IncludeLazyConfig(confuse.LazyConfig): YAML files specified in an `include` setting. """ - def read(self, user=True, defaults=True): + def read(self, user: bool = True, defaults: bool = True) -> None: super().read(user, defaults) try: - for view in self["include"]: + for view in self["include"].sequence(): self.set_file(view.as_filename()) except confuse.NotFoundError: pass diff --git a/beets/plugins.py b/beets/plugins.py index 01d9d33272..b1650d5b04 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -37,7 +37,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable, Iterator, Sequence - from confuse import ConfigView + from confuse import Subview from beets.dbcore import Query from beets.dbcore.db import FieldQueryType @@ -162,7 +162,7 @@ class BeetsPlugin(metaclass=BeetsPluginMeta): album_template_fields: TFuncMap[Album] name: str - config: ConfigView + config: Subview early_import_stages: list[ImportStageFunc] import_stages: list[ImportStageFunc] diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 8db4dd79f8..4c93d66d80 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -30,7 +30,6 @@ import traceback from difflib import SequenceMatcher from functools import cache -from itertools import chain from typing import TYPE_CHECKING, Any, Literal import confuse @@ -551,19 +550,21 @@ def get_color_config() -> dict[ColorName, str]: legacy single-color format. Validates all color names against known codes and raises an error for any invalid entries. """ - colors_by_color_name: dict[ColorName, list[str]] = { + template_dict: dict[ColorName, confuse.OneOf[str | list[str]]] = { + n: confuse.OneOf( + [ + confuse.Choice(sorted(LEGACY_COLORS)), + confuse.Sequence(confuse.Choice(sorted(CODE_BY_COLOR))), + ] + ) + for n in ColorName.__args__ # type: ignore[attr-defined] + } + template = confuse.MappingTemplate(template_dict) + colors_by_color_name = { k: (v if isinstance(v, list) else LEGACY_COLORS.get(v, [v])) - for k, v in config["ui"]["colors"].flatten().items() + for k, v in config["ui"]["colors"].get(template).items() } - if invalid_colors := ( - set(chain.from_iterable(colors_by_color_name.values())) - - CODE_BY_COLOR.keys() - ): - raise UserError( - f"Invalid color(s) in configuration: {', '.join(invalid_colors)}" - ) - return { n: ";".join(str(CODE_BY_COLOR[c]) for c in colors) for n, colors in colors_by_color_name.items() diff --git a/beets/ui/commands/import_/session.py b/beets/ui/commands/import_/session.py index 42a8096342..1848e41925 100644 --- a/beets/ui/commands/import_/session.py +++ b/beets/ui/commands/import_/session.py @@ -327,7 +327,7 @@ def summarize_items(items, singleton): return ", ".join(summary_parts) -def _summary_judgment(rec): +def _summary_judgment(rec: Recommendation) -> importer.Action | None: """Determines whether a decision should be made without even asking the user. This occurs in quiet mode and when an action is chosen for NONE recommendations. Return None if the user should be queried. @@ -335,6 +335,7 @@ def _summary_judgment(rec): summary judgment is made. """ + action: importer.Action | None if config["import"]["quiet"]: if rec == Recommendation.strong: return importer.Action.APPLY diff --git a/beetsplug/discogs/__init__.py b/beetsplug/discogs/__init__.py index dc88e0f148..bdbeb8fc04 100644 --- a/beetsplug/discogs/__init__.py +++ b/beetsplug/discogs/__init__.py @@ -355,10 +355,9 @@ def get_album_info(self, result: Release) -> AlbumInfo | None: style = self.format(result.data.get("styles")) base_genre = self.format(result.data.get("genres")) - if self.config["append_style_genre"] and style: - genre = self.config["separator"].as_str().join([base_genre, style]) - else: - genre = base_genre + genre = base_genre + if self.config["append_style_genre"] and genre is not None and style: + genre += f"{self.config['separator'].as_str()}{style}" discogs_albumid = self._extract_id(result.data.get("uri")) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index ef311cbbd8..e4de9181b5 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -288,7 +288,8 @@ def _resize( elif check == ImageAction.REFORMAT: self.path = ArtResizer.shared.reformat( self.path, - plugin.cover_format, + # TODO: fix this gnarly logic to remove the need for type ignore + plugin.cover_format, # type: ignore[arg-type] deinterlaced=plugin.deinterlace, ) @@ -1367,7 +1368,7 @@ def __init__(self) -> None: # allow both pixel and percentage-based margin specifications self.enforce_ratio = self.config["enforce_ratio"].get( - confuse.OneOf( + confuse.OneOf[bool | str]( [ bool, confuse.String(pattern=self.PAT_PX), diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 1c91688a69..f7aef02611 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -41,6 +41,7 @@ import optparse from collections.abc import Callable + from beets.importer import ImportSession, ImportTask from beets.library import LibModel LASTFM = pylast.LastFMNetwork(api_key=plugins.LASTFM_KEY) @@ -178,14 +179,13 @@ def sources(self) -> tuple[str, ...]: """A tuple of allowed genre sources. May contain 'track', 'album', or 'artist.' """ - source = self.config["source"].as_choice(("track", "album", "artist")) - if source == "track": - return "track", "album", "artist" - if source == "album": - return "album", "artist" - if source == "artist": - return ("artist",) - return tuple() + return self.config["source"].as_choice( + { + "track": ("track", "album", "artist"), + "album": ("album", "artist"), + "artist": ("artist",), + } + ) # More canonicalization and general helpers. @@ -603,10 +603,8 @@ def lastgenre_func( lastgenre_cmd.func = lastgenre_func return [lastgenre_cmd] - def imported( - self, session: library.Session, task: library.ImportTask - ) -> None: - self._process(task.album if task.is_album else task.item, write=False) + def imported(self, _: ImportSession, task: ImportTask) -> None: + self._process(task.album if task.is_album else task.item, write=False) # type: ignore[attr-defined] def _tags_for( self, diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 3b626a50b7..72df907db3 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -358,7 +358,7 @@ def fetch( for group in self.fetch_candidates(artist, title, album, length): candidates = [evaluate_item(item) for item in group] if item := self.pick_best_match(candidates): - lyrics = item.get_text(self.config["synced"]) + lyrics = item.get_text(self.config["synced"].get(bool)) return lyrics, f"{self.GET_URL}/{item.id}" return None diff --git a/beetsplug/playlist.py b/beetsplug/playlist.py index a1f9fff395..54a03646f7 100644 --- a/beetsplug/playlist.py +++ b/beetsplug/playlist.py @@ -69,7 +69,7 @@ def __init__(self, _, pattern: str, __): relative_to = os.path.dirname(playlist_path) else: relative_to = config["relative_to"].as_filename() - relative_to = beets.util.bytestring_path(relative_to) + relative_to_bytes = beets.util.bytestring_path(relative_to) for line in f: if line[0] == "#": @@ -78,7 +78,7 @@ def __init__(self, _, pattern: str, __): paths.append( beets.util.normpath( - os.path.join(relative_to, line.rstrip()) + os.path.join(relative_to_bytes, line.rstrip()) ) ) f.close() diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index e22a65787c..a5cc8e3624 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -262,8 +262,9 @@ def update_playlists(self, lib: Library, pretend: bool = False) -> None: "Updating {} smart playlists...", len(self._matched_playlists) ) - playlist_dir = self.config["playlist_dir"].as_filename() - playlist_dir = bytestring_path(playlist_dir) + playlist_dir = bytestring_path( + self.config["playlist_dir"].as_filename() + ) tpl = self.config["uri_format"].get() prefix = bytestring_path(self.config["prefix"].as_str()) relative_to = self.config["relative_to"].get() diff --git a/beetsplug/titlecase.py b/beetsplug/titlecase.py index d722d4d163..634f5fe4da 100644 --- a/beetsplug/titlecase.py +++ b/beetsplug/titlecase.py @@ -104,7 +104,7 @@ def force_lowercase(self) -> bool: @cached_property def replace(self) -> list[tuple[str, str]]: - return self.config["replace"].as_pairs() + return self.config["replace"].as_pairs(default_value="") @cached_property def the_artist(self) -> bool: From 15755c1ff9ce4d270e7ec81d96867b6fed6a1c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Wed, 28 Jan 2026 10:46:37 +0000 Subject: [PATCH 2/2] Update confuse --- poetry.lock | 13 ++++++++----- pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1a0515819a..023ea77188 100644 --- a/poetry.lock +++ b/poetry.lock @@ -722,18 +722,21 @@ files = [ [[package]] name = "confuse" -version = "2.1.0" +version = "2.2.0" description = "Painless YAML config files" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "confuse-2.1.0-py3-none-any.whl", hash = "sha256:502be1299aa6bf7c48f7719f56795720c073fb28550c0c7a37394366c9d30316"}, - {file = "confuse-2.1.0.tar.gz", hash = "sha256:abb9674a99c7a6efaef84e2fc84403ecd2dd304503073ff76ea18ed4176e218d"}, + {file = "confuse-2.2.0-py3-none-any.whl", hash = "sha256:470c6aa1a5008c8d740267f2ad574e3a715b6dd873c1e5f8778b7f7abb954722"}, + {file = "confuse-2.2.0.tar.gz", hash = "sha256:35c1b53e81be125f441bee535130559c935917b26aeaa61289010cd1f55c2b9e"}, ] [package.dependencies] pyyaml = "*" +[package.extras] +docs = ["sphinx (>=7.4.7)", "sphinx-rtd-theme (>=3.0.2)"] + [[package]] name = "coverage" version = "7.11.0" @@ -4558,4 +4561,4 @@ web = ["flask", "flask-cors"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4" -content-hash = "eefe427d3b3b9b871ca6bcd8405e3578a16d660afd7925c14793514f03c96ac6" +content-hash = "9cff39f63616b2654fbf44b006f7eedcae6c1846fbb9f04af82483891b7d77b9" diff --git a/pyproject.toml b/pyproject.toml index 2a0a1904d8..57a12fa751 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ Changelog = "https://github.com/beetbox/beets/blob/master/docs/changelog.rst" python = ">=3.10,<4" colorama = { version = "*", markers = "sys_platform == 'win32'" } -confuse = ">=2.1.0" +confuse = ">=2.2.0" jellyfish = "*" lap = ">=0.5.12" mediafile = ">=0.12.0"