Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions beets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions beets/importer/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"]:
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions beets/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]

Expand Down
23 changes: 12 additions & 11 deletions beets/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions beets/ui/commands/import_/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions beetsplug/discogs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down
5 changes: 3 additions & 2 deletions beetsplug/fetchart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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),
Expand Down
22 changes: 10 additions & 12 deletions beetsplug/lastgenre/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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",),
Comment on lines +182 to +186
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The use of as_choice with a dict here likely returns a single string, not the tuple of sources you expect.

In confuse, as_choice treats a dict as {canonical: [aliases...]} and returns the key, not the value. Here that means you’ll only ever get 'track', 'album', or 'artist', never the tuples. The alias lists also mean e.g. 'album' in the config might be treated as an alias for 'track'. This changes the previous behaviour and is likely a bug. To map a single configured value to a tuple of sources, use as_choice over a simple list of strings and then map that string to the tuple in Python, or add a small helper to do that mapping explicitly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

}
)

# More canonicalization and general helpers.

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion beetsplug/lyrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions beetsplug/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] == "#":
Expand All @@ -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()
Expand Down
5 changes: 3 additions & 2 deletions beetsplug/smartplaylist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion beetsplug/titlecase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading