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] Adapt code to fully typed confuse library --- beets/__init__.py | 4 ++-- beets/plugins.py | 4 ++-- beets/ui/__init__.py | 25 +++++++++++++------------ 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, 42 insertions(+), 41 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index 4bde53504..750efb3b3 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 01d9d3327..b1650d5b0 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -37,7 +37,7 @@ from beets.util.deprecation import deprecate_for_maintainers, deprecate_for_user 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 8db4dd79f..4c93d66d8 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -30,7 +30,6 @@ import textwrap import traceback from difflib import SequenceMatcher from functools import cache -from itertools import chain from typing import TYPE_CHECKING, Any, Literal import confuse @@ -551,18 +550,20 @@ 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]] = { - k: (v if isinstance(v, list) else LEGACY_COLORS.get(v, [v])) - for k, v in config["ui"]["colors"].flatten().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)}" + 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"].get(template).items() + } return { n: ";".join(str(CODE_BY_COLOR[c]) for c in colors) diff --git a/beets/ui/commands/import_/session.py b/beets/ui/commands/import_/session.py index 42a809634..1848e4192 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 dc88e0f14..bdbeb8fc0 100644 --- a/beetsplug/discogs/__init__.py +++ b/beetsplug/discogs/__init__.py @@ -355,10 +355,9 @@ class DiscogsPlugin(MetadataSourcePlugin): 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 ef311cbbd..e4de9181b 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -288,7 +288,8 @@ class Candidate: 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 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): # 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 1c91688a6..f7aef0261 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -41,6 +41,7 @@ if TYPE_CHECKING: 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 @@ class LastGenrePlugin(plugins.BeetsPlugin): """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 @@ class LastGenrePlugin(plugins.BeetsPlugin): 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 3b626a50b..72df907db 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -358,7 +358,7 @@ class LRCLib(Backend): 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 a1f9fff39..54a03646f 100644 --- a/beetsplug/playlist.py +++ b/beetsplug/playlist.py @@ -69,7 +69,7 @@ class PlaylistQuery(InQuery[bytes]): 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 @@ class PlaylistQuery(InQuery[bytes]): 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 e22a65787..a5cc8e362 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -262,8 +262,9 @@ class SmartPlaylistPlugin(BeetsPlugin): "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 d722d4d16..634f5fe4d 100644 --- a/beetsplug/titlecase.py +++ b/beetsplug/titlecase.py @@ -104,7 +104,7 @@ class TitlecasePlugin(BeetsPlugin): @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: