From b171ee811daa74407a04a17745d6190f7f72413d 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] Adapt code to fully typed confuse library --- beets/__init__.py | 4 ++-- beets/importer/session.py | 8 +++++--- beets/plugins.py | 4 ++-- beets/ui/__init__.py | 23 ++++++++++++----------- beets/ui/commands/import_/session.py | 1 + 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 +- 12 files changed, 45 insertions(+), 42 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index 2c6069b294..b6d4b2103f 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/importer/session.py b/beets/importer/session.py index 123cc72480..c50f4cfd63 100644 --- a/beets/importer/session.py +++ b/beets/importer/session.py @@ -27,6 +27,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from confuse import Subview + from beets import dbcore, library from beets.util import PathBytes @@ -97,14 +99,13 @@ def _setup_logging(self, loghandler: logging.Handler | None): logger.handlers = [loghandler] return logger - def set_config(self, config): + def set_config(self, config: Subview): """Set `config` property from global import config and make implied changes. """ # FIXME: Maybe this function should not exist and should instead # provide "decision wrappers" like "should_resume()", etc. - iconfig = dict(config) - self.config = iconfig + iconfig = config # Incremental and progress are mutually exclusive. if iconfig["incremental"]: @@ -148,6 +149,7 @@ def set_config(self, config): iconfig["delete"] = False self.want_resume = config["resume"].as_choice([True, False, "ask"]) + self.config = dict(iconfig) def tag_log(self, status, paths: Sequence[PathBytes]): """Log a message about a given album to the importer log. The status diff --git a/beets/plugins.py b/beets/plugins.py index ec3f999c4c..4d6f03d2b9 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -37,7 +37,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable, Sequence - from confuse import ConfigView + from confuse import Subview from beets.dbcore import Query from beets.dbcore.db import FieldQueryType @@ -163,7 +163,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 5eeef815de..3ee5f115aa 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 @@ -535,19 +534,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..eee5afbf7f 100644 --- a/beets/ui/commands/import_/session.py +++ b/beets/ui/commands/import_/session.py @@ -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 121d76596a..8f1cba0a6b 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. @@ -596,10 +596,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 7995daefc7..a2afc26619 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: