diff --git a/beets/__init__.py b/beets/__init__.py index 036b197ef..564c5b572 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..eee5afbf7 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 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 7995daefc..a2afc2661 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: diff --git a/poetry.lock b/poetry.lock index 8eb7c74ac..cab1bb0ca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -747,18 +747,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" @@ -4583,4 +4586,4 @@ web = ["flask", "flask-cors"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4" -content-hash = "f8ce55ae74c5e3c5d1d330582f83dae30ef963a0b8dd8c8b79f16c3bcfdb525a" +content-hash = "d00ffb911964e9943b08c5ae3de28867cd5636700bdfe4a70534b02843ea80d7" diff --git a/pyproject.toml b/pyproject.toml index aa3c9d5c7..1c35fb048 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"