lint: enable flake8-pie, flake8-return, flake8-builtins (#6691)

- Enables additional Ruff lint rules in `pyproject.toml`: `PIE`, `RET`,
and `A`.
- Applies the required cleanup across the codebase, mostly as
non-functional refactors:
  - simplifies return/control-flow patterns
- renames variables that shadow Python builtins like `id`, `dir`,
`type`, `hash`, and `input`
- adds a few targeted `noqa` exceptions where names are part of stable
APIs or intentional signatures
- Touches core modules, plugins, tests, and docs, but the architectural
impact is low: this is mainly a repo-wide consistency and
maintainability pass rather than a behavior change.
- Updates `.git-blame-ignore-revs` so these broad lint-only commits are
ignored in blame, keeping future history easier to review.
This commit is contained in:
Šarūnas Nejus
2026-06-09 00:24:18 +01:00
committed by GitHub
92 changed files with 501 additions and 641 deletions

View File

@@ -197,3 +197,9 @@ adfb73b66dba0fc799880af176ef6f9ac0a7ee68
ca8544d4bcab503e1e4c2cd6688655c8211aa7a1
# Fix autobpm tests
5804a21620098e8da78b1f4a5f85063033586632
# Enable and fix flake8-pie
3a419c6a347803fc962f018f5ad027b5399ab3ae
# Enable and fix flake8-return
d0dc5f86a626765c86cdadc348595429f4c6e6ef
# Enable and fix flake8-builtins
b7974ace83ab193b6417bdcecaf68a742bf5a1a8

View File

@@ -528,9 +528,8 @@ class TrackInfo(Info):
if not raw_track[k] and (v := album.get(k)):
track[k] = v
merged = (
return (
album_info.item_data
| {"tracktotal": len(album_info.tracks)}
| track.item_data
)
return merged

View File

@@ -519,8 +519,7 @@ def tag_item(
if candidates:
assert rec is not None
return Proposal(_sort_candidates(candidates.values()), rec)
else:
return Proposal([], Recommendation.none)
return Proposal([], Recommendation.none)
# Search terms.
search_artist = search_artist or item.artist

View File

@@ -135,8 +135,7 @@ class FormattedMapping(Mapping[str, str]):
def __getitem__(self, key: str) -> str:
if key in self.model_keys:
return self._get_formatted(self.model, key)
else:
raise KeyError(key)
raise KeyError(key)
def __iter__(self) -> Iterator[str]:
return iter(self.model_keys)
@@ -210,10 +209,11 @@ class LazyConvertDict:
"""
if key in self._converted:
return self._converted[key]
elif key in self._data:
if key in self._data:
value = self._convert(key, self._data[key])
self._converted[key] = value
return value
return None
def __delitem__(self, key: str):
"""Delete both converted and base data"""
@@ -253,8 +253,7 @@ class LazyConvertDict:
"""
if key in self:
return self[key]
else:
return default
return default
def __contains__(self, key: Any) -> bool:
"""Determine whether `key` is an attribute on this object."""
@@ -520,17 +519,15 @@ class Model(ABC, Generic[D]):
getters = self._getters()
if key in getters: # Computed.
return getters[key](self)
elif key in self._fields: # Fixed.
if key in self._fields: # Fixed.
if key in self._values_fixed:
return self._values_fixed[key]
else:
return self._type(key).null
elif key in self._values_flex: # Flexible.
return self._type(key).null
if key in self._values_flex: # Flexible.
return self._values_flex[key]
elif raise_:
if raise_:
raise KeyError(key)
else:
return default
return default
get = _get
@@ -625,11 +622,10 @@ class Model(ABC, Generic[D]):
def __getattr__(self, key):
if key.startswith("_"):
raise AttributeError(f"model has no attribute {key!r}")
else:
try:
return self[key]
except KeyError:
raise AttributeError(f"no such field {key!r}")
try:
return self[key]
except KeyError:
raise AttributeError(f"no such field {key!r}")
def __setattr__(self, key, value):
if key.startswith("_"):
@@ -884,9 +880,8 @@ class Results(Generic[AnyModel]):
objects = self.sort.sort(list(self._get_objects()))
return iter(objects)
else:
# Objects are pre-sorted (i.e., by the database).
return self._get_objects()
# Objects are pre-sorted (i.e., by the database).
return self._get_objects()
def _get_indexed_flex_attrs(self) -> dict[int, FlexAttrs]:
"""Index flexible attributes by the entity id they belong to"""
@@ -907,8 +902,7 @@ class Results(Generic[AnyModel]):
values = {k: v for (k, v) in cols.items() if not k[:4] == "flex"}
# Construct the Python object
obj = self.model_class._awaken(self.db, values, flex_values)
return obj
return self.model_class._awaken(self.db, values, flex_values)
def __len__(self) -> int:
"""Get the number of matching objects."""
@@ -916,16 +910,15 @@ class Results(Generic[AnyModel]):
# Fully materialized. Just count the objects.
return len(self._objects)
elif self.query:
if self.query:
# A slow query. Fall back to testing every object.
count = 0
for obj in self:
count += 1
return count
else:
# A fast query. Just count the rows.
return self._row_count
# A fast query. Just count the rows.
return self._row_count
def __nonzero__(self) -> bool:
"""Does this result contain any objects?"""
@@ -1223,10 +1216,9 @@ class Database:
with self._shared_map_lock:
if thread_id in self._connections:
return self._connections[thread_id]
else:
conn = self._create_connection()
self._connections[thread_id] = conn
return conn
conn = self._create_connection()
self._connections[thread_id] = conn
return conn
def _create_connection(self) -> Connection:
"""Create a SQLite connection to the underlying database.

View File

@@ -161,9 +161,8 @@ class FieldQuery(Query, Generic[P]):
def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:
if self.fast:
return self.col_clause()
else:
# Matching a flexattr. This is a slow query.
return None, ()
# Matching a flexattr. This is a slow query.
return None, ()
@classmethod
def value_match(cls, pattern: P, value: Any):
@@ -477,28 +476,25 @@ class NumericQuery(FieldQuery[str]):
if self.point is not None:
return value == self.point
else:
if self.rangemin is not None and value < self.rangemin:
return False
if self.rangemax is not None and value > self.rangemax:
return False
return True
if self.rangemin is not None and value < self.rangemin:
return False
if self.rangemax is not None and value > self.rangemax:
return False
return True
def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
if self.point is not None:
return f"{self.field}=?", (self.point,)
else:
if self.rangemin is not None and self.rangemax is not None:
return (
f"{self.field} >= ? AND {self.field} <= ?",
(self.rangemin, self.rangemax),
)
elif self.rangemin is not None:
return f"{self.field} >= ?", (self.rangemin,)
elif self.rangemax is not None:
return f"{self.field} <= ?", (self.rangemax,)
else:
return "1", ()
if self.rangemin is not None and self.rangemax is not None:
return (
f"{self.field} >= ? AND {self.field} <= ?",
(self.rangemin, self.rangemax),
)
if self.rangemin is not None:
return f"{self.field} >= ?", (self.rangemin,)
if self.rangemax is not None:
return f"{self.field} <= ?", (self.rangemax,)
return "1", ()
class InQuery(Generic[AnySQLiteType], FieldQuery[Sequence[AnySQLiteType]]):
@@ -632,10 +628,9 @@ class NotQuery(Query):
clause, subvals = self.subquery.clause()
if clause:
return f"not ({clause})", subvals
else:
# If there is no clause, there is nothing to negate. All the logic
# is handled by match() for slow queries.
return clause, subvals
# If there is no clause, there is nothing to negate. All the logic
# is handled by match() for slow queries.
return clause, subvals
def match(self, obj: Model) -> bool:
return not self.subquery.match(obj)
@@ -681,10 +676,9 @@ def _parse_periods(pattern: str) -> tuple[Period | None, Period | None]:
if len(parts) == 1:
instant = Period.parse(parts[0])
return (instant, instant)
else:
start = Period.parse(parts[0])
end = Period.parse(parts[1])
return (start, end)
start = Period.parse(parts[0])
end = Period.parse(parts[1])
return (start, end)
class Period:
@@ -742,11 +736,11 @@ class Period:
def find_date_and_format(
string: str,
) -> tuple[None, None] | tuple[datetime, int]:
for ord, format in enumerate(cls.date_formats):
for format_option in format:
for ord_, format_ in enumerate(cls.date_formats):
for format_option in format_:
try:
date = datetime.strptime(string, format_option)
return date, ord
return date, ord_
except ValueError:
# Parsing failed.
pass
@@ -791,21 +785,19 @@ class Period:
date = self.date
if "year" == self.precision:
return date.replace(year=date.year + 1, month=1)
elif "month" == precision:
if "month" == precision:
if date.month < 12:
return date.replace(month=date.month + 1)
else:
return date.replace(year=date.year + 1, month=1)
elif "day" == precision:
return date.replace(year=date.year + 1, month=1)
if "day" == precision:
return date + timedelta(days=1)
elif "hour" == precision:
if "hour" == precision:
return date + timedelta(hours=1)
elif "minute" == precision:
if "minute" == precision:
return date + timedelta(minutes=1)
elif "second" == precision:
if "second" == precision:
return date + timedelta(seconds=1)
else:
raise ValueError(f"unhandled precision {precision}")
raise ValueError(f"unhandled precision {precision}")
class DateInterval:

View File

@@ -158,8 +158,7 @@ def construct_query_part(
# Apply negation.
if negate:
return query.NotQuery(out_query)
else:
return out_query
return out_query
# TYPING ERROR
@@ -218,13 +217,12 @@ def sort_from_strings(
"""Create a `Sort` from a list of sort criteria (strings)."""
if not sort_parts:
return sort.NullSort()
elif len(sort_parts) == 1:
if len(sort_parts) == 1:
return construct_sort_part(model_cls, sort_parts[0], case_insensitive)
else:
s = sort.MultipleSort()
for part in sort_parts:
s.add_sort(construct_sort_part(model_cls, part, case_insensitive))
return s
s = sort.MultipleSort()
for part in sort_parts:
s.add_sort(construct_sort_part(model_cls, part, case_insensitive))
return s
def parse_sorted_query(

View File

@@ -94,10 +94,9 @@ class Type(ABC, Generic[T, N]):
# `self.null` might be `None`
if value is None:
return ""
elif isinstance(value, bytes):
if isinstance(value, bytes):
return value.decode("utf-8", "ignore")
else:
return str(value)
return str(value)
def parse(self, string: str) -> T | N:
"""Parse a (possibly human-written) string and return the
@@ -116,10 +115,9 @@ class Type(ABC, Generic[T, N]):
# TYPING ERROR
if value is None:
return self.null
else:
# TODO This should eventually be replaced by
# `self.model_type(value)`
return cast(T, value)
# TODO This should eventually be replaced by
# `self.model_type(value)`
return cast(T, value)
def from_sql(self, sql_value: SQLiteType) -> T | N:
"""Receives the value stored in the SQL backend and return the
@@ -139,8 +137,7 @@ class Type(ABC, Generic[T, N]):
sql_value = bytes(sql_value).decode("utf-8", "ignore")
if isinstance(sql_value, str):
return self.parse(sql_value)
else:
return self.normalize(sql_value)
return self.normalize(sql_value)
def to_sql(self, model_value: Any) -> SQLiteType:
"""Convert a value as stored in the model object to a value used
@@ -272,8 +269,7 @@ class BaseString(Type[T, N]):
def normalize(self, value: Any) -> T | N:
if value is None:
return self.null
else:
return self.model_type(value)
return self.model_type(value)
class String(BaseString[str, Any]):
@@ -396,12 +392,11 @@ class BasePathType(Type[bytes, N]):
# Paths stored internally as encoded bytes.
return util.bytestring_path(value)
elif isinstance(value, BLOB_TYPE):
if isinstance(value, BLOB_TYPE):
# We unwrap buffers to bytes.
return bytes(value)
else:
return value
return value
def from_sql(self, sql_value):
return pathutils.expand_path_from_db(self.normalize(sql_value))
@@ -459,8 +454,7 @@ class MusicalKey(String):
def normalize(self, key):
if key is None:
return None
else:
return self.parse(key)
return self.parse(key)
class DurationType(Float):
@@ -471,8 +465,7 @@ class DurationType(Float):
def format(self, value):
if not beets.config["format_raw_length"].get(bool):
return human_seconds_short(value or 0.0)
else:
return value
return value
def parse(self, string):
try:

View File

@@ -42,8 +42,6 @@ log = logging.getLogger("beets")
class ImportAbortError(Exception):
"""Raised when the user aborts the tagging operation."""
pass
class ImportSession:
"""Controls an import action. Subclasses should implement methods to

View File

@@ -75,8 +75,6 @@ log = logging.getLogger("beets")
class ImportAbortError(Exception):
"""Raised when the user aborts the tagging operation."""
pass
class Action(Enum):
"""Enumeration of possible actions for an import task."""
@@ -237,7 +235,7 @@ class ImportTask(BaseImportTask):
if self.choice_flag in (Action.ASIS, Action.RETAG):
likelies, _ = util.get_most_common_tags(self.items)
return likelies
elif self.choice_flag is Action.APPLY and self.match:
if self.choice_flag is Action.APPLY and self.match:
return self.match.info.copy()
assert False
@@ -249,12 +247,11 @@ class ImportTask(BaseImportTask):
"""
if self.choice_flag in (Action.ASIS, Action.RETAG):
return self.items
elif self.choice_flag == Action.APPLY and isinstance(
if self.choice_flag == Action.APPLY and isinstance(
self.match, AlbumMatch
):
return self.match.items
else:
return []
return []
def apply_metadata(self) -> None:
"""Copy metadata from match info to the items."""
@@ -693,8 +690,9 @@ class SingletonImportTask(ImportTask):
assert self.choice_flag in (Action.ASIS, Action.RETAG, Action.APPLY)
if self.choice_flag in (Action.ASIS, Action.RETAG):
return dict(self.item)
elif self.choice_flag is Action.APPLY:
if self.choice_flag is Action.APPLY:
return self.match.info.copy()
return None
def imported_items(self):
return [self.item]
@@ -1065,8 +1063,7 @@ class ImportTaskFactory:
item = self.read_item(path)
if item:
return SingletonImportTask(self.toppath, item)
else:
return None
return None
def album(self, paths: Iterable[util.PathBytes], dirs=None):
"""Return a `ImportTask` with all media files from paths.
@@ -1092,8 +1089,7 @@ class ImportTaskFactory:
if len(items) > 0:
return ImportTask(self.toppath, dirs, items)
else:
return None
return None
def sentinel(self, paths: Iterable[util.PathBytes] | None = None):
"""Return a `SentinelImportTask` indicating the end of a
@@ -1115,7 +1111,7 @@ class ImportTaskFactory:
"Archive importing requires either "
"'copy' or 'move' to be enabled."
)
return
return None
log.debug("Extracting archive: {}", util.displayable_path(self.toppath))
archive_task = ArchiveImportTask(self.toppath)
@@ -1123,7 +1119,7 @@ class ImportTaskFactory:
archive_task.extract()
except Exception as exc:
log.error("extraction failed: {}", exc)
return
return None
# Now read albums from the extracted directory.
self.toppath = archive_task.toppath

View File

@@ -216,12 +216,11 @@ class FormattedItemMapping(dbcore.db.FormattedMapping):
"""
if self.for_path and key in self.album_keys:
return self._get_formatted(self.album, key)
elif key in self.model_keys:
if key in self.model_keys:
return self._get_formatted(self.model, key)
elif key in self.album_keys:
if key in self.album_keys:
return self._get_formatted(self.album, key)
else:
raise KeyError(key)
raise KeyError(key)
def __getitem__(self, key):
"""Get the value for a key.
@@ -237,7 +236,7 @@ class FormattedItemMapping(dbcore.db.FormattedMapping):
try:
if key == "artist" and not value:
return self._get("albumartist")
elif key == "albumartist" and not value:
if key == "albumartist" and not value:
return self._get("artist")
except KeyError:
pass
@@ -556,7 +555,7 @@ class Album(LibModel):
if oldart and samefile(path, oldart):
# Art already set.
return
elif samefile(path, artdest):
if samefile(path, artdest):
# Art already in place.
self.artpath = path
return
@@ -1339,8 +1338,7 @@ class DefaultTemplateFunctions:
if condition:
return trueval
else:
return falseval
return falseval
@staticmethod
def tmpl_asciify(s):
@@ -1555,5 +1553,4 @@ class DefaultTemplateFunctions:
"""
if field in self.item:
return trueval if trueval else self.item.formatted().get(field)
else:
return falseval
return falseval

View File

@@ -207,5 +207,4 @@ def getLogger(name: None = ...) -> RootLogger: ...
def getLogger(name=None) -> BeetsLogger | RootLogger: # noqa: N802
if name:
return my_manager.getLogger(name) # type: ignore[return-value]
else:
return Logger.root
return Logger.root

View File

@@ -256,7 +256,7 @@ class MetadataSourcePlugin(BeetsPlugin, metaclass=abc.ABCMeta):
single calls to album_for_id.
"""
return (self.album_for_id(id) for id in ids)
return (self.album_for_id(id_) for id_ in ids)
def tracks_for_ids(self, ids: Iterable[str]) -> Iterable[TrackInfo | None]:
"""Batch lookup of track metadata for a list of track IDs.
@@ -267,7 +267,7 @@ class MetadataSourcePlugin(BeetsPlugin, metaclass=abc.ABCMeta):
single calls to track_for_id.
"""
return (self.track_for_id(id) for id in ids)
return (self.track_for_id(id_) for id_ in ids)
def _extract_id(self, url: str) -> str | None:
"""Extract an ID from a URL for this metadata source plugin.

View File

@@ -384,15 +384,15 @@ class TestHelper(RunMixin, ConfigMixin):
"""Delete the temporary directory created by `create_temp_dir`."""
shutil.rmtree(self.temp_dir_path)
def touch(self, path, dir=None, content=""):
def touch(self, path, dir_=None, content=""):
"""Create a file at `path` with given content.
If `dir` is given, it is prepended to `path`. After that, if the
path is relative, it is resolved with respect to
`self.temp_dir`.
"""
if dir:
path = os.path.join(dir, path)
if dir_:
path = os.path.join(dir_, path)
if not os.path.isabs(path):
path = os.path.join(self.temp_dir, path)
@@ -481,8 +481,6 @@ class PluginTestCase(PluginMixin, BeetsTestCase):
DEPRECATED: Use PluginTestHelper instead.
"""
pass
class PluginTestHelper(PluginMixin, TestHelper):
"""Helper mixin for pytest-based plugin tests.
@@ -657,10 +655,9 @@ class ImportSessionFixture(ImportSession):
if choice == importer.Action.APPLY:
return task.candidates[0]
elif isinstance(choice, int):
if isinstance(choice, int):
return task.candidates[choice - 1]
else:
return choice
return choice
choose_item = choose_match
@@ -807,13 +804,13 @@ class AutotagStub:
)
def _make_album_match(self, artist, album, tracks, distance=0, missing=0):
id = f" {'M' * distance}" if distance else ""
id_ = f" {'M' * distance}" if distance else ""
if artist is None:
artist = "Various Artists"
else:
artist = f"{artist.replace('Tag', 'Applied')}{id}"
album = f"{album.replace('Tag', 'Applied')}{id}"
artist = f"{artist.replace('Tag', 'Applied')}{id_}"
album = f"{album.replace('Tag', 'Applied')}{id_}"
track_infos = []
for i in range(tracks - missing):
@@ -824,8 +821,8 @@ class AutotagStub:
album=album,
tracks=track_infos,
va=False,
album_id=f"albumid{id}",
artist_id=f"artistid{id}",
album_id=f"albumid{id_}",
artist_id=f"artistid{id_}",
albumtype="soundtrack",
data_source="match_source",
bandcamp_album_id="bc_url",

View File

@@ -139,9 +139,8 @@ def _bool_fallback(a, b):
if a is None:
assert isinstance(b, bool)
return b
else:
assert isinstance(a, bool)
return a
assert isinstance(a, bool)
return a
def should_write(write_opt=None):
@@ -354,8 +353,7 @@ def input_options(
low, high = numrange
if low <= resp <= high:
return resp
else:
resp = None
resp = None
# Try a normal letter input.
if resp:
@@ -397,7 +395,7 @@ def input_select_objects(prompt, objs, rep, prompt_all=None):
if choice == "y": # Yes.
return objs
elif choice == "s": # Select.
if choice == "s": # Select.
out = []
for obj in objs:
rep(obj)
@@ -413,8 +411,8 @@ def input_select_objects(prompt, objs, rep, prompt_all=None):
return out
return out
else: # No.
return []
# No.
return []
@cache
@@ -602,7 +600,7 @@ class Subcommand:
func: Callable[[library.Library, optparse.Values, list[str]], Any]
def __init__(self, name, parser=None, help="", aliases=(), hide=False):
def __init__(self, name, parser=None, help="", aliases=(), hide=False): # noqa: A002
"""Creates a new subcommand. name is the primary way to invoke
the subcommand; aliases are alternate names. parser is an
OptionParser responsible for parsing the subcommand's options.
@@ -947,6 +945,7 @@ def _raw_main(args: list[str] | None) -> None:
plugins.send("cli_exit", lib=lib)
lib._close()
return None
def main(args: list[str] | None = None) -> None:

View File

@@ -131,12 +131,11 @@ class ChangeRepresentation:
return (
f"* {track_media} {track_info.medium}: {track_info.disctitle}"
)
elif self.match.info.mediums > 1:
if self.match.info.mediums > 1:
return f"* {track_media} {track_info.medium}"
elif track_info.disctitle:
if track_info.disctitle:
return f"* {track_media}: {track_info.disctitle}"
else:
return ""
return ""
def format_index(self, track_info: TrackInfo | Item) -> str:
"""Return a string representing the track index of the given
@@ -154,10 +153,8 @@ class ChangeRepresentation:
if config["per_disc_numbering"]:
if mediums and mediums > 1:
return f"{medium}-{medium_index}"
else:
return str(medium_index if medium_index is not None else index)
else:
return str(index)
return str(medium_index if medium_index is not None else index)
return str(index)
def make_track_numbers(
self, item: Item, track_info: TrackInfo
@@ -191,11 +188,10 @@ class ChangeRepresentation:
# If there's no title, we use the filename. Don't colordiff.
cur_title = displayable_path(os.path.basename(item.path))
return cur_title, new_title, True
else:
# If there is a title, highlight differences.
cur_title = item.title.strip()
cur_col, new_col = colordiff(cur_title, new_title)
return cur_col, new_col, cur_title != new_title
# If there is a title, highlight differences.
cur_title = item.title.strip()
cur_col, new_col = colordiff(cur_title, new_title)
return cur_col, new_col, cur_title != new_title
@staticmethod
def make_track_lengths(

View File

@@ -48,7 +48,7 @@ class TerminalImportSession(importer.ImportSession):
if len(actions) == 1:
return actions[0]
elif len(actions) > 1:
if len(actions) > 1:
raise plugins.PluginConflictError(
"Only one handler for `import_task_before_choice` may return "
"an action."
@@ -60,7 +60,7 @@ class TerminalImportSession(importer.ImportSession):
match = task.candidates[0]
show_change(task.cur_artist, task.cur_album, match)
return match
elif action is not None:
if action is not None:
return action
# Loop until we have a choice.
@@ -87,11 +87,11 @@ class TerminalImportSession(importer.ImportSession):
# Plugin-provided choices. We invoke the associated callback
# function.
elif choice in choices:
if choice in choices:
post_choice = choice.callback(self, task)
if isinstance(post_choice, importer.Action):
return post_choice
elif isinstance(post_choice, Proposal):
if isinstance(post_choice, Proposal):
# Use the new candidates and continue around the loop.
task.candidates = post_choice.candidates
task.rec = post_choice.recommendation
@@ -117,7 +117,7 @@ class TerminalImportSession(importer.ImportSession):
match = candidates[0]
show_item_change(task.item, match)
return match
elif action is not None:
if action is not None:
return action
while True:
@@ -130,11 +130,11 @@ class TerminalImportSession(importer.ImportSession):
if choice in (importer.Action.SKIP, importer.Action.ASIS):
return choice
elif choice in choices:
if choice in choices:
post_choice = choice.callback(self, task)
if isinstance(post_choice, importer.Action):
return post_choice
elif isinstance(post_choice, Proposal):
if isinstance(post_choice, Proposal):
candidates = post_choice.candidates
rec = post_choice.recommendation
@@ -340,10 +340,9 @@ def _summary_judgment(rec: Recommendation) -> importer.Action | None:
if config["import"]["quiet"]:
if rec == Recommendation.strong:
return importer.Action.APPLY
else:
action = config["import"]["quiet_fallback"].as_choice(
{"skip": importer.Action.SKIP, "asis": importer.Action.ASIS}
)
action = config["import"]["quiet_fallback"].as_choice(
{"skip": importer.Action.SKIP, "asis": importer.Action.ASIS}
)
elif config["import"]["timid"]:
return None
elif rec == Recommendation.none:
@@ -412,8 +411,7 @@ def choose_candidate(
sel = ui.input_options(choice_opts)
if sel in choice_actions:
return choice_actions[sel]
else:
assert False
assert False
# Is the change good enough?
bypass_candidates = False
@@ -501,7 +499,7 @@ def choose_candidate(
)
if sel == "a":
return match
elif sel in choice_actions:
if sel in choice_actions:
return choice_actions[sel]
@@ -517,8 +515,7 @@ def manual_search(session, task):
if task.is_album:
_, _, prop = tag_album(task.items, artist, name)
return prop
else:
return tag_item(task.item, artist, name)
return tag_item(task.item, artist, name)
def manual_id(session, task):
@@ -532,8 +529,7 @@ def manual_id(session, task):
if task.is_album:
_, _, prop = tag_album(task.items, search_ids=search_id.split())
return prop
else:
return tag_item(task.item, search_ids=search_id.split())
return tag_item(task.item, search_ids=search_id.split())
def abort_action(session, task):

View File

@@ -23,7 +23,7 @@ def do_query(lib, query, album, also_items=True):
if album and not albums:
raise UserError("No matching albums found.")
elif not album and not items:
if not album and not items:
raise UserError("No matching items found.")
return items, albums

View File

@@ -105,12 +105,11 @@ class HumanReadableError(Exception):
"""Get the reason as a string."""
if isinstance(self.reason, str):
return self.reason
elif isinstance(self.reason, bytes):
if isinstance(self.reason, bytes):
return self.reason.decode("utf-8", "ignore")
elif hasattr(self.reason, "strerror"): # i.e., EnvironmentError
if hasattr(self.reason, "strerror"): # i.e., EnvironmentError
return self.reason.strerror
else:
return f'"{self.reason}"'
return f'"{self.reason}"'
def get_message(self):
"""Create the human-readable description of the error, sans
@@ -412,9 +411,9 @@ def displayable_path(
if isinstance(path, (list, tuple)):
return separator.join(displayable_path(p) for p in path)
elif isinstance(path, str):
if isinstance(path, str):
return path
elif not isinstance(path, bytes):
if not isinstance(path, bytes):
# A non-string object: just get its unicode representation.
return str(path)
@@ -602,10 +601,7 @@ def hardlink(path: bytes, dest: bytes, replace: bool = False):
(path, dest),
traceback.format_exc(),
)
else:
raise FilesystemError(
exc, "link", (path, dest), traceback.format_exc()
)
raise FilesystemError(exc, "link", (path, dest), traceback.format_exc())
def reflink(
@@ -620,7 +616,7 @@ def reflink(
Otherwise, errors are re-raised as FilesystemError with an explanation.
"""
if samefile(path, dest):
return
return None
if os.path.exists(syspath(dest)) and not replace:
raise FilesystemError("target exists", "rename", (path, dest))
@@ -797,12 +793,11 @@ def as_string(value: Any) -> str:
"""
if value is None:
return ""
elif isinstance(value, memoryview):
if isinstance(value, memoryview):
return bytes(value).decode("utf-8", "ignore")
elif isinstance(value, bytes):
if isinstance(value, bytes):
return value.decode("utf-8", "ignore")
else:
return str(value)
return str(value)
def plurality(objs: Iterable[T]) -> tuple[T, int]:
@@ -919,8 +914,7 @@ def get_max_filename_length() -> int:
except OSError:
return limit
return min(res[9], limit)
else:
return limit
return limit
def open_anything() -> str:

View File

@@ -80,7 +80,6 @@ class LocalBackend(ABC):
"""Return the backend version if its dependencies are satisfied or
raise `LocalBackendNotAvailableError`.
"""
pass
@classmethod
def available(cls) -> bool:
@@ -105,12 +104,10 @@ class LocalBackend(ABC):
On error, logs a warning and returns `path_in`.
"""
pass
@abstractmethod
def get_size(self, path_in: bytes) -> tuple[int, int] | None:
"""Return the (width, height) of the image or None if unavailable."""
pass
@abstractmethod
def deinterlace(
@@ -120,12 +117,10 @@ class LocalBackend(ABC):
On error, logs a warning and returns `path_in`.
"""
pass
@abstractmethod
def get_format(self, path_in: bytes) -> str | None:
"""Return the image format (e.g., 'PNG') or None if undetectable."""
pass
@abstractmethod
def convert_format(
@@ -135,7 +130,6 @@ class LocalBackend(ABC):
On error, logs a warning and returns `source`.
"""
pass
@property
def can_compare(self) -> bool:
@@ -204,8 +198,7 @@ class IMBackend(LocalBackend):
# cls._version is never None here, but mypy doesn't get that
if cls._version is _NOT_AVAILABLE or cls._version is None:
raise LocalBackendNotAvailableError()
else:
return cls._version
return cls._version
convert_cmd: list[str]
identify_cmd: list[str]
@@ -564,8 +557,7 @@ class PILBackend(LocalBackend):
)
return path_out
else:
return path_out
return path_out
except OSError:
log.error(
"PIL cannot create thumbnail for '{}'",
@@ -699,8 +691,7 @@ class ArtResizer:
def method(self) -> str:
if self.local_method is not None:
return self.local_method.NAME
else:
return "WEBPROXY"
return "WEBPROXY"
def resize(
self,
@@ -723,9 +714,8 @@ class ArtResizer:
quality=quality,
max_filesize=max_filesize,
)
else:
# Handled by `proxy_url` already.
return path_in
# Handled by `proxy_url` already.
return path_in
def deinterlace(
self, path_in: bytes, path_out: bytes | None = None
@@ -736,9 +726,8 @@ class ArtResizer:
"""
if self.local_method is not None:
return self.local_method.deinterlace(path_in, path_out)
else:
# FIXME: Should probably issue a warning?
return path_in
# FIXME: Should probably issue a warning?
return path_in
def proxy_url(self, maxwidth: int, url: str, quality: int = 0) -> str:
"""Modifies an image URL according the method, returning a new
@@ -748,8 +737,7 @@ class ArtResizer:
if self.local:
# Going to be handled by `resize()`.
return url
else:
return resize_url(url, maxwidth, quality)
return resize_url(url, maxwidth, quality)
@property
def local(self) -> bool:
@@ -766,10 +754,9 @@ class ArtResizer:
"""
if self.local_method is not None:
return self.local_method.get_size(path_in)
else:
raise RuntimeError(
"image cannot be obtained without artresizer backend"
)
raise RuntimeError(
"image cannot be obtained without artresizer backend"
)
def get_format(self, path_in: bytes) -> str | None:
"""Returns the format of the image as a string.
@@ -778,9 +765,8 @@ class ArtResizer:
"""
if self.local_method is not None:
return self.local_method.get_format(path_in)
else:
# FIXME: Should probably issue a warning?
return None
# FIXME: Should probably issue a warning?
return None
def reformat(
self, path_in: bytes, new_format: str, deinterlaced: bool = True
@@ -820,8 +806,7 @@ class ArtResizer:
if self.local_method is not None:
return self.local_method.can_compare
else:
return False
return False
def compare(
self, im1: bytes, im2: bytes, compare_threshold: float
@@ -832,9 +817,8 @@ class ArtResizer:
"""
if self.local_method is not None:
return self.local_method.compare(im1, im2, compare_threshold)
else:
# FIXME: Should probably issue a warning?
return None
# FIXME: Should probably issue a warning?
return None
@property
def can_write_metadata(self) -> bool:
@@ -842,8 +826,7 @@ class ArtResizer:
if self.local_method is not None:
return self.local_method.can_write_metadata
else:
return False
return False
def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> None:
"""Write key-value metadata to the image file.

View File

@@ -24,8 +24,6 @@ class Event:
and communicate with the scheduler.
"""
pass
class WaitableEvent(Event):
"""A waitable event is one encapsulating an action that can be
@@ -45,7 +43,6 @@ class WaitableEvent(Event):
"""Called when an associated file descriptor becomes ready
(i.e., is returned from a select() call).
"""
pass
class ValueEvent(Event):
@@ -455,8 +452,7 @@ class Connection:
out = self._buf[:size]
self._buf = self._buf[size:]
return ValueEvent(out)
else:
return ReceiveEvent(self, size)
return ReceiveEvent(self, size)
def send(self, data):
"""Sends data on the socket, returning the number of bytes
@@ -541,8 +537,7 @@ class SendEvent(WaitableEvent):
def fire(self):
if self.sendall:
return self.conn.sock.sendall(self.data)
else:
return self.conn.sock.send(self.data)
return self.conn.sock.send(self.data)
# Public interface for threads; each returns an event object that
@@ -595,8 +590,7 @@ def read(fd, bufsize=None):
return DelegationEvent(reader())
else:
return ReadEvent(fd, bufsize)
return ReadEvent(fd, bufsize)
def write(fd, data):

View File

@@ -140,9 +140,8 @@ class Symbol:
if self.ident in env.values:
# Substitute for a value.
return env.values[self.ident]
else:
# Keep original text.
return self.original
# Keep original text.
return self.original
def translate(self):
"""Compile the variable lookup."""
@@ -175,8 +174,7 @@ class Call:
# the exception will help debug.
return f"<{exc}>"
return str(out)
else:
return self.original
return self.original
def translate(self):
"""Compile the function call."""

View File

@@ -22,8 +22,6 @@ from beets.util import FilesystemError, mkdirall, normpath, syspath
class EmptyPlaylistError(Exception):
"""Raised when a playlist file without media files is saved or loaded."""
pass
class M3UFile:
"""Reads and writes m3u or m3u8 playlist files."""

View File

@@ -59,7 +59,7 @@ def _invalidate_queue(q, val=None, sync=True):
required (because it's not reentrant!).
"""
def _qsize(len=len):
def _qsize(len=len): # noqa: A002
return 1
def _put(item):
@@ -224,10 +224,9 @@ def _allmsgs(obj):
"""
if isinstance(obj, MultiMessage):
return obj.messages
elif obj == BUBBLE:
if obj == BUBBLE:
return []
else:
return [obj]
return [obj]
class PipelineThread(Thread):

View File

@@ -39,7 +39,7 @@ def get_art(log, item):
mf = mediafile.MediaFile(syspath(item.path))
except mediafile.UnreadableFileError as exc:
log.warning("Could not extract art from {.filepath}: {}", item, exc)
return
return None
return mf.art
@@ -65,7 +65,7 @@ def embed_item(
if is_similar is None:
log.warning("Error while checking art similarity; skipping.")
return
elif not is_similar:
if not is_similar:
log.info("Image not similar; skipping.")
return
@@ -143,10 +143,9 @@ def resize_image(log, imagepath, maxwidth, quality):
maxwidth,
quality,
)
imagepath = ArtResizer.shared.resize(
return ArtResizer.shared.resize(
maxwidth, syspath(imagepath), quality=quality
)
return imagepath
def check_art_similarity(
@@ -176,13 +175,13 @@ def extract(log, outpath, item):
outpath = bytestring_path(outpath)
if not art:
log.info("No album art present in {}, skipping.", item)
return
return None
# Add an extension to the filename.
ext = mediafile.image_extension(art)
if not ext:
log.warning("Unknown image type in {.filepath}.", item)
return
return None
outpath += bytestring_path(f".{ext}")
log.info(
@@ -198,6 +197,7 @@ def extract_first(log, outpath, items):
real_path = extract(log, outpath, item)
if real_path:
return real_path
return None
def clear_item(item, log):

View File

@@ -97,7 +97,7 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin):
"AcousticBrainz server base URL must start "
"with an HTTP scheme"
)
elif base_url[-1] != "/":
if base_url[-1] != "/":
base_url = f"{base_url}/"
self.url = f"{base_url}{{mbid}}/low-level"
@@ -134,11 +134,10 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin):
"accepts new submissions. See the base_url configuration "
"option."
)
else:
# Get items from arguments
items = lib.items(args)
self.opts = opts
util.par_map(self.analyze_submit, items)
# Get items from arguments
items = lib.items(args)
self.opts = opts
util.par_map(self.analyze_submit, items)
def analyze_submit(self, item):
analysis = self._get_analysis(item)

View File

@@ -98,7 +98,7 @@ class AcousticPlugin(plugins.BeetsPlugin):
"AcousticBrainz server base URL must start "
"with an HTTP scheme"
)
elif self.base_url[-1] != "/":
if self.base_url[-1] != "/":
self.base_url = f"{self.base_url}/"
if self.config["auto"]:

View File

@@ -65,9 +65,9 @@ class AlbumTypesPlugin(BeetsPlugin):
res = ""
albumtypes = item.albumtypes
is_va = item.mb_albumartistid == VARIOUS_ARTISTS_ID
for type in types:
if type[0] in albumtypes and type[1]:
if not is_va or (type[0] not in ignore_va and is_va):
res += f"{bracket_l}{type[1]}{bracket_r}"
for type_ in types:
if type_[0] in albumtypes and type_[1]:
if not is_va or (type_[0] not in ignore_va and is_va):
res += f"{bracket_l}{type_[1]}{bracket_r}"
return res

View File

@@ -664,8 +664,7 @@ class ImageDocument(AURADocument):
# Check the image actually exists
if os.path.isfile(img_path):
return img_path
else:
return None
return None
@staticmethod
def get_resource_object(lib: Library, image_id):

View File

@@ -108,6 +108,7 @@ class BadFiles(BeetsPlugin):
return self.check_mp3val
if ext == "flac":
return self.check_flac
return None
def check_item(self, item):
# First, check whether the path exists. If not, the user
@@ -226,7 +227,7 @@ class BadFiles(BeetsPlugin):
# Errors always take precedence over warnings.
if found_error and error_action != "ask":
return self.handle_import_action(error_action, "error")
elif found_warning and warning_action != "ask":
if found_warning and warning_action != "ask":
return self.handle_import_action(warning_action, "warning")
# Defer the quiet check to after automatic import action options are handled
@@ -240,12 +241,12 @@ class BadFiles(BeetsPlugin):
if sel == "s":
return importer.Action.SKIP
elif sel == "c":
if sel == "c":
return None
elif sel == "b":
if sel == "b":
raise importer.ImportAbortError()
else:
raise Exception(f"Unexpected selection: {sel}")
raise Exception(f"Unexpected selection: {sel}")
return None
def command(self, lib, opts, args):
# Get items from arguments

View File

@@ -294,8 +294,8 @@ class BeatportTrack(BeatportObject):
self.length = timedelta(milliseconds=data.get("lengthMs", 0) or 0)
if not self.length:
try:
min, sec = data.get("length", "0:0").split(":")
self.length = timedelta(minutes=int(min), seconds=int(sec))
min_, sec = data.get("length", "0:0").split(":")
self.length = timedelta(minutes=int(min_), seconds=int(sec))
except ValueError:
pass
if "slug" in data:
@@ -509,5 +509,4 @@ class BeatportPlugin(MetadataSourcePlugin):
def _get_tracks(self, query):
"""Returns a list of TrackInfo objects for a Beatport query."""
bp_tracks = self.client.search(query, release_type="track")
tracks = [self._get_track_info(x) for x in bp_tracks]
return tracks
return [self._get_track_info(x) for x in bp_tracks]

View File

@@ -165,11 +165,10 @@ def cast_arg(t, val):
"""
if t == "intbool":
return cast_arg(bool, cast_arg(int, val))
else:
try:
return t(val)
except ValueError:
raise ArgumentTypeError()
try:
return t(val)
except ValueError:
raise ArgumentTypeError()
class BPDCloseError(Exception):
@@ -346,7 +345,6 @@ class BaseServer:
def cmd_ping(self, conn):
"""Succeeds."""
pass
def cmd_idle(self, conn, *subsystems):
subsystems = subsystems or SUBSYSTEMS
@@ -604,7 +602,6 @@ class BaseServer:
def cmd_urlhandlers(self, conn):
"""Indicates supported URL schemes. None by default."""
pass
def cmd_playlistinfo(self, conn, index=None):
"""Gives metadata information about the entire playlist or a
@@ -669,10 +666,9 @@ class BaseServer:
self.current_index = -1
return self.cmd_play(conn)
return self.cmd_stop(conn)
elif self.single and not self.repeat:
if self.single and not self.repeat:
return self.cmd_stop(conn)
else:
return self.cmd_play(conn)
return self.cmd_play(conn)
def cmd_previous(self, conn):
"""Step back to the last song."""
@@ -714,6 +710,7 @@ class BaseServer:
self.paused = False
self._send_event("player")
return None
def cmd_playid(self, conn, track_id=0):
track_id = cast_arg(int, track_id)
@@ -1410,8 +1407,8 @@ class Server(BaseServer):
_, key = self._tagtype_lookup(tag)
queries.append(Item.field_query(key, value, query_type))
return dbcore.query.AndQuery(queries)
else: # No key-value pairs.
return dbcore.query.TrueQuery()
# No key-value pairs.
return dbcore.query.TrueQuery()
def cmd_search(self, conn, *kv):
"""Perform a substring match for items."""
@@ -1528,8 +1525,7 @@ class Server(BaseServer):
output_id = cast_arg(int, output_id)
if output_id == 0:
raise BPDError(ERROR_ARG, "cannot disable this output")
else:
raise ArgumentIndexError()
raise ArgumentIndexError()
# Playback control. The functions below hook into the
# half-implementations provided by the base class. Together, they're

View File

@@ -200,8 +200,7 @@ class GstPlayer:
# reason, we cache recent.
if self.playing and self.cached_time:
return self.cached_time
else:
return (0, 0)
return (0, 0)
def seek(self, position):
"""Seeks to position (in seconds)."""

View File

@@ -40,8 +40,7 @@ def bpm(max_strokes):
# Return average BPM
# bpm = (max_strokes-1) / sum(dt) * 60
ave = sum([1.0 / dti * 60 for dti in dt]) / len(dt)
return ave
return sum([1.0 / dti * 60 for dti in dt]) / len(dt)
class BPMPlugin(BeetsPlugin):

View File

@@ -216,14 +216,13 @@ class BucketPlugin(plugins.BeetsPlugin):
if ys["from"] <= int(year) <= ys["to"]:
if "str" in ys:
return ys["str"]
else:
return format_span(
self.ys_repr_mode["fmt"],
ys["from"],
ys["to"],
self.ys_repr_mode["fromnchars"],
self.ys_repr_mode["tonchars"],
)
return format_span(
self.ys_repr_mode["fmt"],
ys["from"],
ys["to"],
self.ys_repr_mode["fromnchars"],
self.ys_repr_mode["tonchars"],
)
return year
def find_bucket_alpha(self, s):

View File

@@ -104,7 +104,7 @@ def acoustid_match(log, path):
util.displayable_path(repr(path)),
exc,
)
return None
return
fp = fp.decode()
_fingerprints[path] = fp
try:
@@ -117,23 +117,23 @@ def acoustid_match(log, path):
util.displayable_path(repr(path)),
exc,
)
return None
return
log.debug("chroma: fingerprinted {}", util.displayable_path(repr(path)))
# Ensure the response is usable and parse it.
if res["status"] != "ok" or not res.get("results"):
log.debug("no match found")
return None
return
result = res["results"][0] # Best match.
if result["score"] < SCORE_THRESH:
log.debug("no results above threshold")
return None
return
_acoustids[path] = result["id"]
# Get recording and releases from the result
if not result.get("recordings"):
log.debug("no recordings found")
return None
return
recording_ids = []
releases = []
for recording in result["recordings"]:
@@ -466,6 +466,7 @@ def fingerprint_item(log, item, write=False, quiet=False):
return item.acoustid_fingerprint
except acoustid.FingerprintGenerationError as exc:
log.info("fingerprint generation failed: {}", exc)
return None
# Classes for search.

View File

@@ -372,8 +372,7 @@ class ConvertPlugin(BeetsPlugin):
if no_convert_query:
query, _ = parse_query_string(no_convert_query, Item)
return query.match(item)
else:
return False
return False
def should_transcode(self, item: Item) -> bool:
"""Determine whether the item should be transcoded as part of

View File

@@ -30,9 +30,9 @@ from string import ascii_lowercase
from typing import TYPE_CHECKING
import confuse
import requests
from discogs_client import Client, Master, Release
from discogs_client.exceptions import DiscogsAPIError
from requests.exceptions import ConnectionError
import beets
import beets.ui
@@ -58,7 +58,7 @@ API_SECRET = "plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy"
# Exceptions that discogs_client should really handle but does not.
CONNECTION_ERRORS = (
ConnectionError,
requests.exceptions.ConnectionError,
socket.error,
http.client.HTTPException,
ValueError, # JSON decoding raises a ValueError.

View File

@@ -117,8 +117,7 @@ def flatten(obj, fields):
# Possibly filter field names.
if fields:
return {k: v for k, v in d.items() if k in fields}
else:
return d
return d
def apply_(obj, data):
@@ -259,8 +258,7 @@ class EditPlugin(plugins.BeetsPlugin):
ui.print_(f"Could not read data: {e}")
if ui.input_yn("Edit again to fix? (Y/n)", True):
continue
else:
return False
return False
# Show the changes.
# If the objects are not on the DB yet, we need a copy of their
@@ -281,10 +279,10 @@ class EditPlugin(plugins.BeetsPlugin):
)
if choice == "a": # Apply.
return True
elif choice == "c": # Cancel.
if choice == "c": # Cancel.
self.apply_data(objs, new_data, old_data)
return False
elif choice == "e": # Keep editing.
if choice == "e": # Keep editing.
self.apply_data(objs, new_data, old_data)
continue
@@ -373,8 +371,7 @@ class EditPlugin(plugins.BeetsPlugin):
# Return Action.RETAG, which makes the importer write the tags
# to the files if needed without re-applying metadata.
return Action.RETAG
else:
return None
return None
def importer_edit_candidate(self, session, task):
"""Callback for invoking the functionality during an interactive

View File

@@ -32,7 +32,7 @@ def api_url(host, port, endpoint):
"""
# check if http or https is defined as host and create hostname
hostname_list = [host]
if host.startswith("http://") or host.startswith("https://"):
if host.startswith(("http://", "https://")):
hostname = "".join(hostname_list)
else:
hostname_list.insert(0, "http://")
@@ -128,9 +128,7 @@ def get_user(host, port, username):
"""
url = api_url(host, port, "/Users/Public")
r = requests.get(url, timeout=10)
user = [i for i in r.json() if i["Name"] == username]
return user
return [i for i in r.json() if i["Name"] == username]
class EmbyUpdate(BeetsPlugin):

View File

@@ -128,8 +128,7 @@ class ExportPlugin(BeetsPlugin):
format_options = self.config[file_format]["formatting"].get(dict)
export_format = ExportFormat.factory(
file_type=file_format,
**{"file_path": file_path, "file_mode": file_mode},
file_type=file_format, file_path=file_path, file_mode=file_mode
)
if opts.library or opts.album:
@@ -180,12 +179,11 @@ class ExportFormat:
def factory(cls, file_type, **kwargs):
if file_type in ["json", "jsonlines"]:
return JsonFormat(**kwargs)
elif file_type == "csv":
if file_type == "csv":
return CSVFormat(**kwargs)
elif file_type == "xml":
if file_type == "xml":
return XMLFormat(**kwargs)
else:
raise NotImplementedError()
raise NotImplementedError()
def export(self, data, **kwargs):
raise NotImplementedError()

View File

@@ -414,7 +414,6 @@ class ArtSource(RequestMixin, ABC):
After calling this, `Candidate.path` is set to the image path if
successful, or to `None` otherwise.
"""
pass
def cleanup(self, candidate: Candidate) -> None:
pass

View File

@@ -73,6 +73,5 @@ class FileFilterPlugin(BeetsPlugin):
if "singletons" not in import_config or not import_config["singletons"]:
# Album
return self.path_album_regex.match(full_path) is not None
else:
# Singleton
return self.path_singleton_regex.match(full_path) is not None
# Singleton
return self.path_singleton_regex.match(full_path) is not None

View File

@@ -179,7 +179,7 @@ def get_set_of_values_for_field(lib, fields):
def get_basic_beet_options():
word = (
return (
BL_NEED2.format("-l format-item", "-f -d 'print with custom format'")
+ BL_NEED2.format("-l format-album", "-f -d 'print with custom format'")
+ BL_NEED2.format(
@@ -198,7 +198,6 @@ def get_basic_beet_options():
"-s h -l help", "-f -d 'print this help message and exit'"
)
)
return word
def get_subcommands(cmd_name_and_help, nobasicfields, extravalues):

View File

@@ -84,9 +84,8 @@ def split_on_feat(
if len(parts) == 1:
return parts[0], None
else:
assert len(parts) == 2 # help mypy out
return parts
assert len(parts) == 2 # help mypy out
return parts
def contains_feat(title: str, custom_words: list[str] | None = None) -> bool:
@@ -123,12 +122,9 @@ def find_feat_part(
# Otherwise, if there's nothing on the right-hand side,
# look for a featuring artist on the left-hand side.
else:
lhs, _ = split_on_feat(
albumartist_split[0], custom_words=custom_words
)
if lhs:
return lhs
lhs, _ = split_on_feat(albumartist_split[0], custom_words=custom_words)
if lhs:
return lhs
# Fall back to conservative handling of the track artist without relying
# on albumartist, which covers compilations using a 'Various Artists'

View File

@@ -29,8 +29,7 @@ def summary(task):
"""
if task.is_album:
return f"{task.cur_artist} - {task.cur_album}"
else:
return f"{task.item.artist} - {task.item.title}"
return f"{task.item.artist} - {task.item.title}"
class IHatePlugin(BeetsPlugin):

View File

@@ -34,12 +34,11 @@ def _build_m3u_session_filename(basename):
basename and file ending."""
date = datetime.datetime.now().strftime("%Y%m%d_%Hh%M")
basename = re.sub(r"(\.m3u|\.M3U)", "", basename)
path = normpath(
return normpath(
os.path.join(
config["importfeeds"]["dir"].as_filename(), f"{basename}_{date}.m3u"
)
)
return path
def _build_m3u_filename(basename):
@@ -47,12 +46,11 @@ def _build_m3u_filename(basename):
date."""
basename = re.sub(r"[\s,/\\'\"]", "_", basename)
date = datetime.datetime.now().strftime("%Y%m%d_%Hh%M")
path = normpath(
return normpath(
os.path.join(
config["importfeeds"]["dir"].as_filename(), f"{date}_{basename}.m3u"
)
)
return path
def _write_m3u(m3u_path, items_paths):

View File

@@ -90,7 +90,7 @@ class InlinePlugin(BeetsPlugin):
"syntax error in inline field definition:\n{}",
traceback.format_exc(),
)
return
return None
else:
is_expr = False
else:
@@ -118,18 +118,18 @@ class InlinePlugin(BeetsPlugin):
raise InlineError(python_code, exc)
return _expr_func
else:
# For function bodies, invoke the function with values as global
# variables.
def _func_func(obj):
old_globals = dict(func.__globals__)
func.__globals__.update(_dict_for(obj))
try:
return func(obj)
except Exception as exc:
raise InlineError(python_code, exc)
finally:
func.__globals__.clear()
func.__globals__.update(old_globals)
return _func_func
# For function bodies, invoke the function with values as global
# variables.
def _func_func(obj):
old_globals = dict(func.__globals__)
func.__globals__.update(_dict_for(obj))
try:
return func(obj)
except Exception as exc:
raise InlineError(python_code, exc)
finally:
func.__globals__.clear()
func.__globals__.update(old_globals)
return _func_func

View File

@@ -188,6 +188,7 @@ class IPFSPlugin(BeetsPlugin):
# prefix=True). However, that should be fine since the hash will not
# exceed MAX_PATH.
shutil.rmtree(util.syspath(_hash, prefix=False))
return None
def ipfs_publish(self, lib):
with tempfile.NamedTemporaryFile() as tmp:
@@ -204,6 +205,7 @@ class IPFSPlugin(BeetsPlugin):
self._log.error(msg)
return False
self._log.info("hash of library: {}", output)
return None
def ipfs_import(self, lib, args):
_hash = args[0]
@@ -243,6 +245,7 @@ class IPFSPlugin(BeetsPlugin):
added_album = jlib.add_album(new_album)
added_album.ipfs = album.ipfs
added_album.store()
return None
def already_added(self, check, jlib):
for jalbum in jlib.albums():
@@ -263,8 +266,7 @@ class IPFSPlugin(BeetsPlugin):
def query(self, lib, args):
rlib = self.get_remote_lib(lib)
albums = rlib.albums(args)
return albums
return rlib.albums(args)
def get_remote_lib(self, lib):
lib_root = os.path.dirname(lib.path)
@@ -308,3 +310,4 @@ class IPFSPlugin(BeetsPlugin):
new_album = tmplib.add_album(items)
new_album.ipfs = album.ipfs
new_album.store(inherit=False)
return None

View File

@@ -39,12 +39,10 @@ def update_kodi(host, port, user, password):
# Create the payload. Id seems to be mandatory.
payload = {"jsonrpc": "2.0", "method": "AudioLibrary.Scan", "id": 1}
r = requests.post(
return requests.post(
url, auth=(user, password), json=payload, headers=headers, timeout=10
)
return r
class KodiUpdate(BeetsPlugin):
def __init__(self):

View File

@@ -365,8 +365,7 @@ class LastGenrePlugin(plugins.BeetsPlugin):
"""Format to title case if configured."""
if self.config["title_case"]:
return [tag.title() for tag in tags]
else:
return tags
return tags
def _artist_for_filter(self, obj: LibModel) -> str | None:
"""Return the representative artist for genre resolution and filtering."""

View File

@@ -132,7 +132,7 @@ def import_lastfm(lib, log):
f"/{page_total}" if page_total > 1 else "",
)
for retry in range(0, retry_limit):
for retry in range(retry_limit):
tracks, page_total = fetch_tracks(user, page_current + 1, per_page)
if page_total < 1:
# It means nothing to us!
@@ -143,22 +143,21 @@ def import_lastfm(lib, log):
found_total += found
unknown_total += unknown
break
log.error("ERROR: unable to read page #{}", page_current + 1)
if retry < retry_limit:
log.info(
"Retrying page #{}... ({}/{} retry)",
page_current + 1,
retry + 1,
retry_limit,
)
else:
log.error("ERROR: unable to read page #{}", page_current + 1)
if retry < retry_limit:
log.info(
"Retrying page #{}... ({}/{} retry)",
page_current + 1,
retry + 1,
retry_limit,
)
else:
log.error(
"FAIL: unable to fetch page #{}, ",
"tried {} times",
page_current,
retry + 1,
)
log.error(
"FAIL: unable to fetch page #{}, ",
"tried {} times",
page_current,
retry + 1,
)
page_current += 1
log.info("... done!")

View File

@@ -351,9 +351,9 @@ class ListenBrainzPlugin(MusicBrainzAPIMixin, BeetsPlugin):
else:
continue
identifier = playlist_info.get("identifier")
id = identifier.split("/")[-1]
id_ = identifier.split("/")[-1]
listenbrainz_playlists.append(
{"type": playlist_type, "date": date, "identifier": id}
{"type": playlist_type, "date": date, "identifier": id_}
)
listenbrainz_playlists = sorted(
listenbrainz_playlists, key=lambda x: x["type"]

View File

@@ -939,9 +939,9 @@ class RestFiles:
@cached_property
def artists_dir(self) -> Path:
dir = self.directory / "artists"
dir.mkdir(parents=True, exist_ok=True)
return dir
dir_ = self.directory / "artists"
dir_.mkdir(parents=True, exist_ok=True)
return dir_
def write_indexes(self) -> None:
"""Write conf.py and index.rst files necessary for Sphinx

View File

@@ -150,15 +150,13 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
)
self._add_custom_tags(official_release, pseudo_release)
return official_release
else:
return PseudoAlbumInfo(
pseudo_release=_merge_pseudo_and_actual_album(
pseudo_release, official_release
),
official_release=official_release,
)
else:
return official_release
return PseudoAlbumInfo(
pseudo_release=_merge_pseudo_and_actual_album(
pseudo_release, official_release
),
official_release=official_release,
)
return official_release
def _intercept_mb_release(self, data: Release) -> list[str]:
album_id = data["id"] if "id" in data else None
@@ -177,10 +175,9 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
) -> bool:
if len(self._scripts) == 0:
return False
elif script := release.get("text_representation", {}).get("script"):
if script := release.get("text_representation", {}).get("script"):
return script in self._scripts
else:
return False
return False
def _wanted_pseudo_release_id(
self, album_id: str, relation: ReleaseRelation
@@ -201,8 +198,7 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
album_id,
)
return release["id"]
else:
return None
return None
def _replace_artist_with_alias(
self, raw_pseudo_release: Release, pseudo_release: AlbumInfo
@@ -316,9 +312,8 @@ class PseudoAlbumInfo(AlbumInfo):
if official_dist < pseudo_dist:
self.use_official_as_ref()
return "official"
else:
self.use_pseudo_as_ref()
return "pseudo"
self.use_pseudo_as_ref()
return "pseudo"
def _compute_distance(self, items: Sequence[Item]) -> Distance:
mapping, _, _ = assign_items(items, self.tracks)
@@ -334,8 +329,7 @@ class PseudoAlbumInfo(AlbumInfo):
# ensure we don't duplicate an official release's id, always return pseudo's
if self.__dict__["_pseudo_source"] or attr == "album_id":
return super().__getattr__(attr)
else:
return self.__dict__["_official_release"].__getattr__(attr)
return self.__dict__["_official_release"].__getattr__(attr)
def __deepcopy__(self, memo):
cls = self.__class__

View File

@@ -62,6 +62,7 @@ class MBSubmitPlugin(BeetsPlugin):
PromptChoice("p", "Print tracks", self.print_tracks),
PromptChoice("o", "Open files with Picard", self.picard),
]
return None
def picard(self, session, task):
paths = []

View File

@@ -74,41 +74,39 @@ def _item(track_info, album_info, album_id):
a = album_info
return Item(
**{
"album_id": album_id,
"album": a.album,
"albumartist": a.artist,
"albumartist_credit": a.artist_credit,
"albumartist_sort": a.artist_sort,
"albumdisambig": a.albumdisambig,
"albumstatus": a.albumstatus,
"albumtype": a.albumtype,
"artist": t.artist,
"artist_credit": t.artist_credit,
"artist_sort": t.artist_sort,
"asin": a.asin,
"catalognum": a.catalognum,
"comp": a.va,
"country": a.country,
"day": a.day,
"disc": t.medium,
"disctitle": t.disctitle,
"disctotal": a.mediums,
"label": a.label,
"language": a.language,
"length": t.length,
"mb_albumid": a.album_id,
"mb_artistid": t.artist_id,
"mb_releasegroupid": a.releasegroup_id,
"mb_trackid": t.track_id,
"media": t.media,
"month": a.month,
"script": a.script,
"title": t.title,
"track": t.index,
"tracktotal": len(a.tracks),
"year": a.year,
}
album_id=album_id,
album=a.album,
albumartist=a.artist,
albumartist_credit=a.artist_credit,
albumartist_sort=a.artist_sort,
albumdisambig=a.albumdisambig,
albumstatus=a.albumstatus,
albumtype=a.albumtype,
artist=t.artist,
artist_credit=t.artist_credit,
artist_sort=t.artist_sort,
asin=a.asin,
catalognum=a.catalognum,
comp=a.va,
country=a.country,
day=a.day,
disc=t.medium,
disctitle=t.disctitle,
disctotal=a.mediums,
label=a.label,
language=a.language,
length=t.length,
mb_albumid=a.album_id,
mb_artistid=t.artist_id,
mb_releasegroupid=a.releasegroup_id,
mb_trackid=t.track_id,
media=t.media,
month=a.month,
script=a.script,
title=t.title,
track=t.index,
tracktotal=len(a.tracks),
year=a.year,
)

View File

@@ -170,8 +170,8 @@ class MPDStats:
item = self.lib.items(query).get()
if item:
return item
else:
self._log.info("item not found: {}", displayable_path(path))
self._log.info("item not found: {}", displayable_path(path))
return None
def update_item(self, item, attribute, value=None, increment=None):
"""Update the beets item. Set attribute to value or increment the value

View File

@@ -53,8 +53,7 @@ class BufferedSocket:
if self.sep in self.buf:
res, self.buf = self.buf.split(self.sep, 1)
return res + self.sep
else:
return b""
return b""
def send(self, data):
self.sock.send(data)

View File

@@ -137,7 +137,7 @@ class ParentWorkPlugin(MusicBrainzAPIMixin, BeetsPlugin):
"No work for {0}, add one at https://musicbrainz.org/recording/{0.mb_trackid}",
item,
)
return
return None
hasparent = hasattr(item, "parentwork")
work_changed = True
@@ -148,7 +148,7 @@ class ParentWorkPlugin(MusicBrainzAPIMixin, BeetsPlugin):
work_info, work_date = self.find_parentwork_info(item.mb_workid)
except requests.exceptions.RequestException:
self._log.debug("error fetching work", item, exc_info=True)
return
return None
parent_info = self.get_info(item, work_info)
parent_info["parentwork_workid_current"] = item.mb_workid
if "parent_composer" in parent_info:
@@ -165,7 +165,7 @@ class ParentWorkPlugin(MusicBrainzAPIMixin, BeetsPlugin):
elif hasparent:
self._log.debug("{}: Work present, skipping", item)
return
return None
# apply all non-null values to the item
for key, value in parent_info.items():
@@ -188,6 +188,7 @@ class ParentWorkPlugin(MusicBrainzAPIMixin, BeetsPlugin):
"parentwork_date",
],
)
return None
def find_parentwork_info(self, mb_workid: str) -> tuple[Work, str | None]:
"""Get the MusicBrainz information dict about a parent work, including

View File

@@ -181,19 +181,15 @@ class PlayPlugin(BeetsPlugin):
if args:
if ARGS_MARKER in command_str:
return command_str.replace(ARGS_MARKER, args)
else:
return f"{command_str} {args}"
else:
# Don't include the marker in the command.
return command_str.replace(f" {ARGS_MARKER}", "")
return f"{command_str} {args}"
# Don't include the marker in the command.
return command_str.replace(f" {ARGS_MARKER}", "")
def _playlist_or_paths(self, paths):
"""Return either the raw paths of items or a playlist of the items."""
if config["play"]["raw"]:
return paths
else:
return [self._create_tmp_playlist(paths)]
return [shlex.quote(self._create_tmp_playlist(paths))]
return [self._create_tmp_playlist(paths)]
def _exceeds_threshold(
self, selection, command_str, open_args, item_type="track"

View File

@@ -32,6 +32,7 @@ def get_music_section(
for child in tree.findall("Directory"):
if child.get("title") == library_name:
return child.get("key")
return None
def update_plex(host, port, token, library_name, secure, ignore_cert_errors):
@@ -51,8 +52,7 @@ def update_plex(host, port, token, library_name, secure, ignore_cert_errors):
url = urljoin(f"{get_protocol(secure)}://{host}:{port}", api_endpoint)
# Sends request and returns requests object.
r = requests.get(url, verify=not ignore_cert_errors, timeout=10)
return r
return requests.get(url, verify=not ignore_cert_errors, timeout=10)
def append_token(url, token):
@@ -65,8 +65,7 @@ def append_token(url, token):
def get_protocol(secure):
if secure:
return "https"
else:
return "http"
return "http"
class PlexUpdate(BeetsPlugin):

View File

@@ -109,13 +109,13 @@ def _equal_chance_permutation(
del groups[group]
def _take_time(iter: Iterable[LibModel], secs: float) -> Iterable[LibModel]:
def _take_time(iter_: Iterable[LibModel], secs: float) -> Iterable[LibModel]:
"""Return a list containing the first values in `iter`, which should
be Item or Album objects, that add up to the given amount of time in
seconds.
"""
total_time = 0.0
for obj in iter:
for obj in iter_:
length = obj.length
if total_time + length <= secs:
yield obj
@@ -151,5 +151,4 @@ def random_objs(
# Select objects by time our count.
if time_minutes:
return _take_time(perm, time_minutes * 60)
else:
return islice(perm, number)
return islice(perm, number)

View File

@@ -1026,7 +1026,6 @@ class AudioToolsBackend(Backend):
rg = self._mod_replaygain.ReplayGain(audiofile.sample_rate())
except ValueError:
raise ReplayGainError(f"Unsupported sample rate {item.samplerate}")
return
return rg
def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
@@ -1296,15 +1295,14 @@ class ReplayGainPlugin(BeetsPlugin):
self.backend_instance.NAME,
self._log,
)
else:
return RgTask(
items,
album,
self.config["targetlevel"].as_number(),
self.peak_method,
self.backend_instance.NAME,
self._log,
)
return RgTask(
items,
album,
self.config["targetlevel"].as_number(),
self.peak_method,
self.backend_instance.NAME,
self._log,
)
def handle_album(self, album: Album, write: bool, force: bool = False):
"""Compute album and track replay gain store it in all of the

View File

@@ -113,8 +113,6 @@ class APIError(Exception):
class AudioFeaturesUnavailableError(Exception):
"""Raised when audio features API returns 403 (deprecated)."""
pass
class SpotifyPlugin(
SearchApiMetadataSourcePlugin[SearchResponseAlbums | SearchResponseTracks]
@@ -279,12 +277,12 @@ class SpotifyPlugin(
return self._handle_response(
method, url, params=params, retry_count=retry_count + 1
)
elif e.response.status_code == 404:
if e.response.status_code == 404:
raise APIError(
f"API Error: {e.response.status_code}\n"
f"URL: {url}\nparams: {params}"
)
elif e.response.status_code == 403:
if e.response.status_code == 403:
# Check if this is the audio features endpoint
if url.startswith(self.audio_features_url):
raise AudioFeaturesUnavailableError(
@@ -295,7 +293,7 @@ class SpotifyPlugin(
f"API Error: {e.response.status_code}\n"
f"URL: {url}\nparams: {params}"
)
elif e.response.status_code == 429:
if e.response.status_code == 429:
seconds = e.response.headers.get(
"Retry-After", DEFAULT_WAITING_TIME
)
@@ -306,21 +304,20 @@ class SpotifyPlugin(
return self._handle_response(
method, url, params=params, retry_count=retry_count + 1
)
elif e.response.status_code == 503:
if e.response.status_code == 503:
self._log.error("Service Unavailable.")
raise APIError("Service Unavailable.")
elif e.response.status_code == 502:
if e.response.status_code == 502:
self._log.error("Bad Gateway.")
raise APIError("Bad Gateway.")
elif e.response is not None:
if e.response is not None:
raise APIError(
f"{self.data_source} API error:\n"
f"{e.response.text}\n"
f"URL:\n{url}\nparams:\n{params}"
)
else:
self._log.error("Request failed. Error: {}", e)
raise APIError("Request failed.")
self._log.error("Request failed. Error: {}", e)
raise APIError("Request failed.")
def _multi_artist_credit(
self, artists: list[dict[str | int, str]]
@@ -628,7 +625,7 @@ class SpotifyPlugin(
"Your beets query returned no items, skipping {.data_source}.",
self,
)
return
return None
self._log.info("Processing {} tracks...", len(items))

View File

@@ -41,19 +41,18 @@ def filter_to_be_removed(items, keys):
):
dont_remove.append(item)
return [item for item in items if item not in dont_remove]
else:
def to_be_removed(item):
for artist, album, title in keys:
if (
artist == item["artist"]
and album == item["album"]
and title == item["title"]
):
return False
return True
def to_be_removed(item):
for artist, album, title in keys:
if (
artist == item["artist"]
and album == item["album"]
and title == item["title"]
):
return False
return True
return [item for item in items if to_be_removed(item)]
return [item for item in items if to_be_removed(item)]
class SubsonicPlaylistPlugin(BeetsPlugin):
@@ -96,7 +95,7 @@ class SubsonicPlaylistPlugin(BeetsPlugin):
if playlist.attrib.get("code", "200") != "200":
alt_error = "error getting playlist, but no error message found"
self._log.warn(playlist.attrib.get("message", alt_error))
return
return None
name = playlist.attrib.get("name", "undefined")
tracks = [
@@ -167,11 +166,10 @@ class SubsonicPlaylistPlugin(BeetsPlugin):
params["s"] = b
params["v"] = "1.12.0"
params["c"] = "beets"
resp = requests.get(
return requests.get(
f"{self.config['base_url'].get()}/rest/{endpoint}?{urlencode(params)}",
timeout=10,
)
return resp
def get_playlists(self, ids):
output = {}

View File

@@ -36,8 +36,7 @@ class Substitute(BeetsPlugin):
for pattern, replacement in self.substitute_rules:
text = pattern.sub(replacement, text)
return text
else:
return ""
return ""
def __init__(self):
"""Initialize the substitute plugin.

View File

@@ -81,9 +81,8 @@ class ThePlugin(BeetsPlugin):
r = re.sub(r, "", text).strip()
if self.config["strip"]:
return r
else:
fmt = self.config["format"].as_str()
return fmt.format(r, t.strip()).strip()
fmt = self.config["format"].as_str()
return fmt.format(r, t.strip()).strip()
else:
return ""
@@ -97,5 +96,4 @@ class ThePlugin(BeetsPlugin):
self._log.debug('"{}" -> "{}"', text, r)
break
return r
else:
return ""
return ""

View File

@@ -89,9 +89,9 @@ class ThumbnailsPlugin(BeetsPlugin):
)
return False
for dir in (NORMAL_DIR, LARGE_DIR):
if not os.path.exists(syspath(dir)):
os.makedirs(syspath(dir))
for dir_ in (NORMAL_DIR, LARGE_DIR):
if not os.path.exists(syspath(dir_)):
os.makedirs(syspath(dir_))
if not ArtResizer.shared.can_write_metadata:
raise RuntimeError(
@@ -170,8 +170,8 @@ class ThumbnailsPlugin(BeetsPlugin):
See https://standards.freedesktop.org/thumbnail-spec/latest/x227.html
"""
uri = self.get_uri(path)
hash = md5(uri.encode("utf-8")).hexdigest()
return bytestring_path(f"{hash}.png")
hash_ = md5(uri.encode("utf-8")).hexdigest()
return bytestring_path(f"{hash_}.png")
def add_tags(self, album, image_path):
"""Write required metadata to the thumbnail

View File

@@ -432,8 +432,7 @@ class TidalPlugin(MetadataSourcePlugin):
"""
if version := attributes.get("version"):
return f"{attributes['title']} ({version})"
else:
return attributes["title"]
return attributes["title"]
@staticmethod
def _parse_data_url(
@@ -457,8 +456,8 @@ class TidalPlugin(MetadataSourcePlugin):
def _parse_label(
attributes: AlbumAttributes | TrackAttributes,
) -> str | None:
if copyright := attributes.get("copyright"):
return copyright["text"]
if copyright_ := attributes.get("copyright"):
return copyright_["text"]
return None
@staticmethod

View File

@@ -214,9 +214,9 @@ class TidalAPI(RequestHandler):
"links": {"next": url},
}
while next := doc.get("links", {}).get("next"):
while next_ := doc.get("links", {}).get("next"):
page_doc = self.get_json(
url=next, params={**params, "include": include}, **kwargs
url=next_, params={**params, "include": include}, **kwargs
)
doc = self.merge_multiresource_pagination(doc, page_doc)

View File

@@ -226,7 +226,7 @@ class TitlecasePlugin(BeetsPlugin):
# Check if A-Z is all uppercase or all lowercase
if self.all_lowercase and text.islower():
return text
elif self.all_caps and text.isupper():
if self.all_caps and text.isupper():
return text
# Any necessary replacements go first, mainly punctuation.
titlecased = text.lower() if self.force_lowercase else text

View File

@@ -70,7 +70,7 @@ def _rep(obj, expand=False):
return out
elif isinstance(obj, beets.library.Album):
if isinstance(obj, beets.library.Album):
if app.config.get("INCLUDE_PATHS", False):
out["artpath"] = util.displayable_path(out["artpath"])
else:
@@ -78,6 +78,7 @@ def _rep(obj, expand=False):
if expand:
out["items"] = [_rep(item) for item in obj.items()]
return out
return None
def json_generator(items, root, expand=False):
@@ -124,7 +125,7 @@ def resource(name, patchable=False):
def make_responder(retriever):
def responder(ids):
entities = [retriever(id) for id in ids]
entities = [retriever(id_) for id_ in ids]
entities = [entity for entity in entities if entity]
if get_method() == "DELETE":
@@ -136,7 +137,7 @@ def resource(name, patchable=False):
return flask.make_response(jsonify({"deleted": True}), 200)
elif get_method() == "PATCH" and patchable:
if get_method() == "PATCH" and patchable:
if app.config.get("READONLY", True):
return flask.abort(405)
@@ -146,7 +147,7 @@ def resource(name, patchable=False):
if len(entities) == 1:
return flask.jsonify(_rep(entities[0], expand=is_expand()))
elif entities:
if entities:
return app.response_class(
json_generator(entities, root=name),
mimetype="application/json",
@@ -155,16 +156,14 @@ def resource(name, patchable=False):
elif get_method() == "GET":
if len(entities) == 1:
return flask.jsonify(_rep(entities[0], expand=is_expand()))
elif entities:
if entities:
return app.response_class(
json_generator(entities, root=name),
mimetype="application/json",
)
else:
return flask.abort(404)
return flask.abort(404)
else:
return flask.abort(405)
return flask.abort(405)
responder.__name__ = f"get_{name}"
@@ -189,7 +188,7 @@ def resource_query(name, patchable=False):
return flask.make_response(jsonify({"deleted": True}), 200)
elif get_method() == "PATCH" and patchable:
if get_method() == "PATCH" and patchable:
if app.config.get("READONLY", True):
return flask.abort(405)
@@ -202,7 +201,7 @@ def resource_query(name, patchable=False):
mimetype="application/json",
)
elif get_method() == "GET":
if get_method() == "GET":
return app.response_class(
json_generator(
entities, root="results", expand=is_expand()
@@ -210,8 +209,7 @@ def resource_query(name, patchable=False):
mimetype="application/json",
)
else:
return flask.abort(405)
return flask.abort(405)
responder.__name__ = f"query_{name}"
@@ -254,9 +252,9 @@ class IdListConverter(BaseConverter):
def to_python(self, value):
ids = []
for id in value.split(","):
for id_ in value.split(","):
try:
ids.append(int(id))
ids.append(int(id_))
except ValueError:
pass
return ids
@@ -303,8 +301,8 @@ def before_request():
@app.route("/item/<idlist:ids>", methods=["GET", "DELETE", "PATCH"])
@resource("items", patchable=True)
def get_item(id):
return g.lib.get_item(id)
def get_item(id_):
return g.lib.get_item(id_)
@app.route("/item/")
@@ -329,10 +327,9 @@ def item_file(item_id):
else:
safe_filename = base_filename
response = flask.send_file(
return flask.send_file(
item_path, as_attachment=True, download_name=safe_filename
)
return response
@app.route("/item/query/<query:queries>", methods=["GET", "DELETE", "PATCH"])
@@ -347,8 +344,7 @@ def item_at_path(path):
item = g.lib.items(query).get()
if item:
return flask.jsonify(_rep(item))
else:
return flask.abort(404)
return flask.abort(404)
@app.route("/item/values/<string:key>")
@@ -368,8 +364,8 @@ def item_unique_field_values(key):
@app.route("/album/<idlist:ids>", methods=["GET", "DELETE"])
@resource("albums")
def get_album(id):
return g.lib.get_album(id)
def get_album(id_):
return g.lib.get_album(id_)
@app.route("/album/")
@@ -390,8 +386,7 @@ def album_art(album_id):
album = g.lib.get_album(album_id)
if album and album.artpath:
return flask.send_file(album.artpath.decode())
else:
return flask.abort(404)
return flask.abort(404)
@app.route("/album/values/<string:key>")

View File

@@ -14,7 +14,7 @@ sys.path.insert(0, str(Path(__file__).parent / "extensions"))
project = "beets"
AUTHOR = "Adrian Sampson"
copyright = "2016, Adrian Sampson"
copyright = "2016, Adrian Sampson" # noqa: A001
master_doc = "index"
language = "en"

View File

@@ -54,7 +54,7 @@ class Ref(NamedTuple):
if len(line_parts := line.split(" ", 1)) == 1:
return cls(line, None, None)
id, path_with_name = line_parts
id_, path_with_name = line_parts
parts = [p.strip() for p in path_with_name.split(":", 1)]
if len(parts) == 1:
@@ -62,7 +62,7 @@ class Ref(NamedTuple):
else:
name, path = parts
return cls(id, path, name)
return cls(id_, path, name)
@property
def url(self) -> str:

View File

@@ -311,6 +311,7 @@ skip-magic-trailing-comma = true
[tool.ruff.lint]
future-annotations = true
select = [
"A", # flake8-builtins
# "ARG", # flake8-unused-arguments
# "C4", # flake8-comprehensions
"E", # pycodestyle
@@ -320,7 +321,9 @@ select = [
"I", # isort
"ISC", # flake8-implicit-str-concat
"N", # pep8-naming
"PIE", # flake8-pie
"PT", # flake8-pytest-style
"RET", # flake8-return
"RUF", # ruff
"UP", # pyupgrade
"TC", # flake8-type-checking

View File

@@ -95,6 +95,7 @@ def pytest_make_parametrize_id(config, val, argname):
def pytest_assertrepr_compare(op, left, right):
if isinstance(left, Distance) or isinstance(right, Distance):
return [f"Comparing Distance: {float(left)} {op} {float(right)}"]
return None
@pytest.fixture(autouse=True)

View File

@@ -31,7 +31,7 @@ class BeatportTest(BeetsTestCase):
The list of elements on the returned dict is incomplete, including just
those required for the tests on this class.
"""
results = {
return {
"id": 1742984,
"type": "release",
"name": "Charade",
@@ -61,7 +61,6 @@ class BeatportTest(BeetsTestCase):
{"id": 9, "name": "Breaks", "slug": "breaks", "type": "genre"}
],
}
return results
def _make_tracks_response(self):
"""Return a list that mimics a response from the beatport API.
@@ -71,7 +70,7 @@ class BeatportTest(BeetsTestCase):
The list of elements on the returned list is incomplete, including just
those required for the tests on this class.
"""
results = [
return [
{
"id": 7817567,
"type": "track",
@@ -445,7 +444,6 @@ class BeatportTest(BeetsTestCase):
},
},
]
return results
def setUp(self):
super().setUp()
@@ -561,9 +559,10 @@ class BeatportTest(BeetsTestCase):
# Specify beatport ids here because an 'item.id' is beets-internal.
ids = [7817567, 7817568, 7817569, 7817570, 7817571, 7817572]
# Concatenate with 'id' to pass strict equality test.
for track, test_track, id in zip(self.tracks, self.test_tracks, ids):
for track, test_track, id_ in zip(self.tracks, self.test_tracks, ids):
assert (
track.url == f"https://beatport.com/track/{test_track.url}/{id}"
track.url
== f"https://beatport.com/track/{test_track.url}/{id_}"
)
def test_bpm_applied(self):
@@ -581,7 +580,7 @@ class BeatportTest(BeetsTestCase):
class BeatportResponseEmptyTest(unittest.TestCase):
def _make_tracks_response(self):
results = [
return [
{
"id": 7817567,
"name": "Mirage a Trois",
@@ -603,7 +602,6 @@ class BeatportResponseEmptyTest(unittest.TestCase):
],
}
]
return results
def setUp(self):
super().setUp()

View File

@@ -93,15 +93,14 @@ class MPCResponse:
def _parse_status(self, status):
"""Parses the first response line, which contains the status."""
if status.startswith("OK") or status.startswith("list_OK"):
if status.startswith(("OK", "list_OK")):
return True, None
elif status.startswith("ACK"):
if status.startswith("ACK"):
code, rest = status[5:].split("@", 1)
pos, rest = rest.split("]", 1)
cmd, rest = rest[2:].split("}")
return False, (int(code), int(pos), cmd, rest[1:])
else:
raise RuntimeError(f"Unexpected status: {status!r}")
raise RuntimeError(f"Unexpected status: {status!r}")
def _parse_body(self, body):
"""Messages are generally in the format "header: content".
@@ -147,15 +146,14 @@ class MPCClient:
while True:
line = self.readline()
response += line
if line.startswith(b"OK") or line.startswith(b"ACK"):
if line.startswith((b"OK", b"ACK")):
if force_multi or any(responses):
if line.startswith(b"ACK"):
responses.append(MPCResponse(response))
n_remaining = force_multi - len(responses)
responses.extend([None] * n_remaining)
return responses
else:
return MPCResponse(response)
return MPCResponse(response)
if line.startswith(b"list_OK"):
responses.append(MPCResponse(response))
response = b""

View File

@@ -49,9 +49,9 @@ class ChromaTest(IOMixin, PluginMixin, ImportTestCase):
def run_search(self, fp):
return self.run_with_output("chromasearch", "-s", fp, "-f", "$title")
def line_count(self, str):
def line_count(self, str_):
return len(
[line for line in str.split("\n") if line.strip(" \n") != ""]
[line for line in str_.split("\n") if line.strip(" \n") != ""]
)
def compare_fingerprints(self, *args, **kwargs):

View File

@@ -31,10 +31,9 @@ class ExportPluginTest(IOMixin, PluginTestCase):
def execute_command(self, format_type, artist):
query = ",".join(self.test_values.keys())
out = self.run_with_output(
return self.run_with_output(
"export", "-f", format_type, "-i", query, artist
)
return out
def create_item(self):
(item,) = self.add_item_fixtures()

View File

@@ -49,7 +49,7 @@ class FetchartCliTest(IOMixin, PluginTestCase):
self.skipTest("unable to set file attributes")
def test_set_art_from_folder(self):
self.touch(b"c\xc3\xb6ver.jpg", dir=self.album.path, content="IMAGE")
self.touch(b"c\xc3\xb6ver.jpg", dir_=self.album.path, content="IMAGE")
self.run_command("fetchart")
@@ -63,21 +63,21 @@ class FetchartCliTest(IOMixin, PluginTestCase):
assert self.album["artpath"] is None
def test_filesystem_does_not_pick_up_ignored_file(self):
self.touch(b"co_ver.jpg", dir=self.album.path, content="IMAGE")
self.touch(b"co_ver.jpg", dir_=self.album.path, content="IMAGE")
self.config["ignore"] = ["*_*"]
self.run_command("fetchart")
self.album.load()
assert self.album["artpath"] is None
def test_filesystem_picks_up_non_ignored_file(self):
self.touch(b"cover.jpg", dir=self.album.path, content="IMAGE")
self.touch(b"cover.jpg", dir_=self.album.path, content="IMAGE")
self.config["ignore"] = ["*_*"]
self.run_command("fetchart")
self.album.load()
self.check_cover_is_stored()
def test_filesystem_does_not_pick_up_hidden_file(self):
self.touch(b".cover.jpg", dir=self.album.path, content="IMAGE")
self.touch(b".cover.jpg", dir_=self.album.path, content="IMAGE")
if sys.platform == "win32":
self.hide_file_windows()
self.config["ignore"] = [] # By default, ignore includes '.*'.
@@ -87,14 +87,14 @@ class FetchartCliTest(IOMixin, PluginTestCase):
assert self.album["artpath"] is None
def test_filesystem_picks_up_non_hidden_file(self):
self.touch(b"cover.jpg", dir=self.album.path, content="IMAGE")
self.touch(b"cover.jpg", dir_=self.album.path, content="IMAGE")
self.config["ignore_hidden"] = True
self.run_command("fetchart")
self.album.load()
self.check_cover_is_stored()
def test_filesystem_picks_up_hidden_file(self):
self.touch(b".cover.jpg", dir=self.album.path, content="IMAGE")
self.touch(b".cover.jpg", dir_=self.album.path, content="IMAGE")
if sys.platform == "win32":
self.hide_file_windows()
self.config["ignore"] = [] # By default, ignore includes '.*'.
@@ -104,13 +104,13 @@ class FetchartCliTest(IOMixin, PluginTestCase):
self.check_cover_is_stored()
def test_filesystem_picks_up_webp_file(self):
self.touch(b"cover.webp", dir=self.album.path, content="IMAGE")
self.touch(b"cover.webp", dir_=self.album.path, content="IMAGE")
self.run_command("fetchart")
self.album.load()
self.check_cover_is_stored("webp")
def test_filesystem_picks_up_png_file(self):
self.touch(b"cover.png", dir=self.album.path, content="IMAGE")
self.touch(b"cover.png", dir_=self.album.path, content="IMAGE")
self.run_command("fetchart")
self.album.load()
self.check_cover_is_stored("png")
@@ -124,7 +124,7 @@ class FetchartCliTest(IOMixin, PluginTestCase):
"""OSError (e.g. PermissionError) in set_art is logged as a warning,
not an unhandled crash. Regression test for #6193.
"""
self.touch(b"c\xc3\xb6ver.jpg", dir=self.album.path, content="IMAGE")
self.touch(b"c\xc3\xb6ver.jpg", dir_=self.album.path, content="IMAGE")
with mock.patch(
"beets.library.Album.set_art",
side_effect=PermissionError("[WinError 32] file in use"),

View File

@@ -155,7 +155,7 @@ class TestListenBrainzPlugin(ConfigMixin):
call_count += 1
if call_count == 1:
return {"payload": {"listens": page1}}
elif call_count == 2:
if call_count == 2:
return {"payload": {"listens": page2}}
return {"payload": {"listens": []}}

View File

@@ -922,7 +922,7 @@ class TestMusicBrainzPlugin(MusicBrainzPluginTestMixin):
assert excinfo.value is error
@pytest.mark.parametrize(
"input,expected",
"input_,expected",
[
("??-??-??", (None, None, None)),
("??-01-??", (None, 1, None)),
@@ -935,5 +935,5 @@ class TestMusicBrainzPlugin(MusicBrainzPluginTestMixin):
("2010-01-02", (2010, 1, 2)),
],
)
def test_get_date(self, input, expected):
assert musicbrainz._get_date(input) == expected
def test_get_date(self, input_, expected):
assert musicbrainz._get_date(input_) == expected

View File

@@ -188,22 +188,22 @@ class SmartPlaylistTest(BeetsTestCase):
pl = b"$title-my<playlist>.m3u", (q, None), (a_q, None)
spl._matched_playlists = {pl}
dir = mkdtemp()
dir_ = mkdtemp()
config["smartplaylist"]["relative_to"] = False
config["smartplaylist"]["playlist_dir"] = str(dir)
config["smartplaylist"]["playlist_dir"] = str(dir_)
try:
spl.update_playlists(lib)
except Exception:
rmtree(syspath(dir))
rmtree(syspath(dir_))
raise
lib.items.assert_called_once_with(q, None)
lib.albums.assert_called_once_with(a_q, None)
m3u_filepath = Path(dir, "ta_ga_da-my_playlist_.m3u")
m3u_filepath = Path(dir_, "ta_ga_da-my_playlist_.m3u")
assert m3u_filepath.exists()
content = m3u_filepath.read_bytes()
rmtree(syspath(dir))
rmtree(syspath(dir_))
assert content == b"/tagada.mp3\n"
@@ -229,24 +229,24 @@ class SmartPlaylistTest(BeetsTestCase):
pl = b"$title-my<playlist>.m3u", (q, None), (a_q, None)
spl._matched_playlists = {pl}
dir = mkdtemp()
dir_ = mkdtemp()
config["smartplaylist"]["output"] = "extm3u"
config["smartplaylist"]["prefix"] = "http://beets:8337/files"
config["smartplaylist"]["relative_to"] = False
config["smartplaylist"]["playlist_dir"] = str(dir)
config["smartplaylist"]["playlist_dir"] = str(dir_)
try:
spl.update_playlists(lib)
except Exception:
rmtree(syspath(dir))
rmtree(syspath(dir_))
raise
lib.items.assert_called_once_with(q, None)
lib.albums.assert_called_once_with(a_q, None)
m3u_filepath = Path(dir, "ta_ga_da-my_playlist_.m3u")
m3u_filepath = Path(dir_, "ta_ga_da-my_playlist_.m3u")
assert m3u_filepath.exists()
content = m3u_filepath.read_bytes()
rmtree(syspath(dir))
rmtree(syspath(dir_))
assert content == (
b"#EXTM3U\n"
@@ -278,24 +278,24 @@ class SmartPlaylistTest(BeetsTestCase):
pl = b"$title-my<playlist>.m3u", (q, None), (a_q, None)
spl._matched_playlists = {pl}
dir = mkdtemp()
dir_ = mkdtemp()
config["smartplaylist"]["output"] = "extm3u"
config["smartplaylist"]["relative_to"] = False
config["smartplaylist"]["playlist_dir"] = str(dir)
config["smartplaylist"]["playlist_dir"] = str(dir_)
config["smartplaylist"]["fields"] = ["id", "genres"]
try:
spl.update_playlists(lib)
except Exception:
rmtree(syspath(dir))
rmtree(syspath(dir_))
raise
lib.items.assert_called_once_with(q, None)
lib.albums.assert_called_once_with(a_q, None)
m3u_filepath = Path(dir, "ta_ga_da-my_playlist_.m3u")
m3u_filepath = Path(dir_, "ta_ga_da-my_playlist_.m3u")
assert m3u_filepath.exists()
content = m3u_filepath.read_bytes()
rmtree(syspath(dir))
rmtree(syspath(dir_))
assert content == (
b"#EXTM3U\n"

View File

@@ -24,8 +24,8 @@ class SubstitutePluginTest(PluginTestCase):
def run_substitute(self, config, cases):
with self.configure_plugin(config):
for input, expected in cases:
assert Substitute().tmpl_substitute(input) == expected
for input_, expected in cases:
assert Substitute().tmpl_substitute(input_) == expected
def test_simple_substitute(self):
self.run_substitute(

View File

@@ -119,10 +119,9 @@ class ThumbnailsTest(BeetsTestCase):
def os_stat(target):
if target == syspath(md5_file):
return Mock(st_mtime=1)
elif target == syspath(path_to_art):
if target == syspath(path_to_art):
return Mock(st_mtime=2)
else:
raise ValueError(f"invalid target {target}")
raise ValueError(f"invalid target {target}")
mock_os.stat.side_effect = os_stat
@@ -147,10 +146,9 @@ class ThumbnailsTest(BeetsTestCase):
def os_stat(target):
if target == syspath(md5_file):
return Mock(st_mtime=3)
elif target == syspath(path_to_art):
if target == syspath(path_to_art):
return Mock(st_mtime=2)
else:
raise ValueError(f"invalid target {target}")
raise ValueError(f"invalid target {target}")
mock_os.stat.side_effect = os_stat

View File

@@ -22,16 +22,16 @@ if TYPE_CHECKING:
)
def _make_artist(id: str, name: str) -> TidalArtist:
def _make_artist(id_: str, name: str) -> TidalArtist:
return {
"id": id,
"id": id_,
"type": "artists",
"attributes": {"name": name, "popularity": 0.5},
}
def _make_album(
id: str,
id_: str,
title: str,
tracks: list[TidalTrack],
artist_ids: list[str],
@@ -59,7 +59,7 @@ def _make_album(
attrs["version"] = version
album: TidalAlbum = {
"id": id,
"id": id_,
"type": "albums",
"attributes": attrs,
"relationships": {
@@ -77,7 +77,7 @@ def _make_album(
def _make_track(
id: str,
id_: str,
title: str,
duration: str = "PT3M30S",
isrc: str = "ISRC123",
@@ -97,7 +97,7 @@ def _make_track(
if version:
attrs["version"] = version
return {
"id": id,
"id": id_,
"type": "tracks",
"attributes": attrs,
"relationships": {

View File

@@ -45,7 +45,6 @@ class DummyPILBackend(PILBackend):
def __init__(self):
"""Init a dummy backend class for mocked PIL tests."""
pass
class ArtResizerFileSizeTest(CleanupModulesMixin, BeetsTestCase):

View File

@@ -165,8 +165,7 @@ def create_archive(session):
archive = ZipFile(os.fsdecode(path), mode="w")
archive.write(syspath(os.path.join(_common.RSRC, b"full.mp3")), "full.mp3")
archive.close()
path = bytestring_path(path)
return path
return bytestring_path(path)
class RmTempTest(BeetsTestCase):
@@ -519,7 +518,7 @@ class ImportTest(PathsMixin, AutotagImportTestCase):
def test_skip_non_album_dirs(self):
assert (self.import_path / "album").exists()
self.touch(b"cruft", dir=self.import_dir)
self.touch(b"cruft", dir_=self.import_dir)
self.importer.add_choice(importer.Action.APPLY)
self.importer.run()
@@ -534,7 +533,7 @@ class ImportTest(PathsMixin, AutotagImportTestCase):
def test_empty_directory_warning(self):
import_dir = os.path.join(self.temp_dir, b"empty")
self.touch(b"non-audio", dir=import_dir)
self.touch(b"non-audio", dir_=import_dir)
self.setup_importer(import_dir=import_dir)
with capture_log() as logs:
self.importer.run()
@@ -544,7 +543,7 @@ class ImportTest(PathsMixin, AutotagImportTestCase):
def test_empty_directory_singleton_warning(self):
import_dir = os.path.join(self.temp_dir, b"empty")
self.touch(b"non-audio", dir=import_dir)
self.touch(b"non-audio", dir_=import_dir)
self.setup_singleton_importer(import_dir=import_dir)
with capture_log() as logs:
self.importer.run()
@@ -1442,18 +1441,18 @@ class AlbumsInDirTest(BeetsTestCase):
class MultiDiscAlbumsInDirTest(BeetsTestCase):
def create_music(self, files=True, ascii=True):
def create_music(self, files=True, ascii_=True):
"""Create some music in multiple album directories.
`files` indicates whether to create the files (otherwise, only
directories are made). `ascii` indicates ACII-only filenames;
directories are made). `ascii_` indicates ACII-only filenames;
otherwise, we use Unicode names.
"""
self.base = os.path.abspath(os.path.join(self.temp_dir, b"tempdir"))
os.mkdir(syspath(self.base))
name = b"CAT" if ascii else util.bytestring_path("C\xc1T")
name_alt_case = b"CAt" if ascii else util.bytestring_path("C\xc1t")
name = b"CAT" if ascii_ else util.bytestring_path("C\xc1T")
name_alt_case = b"CAt" if ascii_ else util.bytestring_path("C\xc1t")
self.dirs = [
# Nested album, multiple subdirs.
@@ -1492,7 +1491,7 @@ class MultiDiscAlbumsInDirTest(BeetsTestCase):
os.path.join(self.base, b"artist [CD5]", name + b"S", b"song7.mp3"),
]
if not ascii:
if not ascii_:
self.dirs = [self._normalize_path(p) for p in self.dirs]
self.files = [self._normalize_path(p) for p in self.files]
@@ -1546,14 +1545,14 @@ class MultiDiscAlbumsInDirTest(BeetsTestCase):
assert len(albums) == 0
def test_single_disc_unicode(self):
self.create_music(ascii=False)
self.create_music(ascii_=False)
albums = list(albums_in_dir(self.base))
root, items = albums[3]
assert root == self.dirs[8:]
assert len(items) == 1
def test_coalesce_multiple_unicode(self):
self.create_music(ascii=False)
self.create_music(ascii_=False)
albums = list(albums_in_dir(self.base))
assert len(albums) == 4
root, items = albums[0]
@@ -1736,7 +1735,6 @@ class ImportPretendTest(IOMixin, AutotagImportTestCase):
assert len(self.lib.albums()) == 0
return [line for line in logs if not line.startswith("Sending event:")]
assert self._album().data_source == "original_source"
def test_import_singletons_pretend(self):
assert self.__run(self.setup_singleton_importer(pretend=True)) == [

View File

@@ -103,8 +103,8 @@ class ConfigCommandTest(IOMixin, BeetsTestCase):
execlp.assert_called_once_with("myvisual", "myvisual", self.config_path)
def test_edit_config_with_automatic_open(self):
with patch("beets.util.open_anything") as open:
open.return_value = "please_open"
with patch("beets.util.open_anything") as open_:
open_.return_value = "please_open"
with patch("os.execlp") as execlp:
self.run_command("config", "-e")
execlp.assert_called_once_with(

View File

@@ -94,7 +94,7 @@ class ModifyTest(IOMixin, BeetsTestCase):
album = "album"
original_artist = "composer"
new_artist = "coverArtist"
for i in range(0, 10):
for i in range(10):
self.add_item_fixture(
title=f"{title}{i}", artist=original_artist, album=album
)
@@ -109,7 +109,7 @@ class ModifyTest(IOMixin, BeetsTestCase):
assert len(list(new_items)) == 7
def test_modify_formatted(self):
for i in range(0, 3):
for i in range(3):
self.add_item_fixture(
title=f"title{i}", artist="artist", album="album"
)

View File

@@ -22,8 +22,7 @@ class QueryTest(BeetsTestCase):
return item
def add_album(self, items):
album = self.lib.add_album(items)
return album
return self.lib.add_album(items)
def check_do_query(
self, num_items, num_albums, q=(), album=False, also_items=True