diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 7aea1f81a..4137fe11e 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -81,9 +81,17 @@ d93ddf8dd43e4f9ed072a03829e287c78d2570a2 59c93e70139f70e9fd1c6f3c1bceb005945bec33 # Moved ui.commands._utils into ui.commands.utils 25ae330044abf04045e3f378f72bbaed739fb30d -# Refactor test_ui_command.py into multiple modules +# Refactor test_ui_command.py into multiple modules a59e41a88365e414db3282658d2aa456e0b3468a # pyupgrade Python 3.10 301637a1609831947cb5dd90270ed46c24b1ab1b # Fix changelog formatting 658b184c59388635787b447983ecd3a575f4fe56 +# Configure future-annotations +ac7f3d9da95c2d0a32e5c908ea68480518a1582d +# Configure ruff for py310 +c46069654628040316dea9db85d01b263db3ba9e +# Enable RUF rules +4749599913a42e02e66b37db9190de11d6be2cdf +# Address RUF012 +bc71ec308eb938df1d349f6857634ddf2a82e339 diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 8adbaeda1..374ea3c13 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -25,7 +25,7 @@ import lap import numpy as np from beets import config, logging, metadata_plugins, plugins -from beets.autotag import AlbumInfo, AlbumMatch, TrackInfo, TrackMatch, hooks +from beets.autotag import AlbumMatch, TrackMatch, hooks from beets.util import get_most_common_tags from .distance import VA_ARTISTS, distance, track_distance @@ -33,6 +33,7 @@ from .distance import VA_ARTISTS, distance, track_distance if TYPE_CHECKING: from collections.abc import Iterable, Sequence + from beets.autotag import AlbumInfo, TrackInfo from beets.library import Item # Global logger. diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 5d721a121..33d5dd5f2 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -26,17 +26,10 @@ import threading import time from abc import ABC from collections import defaultdict -from collections.abc import ( - Callable, - Generator, - Iterable, - Iterator, - Mapping, - Sequence, -) +from collections.abc import Mapping from functools import cached_property -from sqlite3 import Connection, sqlite_version_info -from typing import TYPE_CHECKING, Any, AnyStr, Generic +from sqlite3 import sqlite_version_info +from typing import TYPE_CHECKING, Any, AnyStr, ClassVar, Generic from typing_extensions import ( Self, @@ -48,20 +41,20 @@ import beets from ..util import cached_classproperty, functemplate from . import types -from .query import ( - FieldQueryType, - FieldSort, - MatchQuery, - NullSort, - Query, - Sort, - TrueQuery, -) +from .query import MatchQuery, NullSort, TrueQuery if TYPE_CHECKING: + from collections.abc import ( + Callable, + Generator, + Iterable, + Iterator, + Sequence, + ) + from sqlite3 import Connection from types import TracebackType - from .query import SQLiteType + from .query import FieldQueryType, FieldSort, Query, Sort, SQLiteType D = TypeVar("D", bound="Database", default=Any) @@ -306,7 +299,7 @@ class Model(ABC, Generic[D]): """The flex field SQLite table name. """ - _fields: dict[str, types.Type] = {} + _fields: ClassVar[dict[str, types.Type]] = {} """A mapping indicating available "fixed" fields on this type. The keys are field names and the values are `Type` objects. """ @@ -321,7 +314,7 @@ class Model(ABC, Generic[D]): """Optional types for non-fixed (flexible and computed) fields.""" return {} - _sorts: dict[str, type[FieldSort]] = {} + _sorts: ClassVar[dict[str, type[FieldSort]]] = {} """Optional named sort criteria. The keys are strings and the values are subclasses of `Sort`. """ diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index dfeb42707..f486df672 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -20,17 +20,19 @@ import os import re import unicodedata from abc import ABC, abstractmethod -from collections.abc import Iterator, MutableSequence, Sequence +from collections.abc import Sequence from datetime import datetime, timedelta from functools import cached_property, reduce from operator import mul, or_ from re import Pattern -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union +from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar from beets import util from beets.util.units import raw_seconds_short if TYPE_CHECKING: + from collections.abc import Iterator, MutableSequence + from beets.dbcore.db import AnyModel, Model P = TypeVar("P", default=Any) @@ -122,7 +124,7 @@ class Query(ABC): return hash(type(self)) -SQLiteType = Union[str, bytes, float, int, memoryview, None] +SQLiteType = str | bytes | float | int | memoryview | None AnySQLiteType = TypeVar("AnySQLiteType", bound=SQLiteType) FieldQueryType = type["FieldQuery"] @@ -689,7 +691,12 @@ class Period: ("%Y-%m-%dT%H:%M", "%Y-%m-%d %H:%M"), # minute ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"), # second ) - relative_units = {"y": 365, "m": 30, "w": 7, "d": 1} + relative_units: ClassVar[dict[str, int]] = { + "y": 365, + "m": 30, + "w": 7, + "d": 1, + } relative_re = "(?P[+|-]?)(?P[0-9]+)(?P[y|m|w|d])" def __init__(self, date: datetime, precision: str): diff --git a/beets/dbcore/queryparse.py b/beets/dbcore/queryparse.py index f84ed7436..f14420448 100644 --- a/beets/dbcore/queryparse.py +++ b/beets/dbcore/queryparse.py @@ -250,7 +250,7 @@ def parse_sorted_query( # Split up query in to comma-separated subqueries, each representing # an AndQuery, which need to be joined together in one OrQuery subquery_parts = [] - for part in parts + [","]: + for part in [*parts, ","]: if part.endswith(","): # Ensure we can catch "foo, bar" as well as "foo , bar" last_subquery_part = part[:-1] diff --git a/beets/dbcore/types.py b/beets/dbcore/types.py index 3b4badd33..61336d9ce 100644 --- a/beets/dbcore/types.py +++ b/beets/dbcore/types.py @@ -20,7 +20,7 @@ import re import time import typing from abc import ABC -from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, cast import beets from beets import util @@ -406,7 +406,7 @@ class MusicalKey(String): The standard format is C, Cm, C#, C#m, etc. """ - ENHARMONIC = { + ENHARMONIC: ClassVar[dict[str, str]] = { r"db": "c#", r"eb": "d#", r"gb": "f#", diff --git a/beets/importer/__init__.py b/beets/importer/__init__.py index 586b238e6..6e49ba9e2 100644 --- a/beets/importer/__init__.py +++ b/beets/importer/__init__.py @@ -28,11 +28,11 @@ from .tasks import ( # Note: Stages are not exposed to the public API __all__ = [ - "ImportSession", - "ImportAbortError", "Action", - "ImportTask", "ArchiveImportTask", + "ImportAbortError", + "ImportSession", + "ImportTask", "SentinelImportTask", "SingletonImportTask", ] diff --git a/beets/importer/session.py b/beets/importer/session.py index 83c5ad4e3..123cc7248 100644 --- a/beets/importer/session.py +++ b/beets/importer/session.py @@ -17,7 +17,7 @@ import os import time from typing import TYPE_CHECKING -from beets import config, dbcore, library, logging, plugins, util +from beets import config, logging, plugins, util from beets.importer.tasks import Action from beets.util import displayable_path, normpath, pipeline, syspath @@ -27,6 +27,7 @@ from .state import ImportState if TYPE_CHECKING: from collections.abc import Sequence + from beets import dbcore, library from beets.util import PathBytes from .tasks import ImportTask diff --git a/beets/importer/stages.py b/beets/importer/stages.py index 5474053d0..0f8cf922b 100644 --- a/beets/importer/stages.py +++ b/beets/importer/stages.py @@ -388,5 +388,5 @@ def _extend_pipeline(tasks, *stages): else: task_iter = tasks - ipl = pipeline.Pipeline([task_iter] + list(stages)) + ipl = pipeline.Pipeline([task_iter, *list(stages)]) return pipeline.multiple(ipl.pull()) diff --git a/beets/importer/tasks.py b/beets/importer/tasks.py index 3a9c044b2..646b64e7f 100644 --- a/beets/importer/tasks.py +++ b/beets/importer/tasks.py @@ -20,7 +20,7 @@ import re import shutil import time from collections import defaultdict -from collections.abc import Callable, Iterable, Sequence +from collections.abc import Callable from enum import Enum from tempfile import mkdtemp from typing import TYPE_CHECKING, Any @@ -33,6 +33,8 @@ from beets.dbcore.query import PathQuery from .state import ImportState if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + from beets.autotag.match import Recommendation from .session import ImportSession @@ -232,7 +234,7 @@ class ImportTask(BaseImportTask): or APPLY (in which case the data comes from the choice). """ if self.choice_flag in (Action.ASIS, Action.RETAG): - likelies, consensus = util.get_most_common_tags(self.items) + likelies, _ = util.get_most_common_tags(self.items) return likelies elif self.choice_flag is Action.APPLY and self.match: return self.match.info.copy() @@ -890,7 +892,7 @@ class ArchiveImportTask(SentinelImportTask): # The (0, 0, -1) is added to date_time because the # function time.mktime expects a 9-element tuple. # The -1 indicates that the DST flag is unknown. - date_time = time.mktime(f.date_time + (0, 0, -1)) + date_time = time.mktime((*f.date_time, 0, 0, -1)) fullpath = os.path.join(extract_to, f.filename) os.utime(fullpath, (date_time, date_time)) diff --git a/beets/library/__init__.py b/beets/library/__init__.py index 22416ecb5..0f3d7d155 100644 --- a/beets/library/__init__.py +++ b/beets/library/__init__.py @@ -17,13 +17,13 @@ def __getattr__(name: str): __all__ = [ - "Library", - "LibModel", "Album", - "Item", - "parse_query_parts", - "parse_query_string", "FileOperationError", + "Item", + "LibModel", + "Library", "ReadError", "WriteError", + "parse_query_parts", + "parse_query_string", ] diff --git a/beets/library/models.py b/beets/library/models.py index 9609989bc..aee055134 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -7,7 +7,7 @@ import time import unicodedata from functools import cached_property from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar from mediafile import MediaFile, UnreadableFileError @@ -229,7 +229,7 @@ class Album(LibModel): _table = "albums" _flex_table = "album_attributes" _always_dirty = True - _fields = { + _fields: ClassVar[dict[str, types.Type]] = { "id": types.PRIMARY_ID, "artpath": types.NullPathType(), "added": types.DATE, @@ -281,13 +281,13 @@ class Album(LibModel): def _types(cls) -> dict[str, types.Type]: return {**super()._types, "path": types.PathType()} - _sorts = { + _sorts: ClassVar[dict[str, type[dbcore.query.FieldSort]]] = { "albumartist": dbcore.query.SmartArtistSort, "artist": dbcore.query.SmartArtistSort, } # List of keys that are set on an album's items. - item_keys = [ + item_keys: ClassVar[list[str]] = [ "added", "albumartist", "albumartists", @@ -624,7 +624,7 @@ class Item(LibModel): _table = "items" _flex_table = "item_attributes" - _fields = { + _fields: ClassVar[dict[str, types.Type]] = { "id": types.PRIMARY_ID, "path": types.PathType(), "album_id": types.FOREIGN_ID, @@ -744,7 +744,9 @@ class Item(LibModel): _formatter = FormattedItemMapping - _sorts = {"artist": dbcore.query.SmartArtistSort} + _sorts: ClassVar[dict[str, type[dbcore.query.FieldSort]]] = { + "artist": dbcore.query.SmartArtistSort + } @cached_classproperty def _queries(cls) -> dict[str, FieldQueryType]: diff --git a/beets/logging.py b/beets/logging.py index 5a837cd80..0fc3a13e7 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -35,10 +35,25 @@ from logging import ( Handler, Logger, NullHandler, - RootLogger, StreamHandler, ) -from typing import TYPE_CHECKING, Any, TypeVar, Union, overload +from typing import TYPE_CHECKING, Any, TypeVar, overload + +if TYPE_CHECKING: + from collections.abc import Mapping + from logging import RootLogger + from types import TracebackType + + T = TypeVar("T") + + # see https://github.com/python/typeshed/blob/main/stdlib/logging/__init__.pyi + _SysExcInfoType = ( + tuple[type[BaseException], BaseException, TracebackType | None] + | tuple[None, None, None] + ) + _ExcInfoType = _SysExcInfoType | BaseException | bool | None + _ArgsType = tuple[object, ...] | Mapping[str, object] + __all__ = [ "DEBUG", @@ -54,21 +69,6 @@ __all__ = [ "getLogger", ] -if TYPE_CHECKING: - from collections.abc import Mapping - - T = TypeVar("T") - from types import TracebackType - - # see https://github.com/python/typeshed/blob/main/stdlib/logging/__init__.pyi - _SysExcInfoType = Union[ - tuple[type[BaseException], BaseException, Union[TracebackType, None]], - tuple[None, None, None], - ] - _ExcInfoType = Union[None, bool, _SysExcInfoType, BaseException] - _ArgsType = Union[tuple[object, ...], Mapping[str, object]] - - # Regular expression to match: # - C0 control characters (0x00-0x1F) except useful whitespace (\t, \n, \r) # - DEL control character (0x7f) diff --git a/beets/plugins.py b/beets/plugins.py index 0dc2754b9..ec3f999c4 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -141,7 +141,13 @@ class PluginLogFilter(logging.Filter): # Managing the plugins themselves. -class BeetsPlugin(metaclass=abc.ABCMeta): +class BeetsPluginMeta(abc.ABCMeta): + template_funcs: ClassVar[TFuncMap[str]] = {} + template_fields: ClassVar[TFuncMap[Item]] = {} + album_template_fields: ClassVar[TFuncMap[Album]] = {} + + +class BeetsPlugin(metaclass=BeetsPluginMeta): """The base class for all beets plugins. Plugins provide functionality by defining a subclass of BeetsPlugin and overriding the abstract methods defined here. @@ -151,9 +157,10 @@ class BeetsPlugin(metaclass=abc.ABCMeta): list ) listeners: ClassVar[dict[EventType, list[Listener]]] = defaultdict(list) - template_funcs: ClassVar[TFuncMap[str]] | TFuncMap[str] = {} # type: ignore[valid-type] - template_fields: ClassVar[TFuncMap[Item]] | TFuncMap[Item] = {} # type: ignore[valid-type] - album_template_fields: ClassVar[TFuncMap[Album]] | TFuncMap[Album] = {} # type: ignore[valid-type] + + template_funcs: TFuncMap[str] + template_fields: TFuncMap[Item] + album_template_fields: TFuncMap[Album] name: str config: ConfigView @@ -161,7 +168,7 @@ class BeetsPlugin(metaclass=abc.ABCMeta): import_stages: list[ImportStageFunc] def __init_subclass__(cls) -> None: - """Enable legacy metadata‐source plugins to work with the new interface. + """Enable legacy metadata source plugins to work with the new interface. When a plugin subclass of BeetsPlugin defines a `data_source` attribute but does not inherit from MetadataSourcePlugin, this hook: @@ -220,14 +227,10 @@ class BeetsPlugin(metaclass=abc.ABCMeta): self.name = name or self.__module__.split(".")[-1] self.config = beets.config[self.name] - # If the class attributes are not set, initialize as instance attributes. - # TODO: Revise with v3.0.0, see also type: ignore[valid-type] above - if not self.template_funcs: - self.template_funcs = {} - if not self.template_fields: - self.template_fields = {} - if not self.album_template_fields: - self.album_template_fields = {} + # create per-instance storage for template fields and functions + self.template_funcs = {} + self.template_fields = {} + self.album_template_fields = {} self.early_import_stages = [] self.import_stages = [] diff --git a/beets/test/helper.py b/beets/test/helper.py index 3cb1e4c3c..207b0e491 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -120,7 +120,7 @@ def capture_stdout(): def has_program(cmd, args=["--version"]): """Returns `True` if `cmd` can be executed.""" - full_cmd = [cmd] + args + full_cmd = [cmd, *args] try: with open(os.devnull, "wb") as devnull: subprocess.check_call( @@ -524,7 +524,7 @@ class ImportHelper(TestHelper): autotagging library and several assertions for the library. """ - default_import_config = { + default_import_config: ClassVar[dict[str, bool]] = { "autotag": True, "copy": True, "hardlink": False, @@ -880,7 +880,7 @@ class FetchImageHelper: def run(self, *args, **kwargs): super().run(*args, **kwargs) - IMAGEHEADER: dict[str, bytes] = { + IMAGEHEADER: ClassVar[dict[str, bytes]] = { "image/jpeg": b"\xff\xd8\xff\x00\x00\x00JFIF", "image/png": b"\211PNG\r\n\032\n", "image/gif": b"GIF89a", diff --git a/beets/ui/commands/import_/display.py b/beets/ui/commands/import_/display.py index 113462d19..bdc44d51f 100644 --- a/beets/ui/commands/import_/display.py +++ b/beets/ui/commands/import_/display.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, TypedDict from typing_extensions import NotRequired -from beets import autotag, config, ui +from beets import config, ui from beets.autotag import hooks from beets.util import displayable_path from beets.util.units import human_seconds_short @@ -17,6 +17,7 @@ if TYPE_CHECKING: import confuse + from beets import autotag from beets.autotag.distance import Distance from beets.library.models import Item from beets.ui import ColorName @@ -338,13 +339,9 @@ class ChangeRepresentation: max_width_l = max(get_width(line_tuple[0]) for line_tuple in lines) max_width_r = max(get_width(line_tuple[1]) for line_tuple in lines) - if ( - (max_width_l <= col_width) - and (max_width_r <= col_width) - or ( - ((max_width_l > col_width) or (max_width_r > col_width)) - and ((max_width_l + max_width_r) <= col_width * 2) - ) + if ((max_width_l <= col_width) and (max_width_r <= col_width)) or ( + ((max_width_l > col_width) or (max_width_r > col_width)) + and ((max_width_l + max_width_r) <= col_width * 2) ): # All content fits. Either both maximum widths are below column # widths, or one of the columns is larger than allowed but the @@ -558,7 +555,7 @@ def penalty_string(distance: Distance, limit: int | None = None) -> str: penalties.append(key) if penalties: if limit and len(penalties) > limit: - penalties = penalties[:limit] + ["..."] + penalties = [*penalties[:limit], "..."] # Prefix penalty string with U+2260: Not Equal To penalty_string = f"\u2260 {', '.join(penalties)}" return ui.colorize("changed", penalty_string) diff --git a/beets/ui/commands/import_/session.py b/beets/ui/commands/import_/session.py index 9c8c8dd62..42a809634 100644 --- a/beets/ui/commands/import_/session.py +++ b/beets/ui/commands/import_/session.py @@ -256,13 +256,11 @@ class TerminalImportSession(importer.ImportSession): # Add a "dummy" choice for the other baked-in option, for # duplicate checking. - all_choices = ( - [ - PromptChoice("a", "Apply", None), - ] - + choices - + extra_choices - ) + all_choices = [ + PromptChoice("a", "Apply", None), + *choices, + *extra_choices, + ] # Check for conflicts. short_letters = [c.short for c in all_choices] @@ -501,7 +499,7 @@ def choose_candidate( if config["import"]["bell"]: ui.print_("\a", end="") sel = ui.input_options( - ("Apply", "More candidates") + choice_opts, + ("Apply", "More candidates", *choice_opts), require=require, default=default, ) diff --git a/beets/ui/commands/move.py b/beets/ui/commands/move.py index 40a9d1b83..206c24dcf 100644 --- a/beets/ui/commands/move.py +++ b/beets/ui/commands/move.py @@ -1,18 +1,18 @@ """The 'move' command: Move/copy files to the library or a new base directory.""" +from __future__ import annotations + import os +from typing import TYPE_CHECKING from beets import logging, ui -from beets.util import ( - MoveOperation, - PathLike, - displayable_path, - normpath, - syspath, -) +from beets.util import MoveOperation, displayable_path, normpath, syspath from .utils import do_query +if TYPE_CHECKING: + from beets.util import PathLike + # Global logger. log = logging.getLogger("beets") diff --git a/beets/ui/commands/write.py b/beets/ui/commands/write.py index 05c3c7565..87fba8236 100644 --- a/beets/ui/commands/write.py +++ b/beets/ui/commands/write.py @@ -15,7 +15,7 @@ def write_items(lib, query, pretend, force): """Write tag information from the database to the respective files in the filesystem. """ - items, albums = do_query(lib, query, False, False) + items, _ = do_query(lib, query, False, False) for item in items: # Item deleted? diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 517e076de..ea08bb65d 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -28,7 +28,7 @@ import sys import tempfile import traceback from collections import Counter -from collections.abc import Callable, Sequence +from collections.abc import Sequence from contextlib import suppress from enum import Enum from functools import cache @@ -44,7 +44,6 @@ from typing import ( Generic, NamedTuple, TypeVar, - Union, cast, ) @@ -54,7 +53,7 @@ import beets from beets.util import hidden if TYPE_CHECKING: - from collections.abc import Iterable, Iterator + from collections.abc import Callable, Iterable, Iterator from logging import Logger from beets.library import Item @@ -63,8 +62,8 @@ if TYPE_CHECKING: MAX_FILENAME_LENGTH = 200 WINDOWS_MAGIC_PREFIX = "\\\\?\\" T = TypeVar("T") -PathLike = Union[str, bytes, Path] -StrPath = Union[str, Path] +StrPath = str | Path +PathLike = StrPath | bytes Replacements = Sequence[tuple[Pattern[str], str]] # Here for now to allow for a easy replace later on diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 72007d0b5..6fec62774 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -268,7 +268,8 @@ class IMBackend(LocalBackend): # with regards to the height. # ImageMagick already seems to default to no interlace, but we include # it here for the sake of explicitness. - cmd: list[str] = self.convert_cmd + [ + cmd: list[str] = [ + *self.convert_cmd, syspath(path_in, prefix=False), "-resize", f"{maxwidth}x>", @@ -298,7 +299,8 @@ class IMBackend(LocalBackend): return path_out def get_size(self, path_in: bytes) -> tuple[int, int] | None: - cmd: list[str] = self.identify_cmd + [ + cmd: list[str] = [ + *self.identify_cmd, "-format", "%w %h", syspath(path_in, prefix=False), @@ -336,7 +338,8 @@ class IMBackend(LocalBackend): if not path_out: path_out = get_temp_filename(__name__, "deinterlace_IM_", path_in) - cmd = self.convert_cmd + [ + cmd = [ + *self.convert_cmd, syspath(path_in, prefix=False), "-interlace", "none", @@ -351,7 +354,7 @@ class IMBackend(LocalBackend): return path_in def get_format(self, path_in: bytes) -> str | None: - cmd = self.identify_cmd + ["-format", "%[magick]", syspath(path_in)] + cmd = [*self.identify_cmd, "-format", "%[magick]", syspath(path_in)] try: # Image formats should really only be ASCII strings such as "PNG", @@ -368,7 +371,8 @@ class IMBackend(LocalBackend): target: bytes, deinterlaced: bool, ) -> bytes: - cmd = self.convert_cmd + [ + cmd = [ + *self.convert_cmd, syspath(source), *(["-interlace", "none"] if deinterlaced else []), syspath(target), @@ -400,14 +404,16 @@ class IMBackend(LocalBackend): # to grayscale and then pipe them into the `compare` command. # On Windows, ImageMagick doesn't support the magic \\?\ prefix # on paths, so we pass `prefix=False` to `syspath`. - convert_cmd = self.convert_cmd + [ + convert_cmd = [ + *self.convert_cmd, syspath(im2, prefix=False), syspath(im1, prefix=False), "-colorspace", "gray", "MIFF:-", ] - compare_cmd = self.compare_cmd + [ + compare_cmd = [ + *self.compare_cmd, "-define", "phash:colorspaces=sRGB,HCLp", "-metric", @@ -487,7 +493,7 @@ class IMBackend(LocalBackend): ("-set", k, v) for k, v in metadata.items() ) str_file = os.fsdecode(file) - command = self.convert_cmd + [str_file, *assignments, str_file] + command = [*self.convert_cmd, str_file, *assignments, str_file] util.command_output(command) @@ -828,7 +834,7 @@ class ArtResizer: "jpeg": "jpg", }.get(new_format, new_format) - fname, ext = os.path.splitext(path_in) + fname, _ = os.path.splitext(path_in) path_new = fname + b"." + new_format.encode("utf8") # allows the exception to propagate, while still making sure a changed diff --git a/beets/util/pipeline.py b/beets/util/pipeline.py index 2ed593904..2c1e72e53 100644 --- a/beets/util/pipeline.py +++ b/beets/util/pipeline.py @@ -192,7 +192,7 @@ def stage( task: R | T | None = None while True: task = yield task - task = func(*(args + (task,))) + task = func(*args, task) return coro @@ -216,7 +216,7 @@ def mutator_stage(func: Callable[[Unpack[A], T], R]): task = None while True: task = yield task - func(*(args + (task,))) + func(*args, task) return coro diff --git a/beetsplug/acousticbrainz.py b/beetsplug/acousticbrainz.py index 92a1976a1..09a56e0a7 100644 --- a/beetsplug/acousticbrainz.py +++ b/beetsplug/acousticbrainz.py @@ -15,6 +15,7 @@ """Fetch various AcousticBrainz metadata using MBID.""" from collections import defaultdict +from typing import ClassVar import requests @@ -55,7 +56,7 @@ ABSCHEME = { class AcousticPlugin(plugins.BeetsPlugin): - item_types = { + item_types: ClassVar[dict[str, types.Type]] = { "average_loudness": types.Float(6), "chords_changes_rate": types.Float(6), "chords_key": types.STRING, diff --git a/beetsplug/albumtypes.py b/beetsplug/albumtypes.py index 180773f58..3b6535d85 100644 --- a/beetsplug/albumtypes.py +++ b/beetsplug/albumtypes.py @@ -14,11 +14,17 @@ """Adds an album template field for formatted album types.""" -from beets.library import Album +from __future__ import annotations + +from typing import TYPE_CHECKING + from beets.plugins import BeetsPlugin from .musicbrainz import VARIOUS_ARTISTS_ID +if TYPE_CHECKING: + from beets.library import Album + class AlbumTypesPlugin(BeetsPlugin): """Adds an album template field for formatted album types.""" diff --git a/beetsplug/aura.py b/beetsplug/aura.py index 7b75f31e5..c1877db82 100644 --- a/beetsplug/aura.py +++ b/beetsplug/aura.py @@ -14,12 +14,13 @@ """An AURA server using Flask.""" +from __future__ import annotations + import os import re -from collections.abc import Mapping from dataclasses import dataclass from mimetypes import guess_type -from typing import ClassVar +from typing import TYPE_CHECKING, ClassVar from flask import ( Blueprint, @@ -40,12 +41,17 @@ from beets.dbcore.query import ( NotQuery, RegexpQuery, SlowFieldSort, - SQLiteType, ) -from beets.library import Album, Item, LibModel, Library +from beets.library import Album, Item from beets.plugins import BeetsPlugin from beets.ui import Subcommand, _open_library +if TYPE_CHECKING: + from collections.abc import Mapping + + from beets.dbcore.query import SQLiteType + from beets.library import LibModel, Library + # Constants # AURA server information diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index 0359259b7..30126f370 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -26,7 +26,7 @@ import sys import time import traceback from string import Template -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar import beets import beets.ui @@ -1037,7 +1037,7 @@ class Command: raise BPDError(ERROR_PERMISSION, "insufficient privileges") try: - args = [conn] + self.args + args = [conn, *self.args] results = func(*args) if results: for data in results: @@ -1344,7 +1344,7 @@ class Server(BaseServer): # Searching. - tagtype_map = { + tagtype_map: ClassVar[dict[str, str]] = { "Artist": "artist", "ArtistSort": "artist_sort", "Album": "album", diff --git a/beetsplug/bpd/gstplayer.py b/beetsplug/bpd/gstplayer.py index f356b3066..e4f38af88 100644 --- a/beetsplug/bpd/gstplayer.py +++ b/beetsplug/bpd/gstplayer.py @@ -37,7 +37,7 @@ except ValueError as e: # makes it so the test collector functions as inteded. raise ImportError from e -from gi.repository import GLib, Gst # noqa: E402 +from gi.repository import GLib, Gst Gst.init(None) @@ -115,7 +115,7 @@ class GstPlayer: elif message.type == Gst.MessageType.ERROR: # error self.player.set_state(Gst.State.NULL) - err, debug = message.parse_error() + err, _ = message.parse_error() print(f"Error: {err}") self.playing = False @@ -205,7 +205,7 @@ class GstPlayer: def seek(self, position): """Seeks to position (in seconds).""" - cur_pos, cur_len = self.time() + _, cur_len = self.time() if position > cur_len: self.stop() return diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py index fbdf8cc70..34cb08cce 100644 --- a/beetsplug/bpsync.py +++ b/beetsplug/bpsync.py @@ -73,7 +73,7 @@ class BPSyncPlugin(BeetsPlugin): """Retrieve and apply info from the autotagger for items matched by query. """ - for item in lib.items(query + ["singleton:true"]): + for item in lib.items([*query, "singleton:true"]): if not item.mb_trackid: self._log.info( "Skipping singleton with no mb_trackid: {}", item diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index 1e9835789..748e6f5cd 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -16,20 +16,26 @@ autotagger. Requires the pyacoustid library. """ +from __future__ import annotations + import re from collections import defaultdict -from collections.abc import Iterable from functools import cached_property, partial +from typing import TYPE_CHECKING import acoustid import confuse from beets import config, ui, util from beets.autotag.distance import Distance -from beets.autotag.hooks import TrackInfo from beets.metadata_plugins import MetadataSourcePlugin from beetsplug.musicbrainz import MusicBrainzPlugin +if TYPE_CHECKING: + from collections.abc import Iterable + + from beets.autotag.hooks import TrackInfo + API_KEY = "1vOwZtEn" SCORE_THRESH = 0.5 TRACK_ID_WEIGHT = 10.0 diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 74ced8ae3..2e837c77f 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -274,7 +274,7 @@ class ConvertPlugin(BeetsPlugin): pretend, hardlink, link, - playlist, + _, force, ) = self._get_opts_and_config(empty_opts) diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py index ef27dddc7..61b028361 100644 --- a/beetsplug/deezer.py +++ b/beetsplug/deezer.py @@ -18,29 +18,26 @@ from __future__ import annotations import collections import time -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, ClassVar, Literal import requests from beets import ui from beets.autotag import AlbumInfo, TrackInfo from beets.dbcore import types -from beets.metadata_plugins import ( - IDResponse, - SearchApiMetadataSourcePlugin, - SearchFilter, -) +from beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin if TYPE_CHECKING: from collections.abc import Sequence from beets.library import Item, Library + from beets.metadata_plugins import SearchFilter from ._typing import JSONDict class DeezerPlugin(SearchApiMetadataSourcePlugin[IDResponse]): - item_types = { + item_types: ClassVar[dict[str, types.Type]] = { "deezer_track_rank": types.INTEGER, "deezer_track_id": types.INTEGER, "deezer_updated": types.DATE, diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 29600a676..08d437d2d 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -572,7 +572,7 @@ class DiscogsPlugin(MetadataSourcePlugin): processed = self._process_clean_tracklist( clean_tracklist, album_artist_data ) - tracks, index_tracks, index, divisions, next_divisions = processed + tracks, index_tracks, *_ = processed # Fix up medium and medium_index for each track. Discogs position is # unreliable, but tracks are in order. medium = None diff --git a/beetsplug/export.py b/beetsplug/export.py index e6c2b88c7..21db190b1 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -148,7 +148,7 @@ class ExportPlugin(BeetsPlugin): album=opts.album, ): try: - data, item = data_emitter(included_keys or "*") + data, _ = data_emitter(included_keys or "*") except (mediafile.UnreadableFileError, OSError) as ex: self._log.error("cannot read file: {}", ex) continue diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index f1cc85f44..ef311cbbd 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -355,7 +355,7 @@ class ArtSource(RequestMixin, ABC): # Specify whether this source fetches local or remote images LOC: ClassVar[SourceLocation] # A list of methods to match metadata, sorted by descending accuracy - VALID_MATCHING_CRITERIA: list[str] = ["default"] + VALID_MATCHING_CRITERIA: ClassVar[list[str]] = ["default"] # A human-readable name for the art source NAME: ClassVar[str] # The key to select the art source in the config. This value will also be @@ -518,8 +518,8 @@ class RemoteArtSource(ArtSource): class CoverArtArchive(RemoteArtSource): NAME = "Cover Art Archive" ID = "coverart" - VALID_MATCHING_CRITERIA = ["release", "releasegroup"] - VALID_THUMBNAIL_SIZES = [250, 500, 1200] + VALID_MATCHING_CRITERIA: ClassVar[list[str]] = ["release", "releasegroup"] + VALID_THUMBNAIL_SIZES: ClassVar[list[int]] = [250, 500, 1200] URL = "https://coverartarchive.org/release/{mbid}" GROUP_URL = "https://coverartarchive.org/release-group/{mbid}" @@ -867,7 +867,7 @@ class ITunesStore(RemoteArtSource): ) except KeyError as e: self._log.debug( - "Malformed itunes candidate: {} not found in {}", # NOQA E501 + "Malformed itunes candidate: {} not found in {}", e, list(c.keys()), ) @@ -1128,7 +1128,7 @@ class LastFM(RemoteArtSource): ID = "lastfm" # Sizes in priority order. - SIZES = OrderedDict( + SIZES: ClassVar[dict[str, tuple[int, int]]] = OrderedDict( [ ("mega", (300, 300)), ("extralarge", (300, 300)), diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index c3fb4bc6b..be7fee23a 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -88,7 +88,7 @@ def apply_matches(d, log): """Given a mapping from items to field dicts, apply the fields to the objects. """ - some_map = list(d.values())[0] + some_map = next(iter(d.values())) keys = some_map.keys() # Only proceed if the "tag" field is equal across all filenames. diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index e2aff24e5..e0e9b8740 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -62,7 +62,7 @@ class KeyFinderPlugin(BeetsPlugin): try: output = util.command_output( - command + [util.syspath(item.path)] + [*command, util.syspath(item.path)] ).stdout except (subprocess.CalledProcessError, OSError) as exc: self._log.error("execution failed: {}", exc) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index e622096cf..121d76596 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -28,7 +28,7 @@ import os import traceback from functools import singledispatchmethod from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any import pylast import yaml @@ -39,6 +39,7 @@ from beets.util import plurality, unique_list if TYPE_CHECKING: import optparse + from collections.abc import Callable from beets.library import LibModel @@ -67,12 +68,12 @@ def flatten_tree( if isinstance(elem, dict): for k, v in elem.items(): - flatten_tree(v, path + [k], branches) + flatten_tree(v, [*path, k], branches) elif isinstance(elem, list): for sub in elem: flatten_tree(sub, path, branches) else: - branches.append(path + [str(elem)]) + branches.append([*path, str(elem)]) def find_parents(candidate: str, branches: list[list[str]]) -> list[str]: diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index d6e14c175..7995daefc 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -26,7 +26,7 @@ from functools import cached_property, partial, total_ordering from html import unescape from itertools import groupby from pathlib import Path -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, ClassVar, NamedTuple from urllib.parse import quote, quote_plus, urlencode, urlparse import langdetect @@ -367,7 +367,7 @@ class LRCLib(Backend): class MusiXmatch(Backend): URL_TEMPLATE = "https://www.musixmatch.com/lyrics/{}/{}" - REPLACEMENTS = { + REPLACEMENTS: ClassVar[dict[str, str]] = { r"\s+": "-", "<": "Less_Than", ">": "Greater_Than", @@ -600,7 +600,7 @@ class Google(SearchBackend): SEARCH_URL = "https://www.googleapis.com/customsearch/v1" #: Exclude some letras.mus.br pages which do not contain lyrics. - EXCLUDE_PAGES = [ + EXCLUDE_PAGES: ClassVar[list[str]] = [ "significado.html", "traduccion.html", "traducao.html", @@ -630,9 +630,12 @@ class Google(SearchBackend): #: Split cleaned up URL title into artist and title parts. URL_TITLE_PARTS_RE = re.compile(r" +(?:[ :|-]+|par|by) +|, ") - SOURCE_DIST_FACTOR = {"www.azlyrics.com": 0.5, "www.songlyrics.com": 0.6} + SOURCE_DIST_FACTOR: ClassVar[dict[str, float]] = { + "www.azlyrics.com": 0.5, + "www.songlyrics.com": 0.6, + } - ignored_domains: set[str] = set() + ignored_domains: ClassVar[set[str]] = set() @classmethod def pre_process_html(cls, html: str) -> str: @@ -937,7 +940,7 @@ class RestFiles: class LyricsPlugin(LyricsRequestHandler, plugins.BeetsPlugin): - BACKEND_BY_NAME = { + BACKEND_BY_NAME: ClassVar[dict[str, type[Backend]]] = { b.name: b for b in [LRCLib, Google, Genius, Tekstowo, MusiXmatch] } diff --git a/beetsplug/mbpseudo.py b/beetsplug/mbpseudo.py index 30ef2e428..d084d1531 100644 --- a/beetsplug/mbpseudo.py +++ b/beetsplug/mbpseudo.py @@ -24,7 +24,7 @@ import mediafile from typing_extensions import override from beets import config -from beets.autotag.distance import Distance, distance +from beets.autotag.distance import distance from beets.autotag.hooks import AlbumInfo from beets.autotag.match import assign_items from beets.plugins import find_plugins @@ -39,6 +39,7 @@ if TYPE_CHECKING: from collections.abc import Iterable, Sequence from beets.autotag import AlbumMatch + from beets.autotag.distance import Distance from beets.library import Item from beetsplug._typing import JSONDict diff --git a/beetsplug/mbsubmit.py b/beetsplug/mbsubmit.py index f6d197256..7136f4c29 100644 --- a/beetsplug/mbsubmit.py +++ b/beetsplug/mbsubmit.py @@ -69,7 +69,7 @@ class MBSubmitPlugin(BeetsPlugin): paths.append(displayable_path(p)) try: picard_path = self.config["picard_path"].as_str() - subprocess.Popen([picard_path] + paths) + subprocess.Popen([picard_path, *paths]) self._log.info("launched picard from\n{}", picard_path) except OSError as exc: self._log.error("Could not open picard, got error:\n{}", exc) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 5b74b67c9..45f34e865 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -71,7 +71,7 @@ class MBSyncPlugin(BeetsPlugin): """Retrieve and apply info from the autotagger for items matched by query. """ - for item in lib.items(query + ["singleton:true"]): + for item in lib.items([*query, "singleton:true"]): if not item.mb_trackid: self._log.info( "Skipping singleton with no mb_trackid: {}", item diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py index d4e31851e..22cc8145e 100644 --- a/beetsplug/metasync/__init__.py +++ b/beetsplug/metasync/__init__.py @@ -14,14 +14,20 @@ """Synchronize information from music player libraries""" +from __future__ import annotations + from abc import ABCMeta, abstractmethod from importlib import import_module +from typing import TYPE_CHECKING, ClassVar from confuse import ConfigValueError from beets import ui from beets.plugins import BeetsPlugin +if TYPE_CHECKING: + from beets.dbcore import types + METASYNC_MODULE = "beetsplug.metasync" # Dictionary to map the MODULE and the CLASS NAME of meta sources @@ -32,8 +38,9 @@ SOURCES = { class MetaSource(metaclass=ABCMeta): + item_types: ClassVar[dict[str, types.Type]] + def __init__(self, config, log): - self.item_types = {} self.config = config self._log = log diff --git a/beetsplug/metasync/amarok.py b/beetsplug/metasync/amarok.py index 47e6a1a65..f092dd59c 100644 --- a/beetsplug/metasync/amarok.py +++ b/beetsplug/metasync/amarok.py @@ -17,6 +17,7 @@ from datetime import datetime from os.path import basename from time import mktime +from typing import ClassVar from xml.sax.saxutils import quoteattr from beets.dbcore import types @@ -35,7 +36,7 @@ dbus = import_dbus() class Amarok(MetaSource): - item_types = { + item_types: ClassVar[dict[str, types.Type]] = { "amarok_rating": types.INTEGER, "amarok_score": types.FLOAT, "amarok_uid": types.STRING, diff --git a/beetsplug/metasync/itunes.py b/beetsplug/metasync/itunes.py index 6f441ef8b..88582622d 100644 --- a/beetsplug/metasync/itunes.py +++ b/beetsplug/metasync/itunes.py @@ -20,6 +20,7 @@ import shutil import tempfile from contextlib import contextmanager from time import mktime +from typing import ClassVar from urllib.parse import unquote, urlparse from confuse import ConfigValueError @@ -58,7 +59,7 @@ def _norm_itunes_path(path): class Itunes(MetaSource): - item_types = { + item_types: ClassVar[dict[str, types.Type]] = { "itunes_rating": types.INTEGER, # 0..100 scale "itunes_playcount": types.INTEGER, "itunes_skipcount": types.INTEGER, diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 63a7bae22..d2aae14e9 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -15,19 +15,26 @@ """List missing tracks.""" +from __future__ import annotations + from collections import defaultdict -from collections.abc import Iterator +from typing import TYPE_CHECKING, ClassVar import requests from beets import config, metadata_plugins from beets.dbcore import types -from beets.library import Album, Item, Library +from beets.library import Item from beets.plugins import BeetsPlugin from beets.ui import Subcommand, print_ from ._utils.musicbrainz import MusicBrainzAPIMixin +if TYPE_CHECKING: + from collections.abc import Iterator + + from beets.library import Album, Library + MB_ARTIST_QUERY = r"mb_albumartistid::^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$" @@ -89,7 +96,7 @@ def _item(track_info, album_info, album_id): class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin): """List missing tracks""" - album_types = { + album_types: ClassVar[dict[str, types.Type]] = { "missing": types.INTEGER, } diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 0a3e1de02..f195df290 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -15,6 +15,7 @@ import os import time +from typing import ClassVar import mpd @@ -318,7 +319,7 @@ class MPDStats: class MPDStatsPlugin(plugins.BeetsPlugin): - item_types = { + item_types: ClassVar[dict[str, types.Type]] = { "play_count": types.INTEGER, "skip_count": types.INTEGER, "last_played": types.DATE, diff --git a/beetsplug/playlist.py b/beetsplug/playlist.py index 07c12e0e0..a1f9fff39 100644 --- a/beetsplug/playlist.py +++ b/beetsplug/playlist.py @@ -10,17 +10,22 @@ # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. - +from __future__ import annotations import os import tempfile -from collections.abc import Sequence from pathlib import Path +from typing import TYPE_CHECKING, ClassVar import beets from beets.dbcore.query import BLOB_TYPE, InQuery from beets.util import path_as_posix +if TYPE_CHECKING: + from collections.abc import Sequence + + from beets.dbcore.query import FieldQueryType + def is_m3u_file(path: str) -> bool: return Path(path).suffix.lower() in {".m3u", ".m3u8"} @@ -82,7 +87,9 @@ class PlaylistQuery(InQuery[bytes]): class PlaylistPlugin(beets.plugins.BeetsPlugin): - item_queries = {"playlist": PlaylistQuery} + item_queries: ClassVar[dict[str, FieldQueryType]] = { + "playlist": PlaylistQuery + } def __init__(self): super().__init__() diff --git a/beetsplug/replace.py b/beetsplug/replace.py index 0c570877b..b585a13c1 100644 --- a/beetsplug/replace.py +++ b/beetsplug/replace.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import shutil from pathlib import Path +from typing import TYPE_CHECKING import mediafile from beets import ui, util -from beets.library import Item, Library from beets.plugins import BeetsPlugin +if TYPE_CHECKING: + from beets.library import Item, Library + class ReplacePlugin(BeetsPlugin): def commands(self): diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index a8c887caa..4e8b429ea 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -642,11 +642,11 @@ class CommandBackend(Backend): cmd: list[str] = [self.command, "-o", "-s", "s"] if self.noclip: # Adjust to avoid clipping. - cmd = cmd + ["-k"] + cmd = [*cmd, "-k"] else: # Disable clipping warning. - cmd = cmd + ["-c"] - cmd = cmd + ["-d", str(int(target_level - 89))] + cmd = [*cmd, "-c"] + cmd = [*cmd, "-d", str(int(target_level - 89))] cmd = cmd + [syspath(i.path) for i in items] self._log.debug("analyzing {} files", len(items)) @@ -1105,7 +1105,7 @@ class AudioToolsBackend(Backend): # The first item is taken and opened to get the sample rate to # initialize the replaygain object. The object is used for all the # tracks in the album to get the album values. - item = list(task.items)[0] + item = next(iter(task.items)) audiofile = self.open_audio_file(item) rg = self.init_replaygain(audiofile, item) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index ed417f2b9..e22a65787 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -17,13 +17,13 @@ from __future__ import annotations import os -from typing import Any, TypeAlias +from typing import TYPE_CHECKING, Any, TypeAlias from urllib.parse import quote from urllib.request import pathname2url from beets import ui from beets.dbcore.query import ParsingError, Query, Sort -from beets.library import Album, Item, Library, parse_query_string +from beets.library import Album, Item, parse_query_string from beets.plugins import BeetsPlugin from beets.plugins import send as send_event from beets.util import ( @@ -36,6 +36,9 @@ from beets.util import ( syspath, ) +if TYPE_CHECKING: + from beets.library import Library + QueryAndSort = tuple[Query, Sort] PlaylistQuery = Query | tuple[QueryAndSort, ...] | None PlaylistMatch: TypeAlias = tuple[ diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index ab920cdd4..9b26b1e49 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -27,7 +27,7 @@ import re import threading import time import webbrowser -from typing import TYPE_CHECKING, Any, Literal, Union +from typing import TYPE_CHECKING, Any, ClassVar, Literal import confuse import requests @@ -36,16 +36,13 @@ from beets import ui from beets.autotag.hooks import AlbumInfo, TrackInfo from beets.dbcore import types from beets.library import Library -from beets.metadata_plugins import ( - IDResponse, - SearchApiMetadataSourcePlugin, - SearchFilter, -) +from beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin if TYPE_CHECKING: from collections.abc import Sequence from beets.library import Library + from beets.metadata_plugins import SearchFilter from beetsplug._typing import JSONDict DEFAULT_WAITING_TIME = 5 @@ -89,11 +86,9 @@ class AudioFeaturesUnavailableError(Exception): class SpotifyPlugin( - SearchApiMetadataSourcePlugin[ - Union[SearchResponseAlbums, SearchResponseTracks] - ] + SearchApiMetadataSourcePlugin[SearchResponseAlbums | SearchResponseTracks] ): - item_types = { + item_types: ClassVar[dict[str, types.Type]] = { "spotify_track_popularity": types.INTEGER, "spotify_acousticness": types.FLOAT, "spotify_danceability": types.FLOAT, @@ -119,7 +114,7 @@ class SpotifyPlugin( track_url = "https://api.spotify.com/v1/tracks/" audio_features_url = "https://api.spotify.com/v1/audio-features/" - spotify_audio_features = { + spotify_audio_features: ClassVar[dict[str, str]] = { "acousticness": "spotify_acousticness", "danceability": "spotify_danceability", "energy": "spotify_energy", diff --git a/beetsplug/the.py b/beetsplug/the.py index 664d4c01e..94dc7ee52 100644 --- a/beetsplug/the.py +++ b/beetsplug/the.py @@ -15,6 +15,7 @@ """Moves patterns in path formats (suitable for moving articles).""" import re +from typing import ClassVar from beets.plugins import BeetsPlugin @@ -27,7 +28,7 @@ FORMAT = "{}, {}" class ThePlugin(BeetsPlugin): - patterns: list[str] = [] + patterns: ClassVar[list[str]] = [] def __init__(self): super().__init__() @@ -58,9 +59,9 @@ class ThePlugin(BeetsPlugin): p, ) if self.config["a"]: - self.patterns = [PATTERN_A] + self.patterns + self.patterns = [PATTERN_A, *self.patterns] if self.config["the"]: - self.patterns = [PATTERN_THE] + self.patterns + self.patterns = [PATTERN_THE, *self.patterns] if not self.patterns: self._log.warning("no patterns defined!") diff --git a/beetsplug/titlecase.py b/beetsplug/titlecase.py index e7003fd28..d722d4d16 100644 --- a/beetsplug/titlecase.py +++ b/beetsplug/titlecase.py @@ -16,18 +16,23 @@ Title case logic is derived from the python-titlecase library. Provides a template function and a tag modification function.""" +from __future__ import annotations + import re from functools import cached_property -from typing import TypedDict +from typing import TYPE_CHECKING, TypedDict from titlecase import titlecase from beets import ui -from beets.autotag.hooks import AlbumInfo, Info -from beets.importer import ImportSession, ImportTask -from beets.library import Item +from beets.autotag.hooks import AlbumInfo from beets.plugins import BeetsPlugin +if TYPE_CHECKING: + from beets.autotag.hooks import Info + from beets.importer import ImportSession, ImportTask + from beets.library import Item + __author__ = "henryoberholtzer@gmail.com" __version__ = "1.0" diff --git a/docs/extensions/conf.py b/docs/extensions/conf.py index 308d28be2..e69103f59 100644 --- a/docs/extensions/conf.py +++ b/docs/extensions/conf.py @@ -72,10 +72,10 @@ class ConfDomain(Domain): name = "conf" label = "Simple Configuration" - object_types = {"conf": ObjType("conf", "conf")} - directives = {"conf": Conf} - roles = {"conf": XRefRole()} - initial_data: dict[str, Any] = {"objects": {}} + object_types = {"conf": ObjType("conf", "conf")} # noqa: RUF012 + directives = {"conf": Conf} # noqa: RUF012 + roles = {"conf": XRefRole()} # noqa: RUF012 + initial_data: dict[str, Any] = {"objects": {}} # noqa: RUF012 def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]: """Return an iterable of object tuples for the inventory.""" diff --git a/poetry.lock b/poetry.lock index 5a0832399..8eb7c74ac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4583,4 +4583,4 @@ web = ["flask", "flask-cors"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4" -content-hash = "8a1714daca55eab559558f2d4bd63d4857686eb607bf4b24f1ea6dbd412e6641" +content-hash = "f8ce55ae74c5e3c5d1d330582f83dae30ef963a0b8dd8c8b79f16c3bcfdb525a" diff --git a/pyproject.toml b/pyproject.toml index dbfc2715b..b14f442ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,7 +117,7 @@ titlecase = "^2.4.1" [tool.poetry.group.lint.dependencies] docstrfmt = ">=1.11.1" -ruff = ">=0.6.4" +ruff = ">=0.13.0" sphinx-lint = ">=1.0.0" [tool.poetry.group.typing.dependencies] @@ -226,7 +226,7 @@ cmd = "make -C docs $COMMANDS" [tool.poe.tasks.format] help = "Format the codebase" -cmd = "ruff format" +cmd = "ruff format --config=pyproject.toml" [tool.poe.tasks.format-docs] help = "Format the documentation" @@ -234,7 +234,7 @@ cmd = "docstrfmt docs *.rst" [tool.poe.tasks.lint] help = "Check the code for linting issues. Accepts ruff options." -cmd = "ruff check" +cmd = "ruff check --config=pyproject.toml" [tool.poe.tasks.lint-docs] help = "Lint the documentation" @@ -290,10 +290,11 @@ extend-exclude = [ ] [tool.ruff] -target-version = "py39" +target-version = "py310" line-length = 80 [tool.ruff.lint] +future-annotations = true select = [ # "ARG", # flake8-unused-arguments # "C4", # flake8-comprehensions @@ -305,9 +306,9 @@ select = [ "ISC", # flake8-implicit-str-concat "N", # pep8-naming "PT", # flake8-pytest-style - # "RUF", # ruff + "RUF", # ruff "UP", # pyupgrade - "TCH", # flake8-type-checking + "TC", # flake8-type-checking "W", # pycodestyle ] ignore = [ @@ -319,6 +320,8 @@ ignore = [ "test/plugins/test_ftintitle.py" = ["E501"] "test/test_util.py" = ["E501"] "test/ui/test_field_diff.py" = ["E501"] +"test/util/test_id_extractors.py" = ["E501"] +"test/**" = ["RUF001"] # we use Unicode characters in tests [tool.ruff.lint.isort] split-on-trailing-comma = false diff --git a/test/autotag/test_distance.py b/test/autotag/test_distance.py index 3686f82c9..ac0864564 100644 --- a/test/autotag/test_distance.py +++ b/test/autotag/test_distance.py @@ -337,15 +337,15 @@ class TestDataSourceDistance: _p("Original", "Original", 0.5, 1.0, True, MATCH, id="match"), _p("Original", "Other", 0.5, 1.0, True, MISMATCH, id="mismatch"), _p("Other", "Original", 0.5, 1.0, True, MISMATCH, id="mismatch"), - _p("Original", "unknown", 0.5, 1.0, True, MISMATCH, id="mismatch-unknown"), # noqa: E501 - _p("Original", None, 0.5, 1.0, True, MISMATCH, id="mismatch-no-info"), # noqa: E501 + _p("Original", "unknown", 0.5, 1.0, True, MISMATCH, id="mismatch-unknown"), + _p("Original", None, 0.5, 1.0, True, MISMATCH, id="mismatch-no-info"), _p(None, "Other", 0.5, 1.0, True, MISMATCH, id="mismatch-no-original-multiple-sources"), # noqa: E501 _p(None, "Other", 0.5, 1.0, False, MATCH, id="match-no-original-but-single-source"), # noqa: E501 _p("unknown", "unknown", 0.5, 1.0, True, MATCH, id="match-unknown"), - _p("Original", "Other", 1.0, 1.0, True, 0.25, id="mismatch-max-penalty"), # noqa: E501 - _p("Original", "Other", 0.5, 5.0, True, 0.3125, id="mismatch-high-weight"), # noqa: E501 - _p("Original", "Other", 0.0, 1.0, True, MATCH, id="match-no-penalty"), # noqa: E501 - _p("Original", "Other", 0.5, 0.0, True, MATCH, id="match-no-weight"), # noqa: E501 + _p("Original", "Other", 1.0, 1.0, True, 0.25, id="mismatch-max-penalty"), + _p("Original", "Other", 0.5, 5.0, True, 0.3125, id="mismatch-high-weight"), + _p("Original", "Other", 0.0, 1.0, True, MATCH, id="match-no-penalty"), + _p("Original", "Other", 0.5, 0.0, True, MATCH, id="match-no-weight"), ], ) # fmt: skip def test_distance(self, item, info, expected_distance): diff --git a/test/plugins/lyrics_pages.py b/test/plugins/lyrics_pages.py index 15cb812a1..047b6e443 100644 --- a/test/plugins/lyrics_pages.py +++ b/test/plugins/lyrics_pages.py @@ -24,7 +24,7 @@ class LyricsPage(NamedTuple): artist: str = "The Beatles" track_title: str = "Lady Madonna" url_title: str | None = None # only relevant to the Google backend - marks: list[str] = [] # markers for pytest.param + marks: list[str] = [] # markers for pytest.param # noqa: RUF012 def __str__(self) -> str: """Return name of this test case.""" diff --git a/test/plugins/test_albumtypes.py b/test/plugins/test_albumtypes.py index 0a9d53349..371bf0415 100644 --- a/test/plugins/test_albumtypes.py +++ b/test/plugins/test_albumtypes.py @@ -14,12 +14,17 @@ """Tests for the 'albumtypes' plugin.""" -from collections.abc import Sequence +from __future__ import annotations + +from typing import TYPE_CHECKING from beets.test.helper import PluginTestCase from beetsplug.albumtypes import AlbumTypesPlugin from beetsplug.musicbrainz import VARIOUS_ARTISTS_ID +if TYPE_CHECKING: + from collections.abc import Sequence + class AlbumTypesPluginTest(PluginTestCase): """Tests for albumtypes plugin.""" diff --git a/test/plugins/test_aura.py b/test/plugins/test_aura.py index 7e840008e..188c44c9e 100644 --- a/test/plugins/test_aura.py +++ b/test/plugins/test_aura.py @@ -1,13 +1,17 @@ +from __future__ import annotations + import os from http import HTTPStatus from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import pytest -from flask.testing import Client from beets.test.helper import TestHelper +if TYPE_CHECKING: + from flask.testing import Client + @pytest.fixture(scope="session", autouse=True) def helper(): diff --git a/test/plugins/test_bpd.py b/test/plugins/test_bpd.py index 16e424d7e..157569bbe 100644 --- a/test/plugins/test_bpd.py +++ b/test/plugins/test_bpd.py @@ -22,6 +22,7 @@ import threading import time import unittest from contextlib import contextmanager +from typing import ClassVar from unittest.mock import MagicMock, patch import confuse @@ -837,7 +838,7 @@ class BPDQueueTest(BPDTestHelper): fail=True, ) - METADATA = {"Pos", "Time", "Id", "file", "duration"} + METADATA: ClassVar[set[str]] = {"Pos", "Time", "Id", "file", "duration"} def test_cmd_add(self): with self.run_bpd() as client: @@ -1032,7 +1033,7 @@ class BPDConnectionTest(BPDTestHelper): } ) - ALL_MPD_TAGTYPES = { + ALL_MPD_TAGTYPES: ClassVar[set[str]] = { "Artist", "ArtistSort", "Album", @@ -1057,7 +1058,7 @@ class BPDConnectionTest(BPDTestHelper): "MUSICBRAINZ_RELEASETRACKID", "MUSICBRAINZ_WORKID", } - UNSUPPORTED_TAGTYPES = { + UNSUPPORTED_TAGTYPES: ClassVar[set[str]] = { "MUSICBRAINZ_WORKID", # not tracked by beets "Performer", # not tracked by beets "AlbumSort", # not tracked by beets diff --git a/test/plugins/test_convert.py b/test/plugins/test_convert.py index 9ae0ebf6d..2a1a3b94d 100644 --- a/test/plugins/test_convert.py +++ b/test/plugins/test_convert.py @@ -11,14 +11,14 @@ # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. - +from __future__ import annotations import fnmatch import os.path import re import sys import unittest -from pathlib import Path +from typing import TYPE_CHECKING import pytest from mediafile import MediaFile @@ -35,6 +35,9 @@ from beets.test.helper import ( ) from beetsplug import convert +if TYPE_CHECKING: + from pathlib import Path + def shell_quote(text): import shlex diff --git a/test/plugins/test_edit.py b/test/plugins/test_edit.py index d0e03d0e5..564b2ff1a 100644 --- a/test/plugins/test_edit.py +++ b/test/plugins/test_edit.py @@ -13,6 +13,7 @@ # included in all copies or substantial portions of the Software. import codecs +from typing import ClassVar from unittest.mock import patch from beets.dbcore.query import TrueQuery @@ -319,7 +320,7 @@ class EditDuringImporterTestCase( matching = AutotagStub.GOOD - IGNORED = ["added", "album_id", "id", "mtime", "path"] + IGNORED: ClassVar[list[str]] = ["added", "album_id", "id", "mtime", "path"] def setUp(self): super().setUp() @@ -350,8 +351,8 @@ class EditDuringImporterNonSingletonTest(EditDuringImporterTestCase): self.lib.items(), self.items_orig, ["title"], - self.IGNORED - + [ + [ + *self.IGNORED, "albumartist", "mb_albumartistid", "mb_albumartistids", @@ -378,7 +379,7 @@ class EditDuringImporterNonSingletonTest(EditDuringImporterTestCase): self.lib.items(), self.items_orig, [], - self.IGNORED + ["albumartist", "mb_albumartistid"], + [*self.IGNORED, "albumartist", "mb_albumartistid"], ) assert all("Tag Track" in i.title for i in self.lib.items()) @@ -490,6 +491,6 @@ class EditDuringImporterSingletonTest(EditDuringImporterTestCase): self.lib.items(), self.items_orig, ["title"], - self.IGNORED + ["albumartist", "mb_albumartistid"], + [*self.IGNORED, "albumartist", "mb_albumartistid"], ) assert all("Edited Track" in i.title for i in self.lib.items()) diff --git a/test/plugins/test_ftintitle.py b/test/plugins/test_ftintitle.py index 51bd4f9c8..aff4dda18 100644 --- a/test/plugins/test_ftintitle.py +++ b/test/plugins/test_ftintitle.py @@ -14,15 +14,21 @@ """Tests for the 'ftintitle' plugin.""" -from collections.abc import Generator -from typing import TypeAlias +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeAlias import pytest -from beets.library.models import Album, Item +from beets.library.models import Album from beets.test.helper import PluginTestCase from beetsplug import ftintitle +if TYPE_CHECKING: + from collections.abc import Generator + + from beets.library.models import Item + ConfigValue: TypeAlias = str | bool | list[str] diff --git a/test/plugins/test_hook.py b/test/plugins/test_hook.py index 033e1ea64..d47162666 100644 --- a/test/plugins/test_hook.py +++ b/test/plugins/test_hook.py @@ -19,7 +19,7 @@ import os import sys import unittest from contextlib import contextmanager -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar from beets import plugins from beets.test.helper import PluginTestCase, capture_log @@ -70,7 +70,7 @@ class HookLogsTest(HookTestCase): class HookCommandTest(HookTestCase): - EVENTS: list[plugins.EventType] = ["write", "after_write"] + EVENTS: ClassVar[list[plugins.EventType]] = ["write", "after_write"] def setUp(self): super().setUp() diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py index 945a7158c..376f0b9f2 100644 --- a/test/plugins/test_lyrics.py +++ b/test/plugins/test_lyrics.py @@ -14,11 +14,13 @@ """Tests for the 'lyrics' plugin.""" +from __future__ import annotations + import re import textwrap from functools import partial from http import HTTPStatus -from pathlib import Path +from typing import TYPE_CHECKING import pytest @@ -26,7 +28,12 @@ from beets.library import Item from beets.test.helper import PluginMixin, TestHelper from beetsplug import lyrics -from .lyrics_pages import LyricsPage, lyrics_pages +from .lyrics_pages import lyrics_pages + +if TYPE_CHECKING: + from pathlib import Path + + from .lyrics_pages import LyricsPage PHRASE_BY_TITLE = { "Lady Madonna": "friday night arrives without a suitcase", @@ -424,7 +431,7 @@ class TestTekstowoLyrics(LyricsBackendTest): [ ("tekstowopl/piosenka24kgoldncityofangels1", True), ( - "tekstowopl/piosenkabeethovenbeethovenpianosonata17tempestthe3rdmovement", # noqa: E501 + "tekstowopl/piosenkabeethovenbeethovenpianosonata17tempestthe3rdmovement", False, ), ], @@ -607,7 +614,7 @@ class TestTranslation: [00:00:50] [00:01.00] Some more synced lyrics / Quelques paroles plus synchronisées - Source: https://lrclib.net/api/123""", # noqa: E501 + Source: https://lrclib.net/api/123""", id="synced", ), pytest.param( diff --git a/test/plugins/test_mbpseudo.py b/test/plugins/test_mbpseudo.py index 6b382ab16..2fb6321b3 100644 --- a/test/plugins/test_mbpseudo.py +++ b/test/plugins/test_mbpseudo.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import json -import pathlib from copy import deepcopy +from typing import TYPE_CHECKING import pytest @@ -9,13 +11,17 @@ from beets.autotag.distance import Distance from beets.autotag.hooks import AlbumInfo, TrackInfo from beets.library import Item from beets.test.helper import PluginMixin -from beetsplug._typing import JSONDict from beetsplug.mbpseudo import ( _STATUS_PSEUDO, MusicBrainzPseudoReleasePlugin, PseudoAlbumInfo, ) +if TYPE_CHECKING: + import pathlib + + from beetsplug._typing import JSONDict + @pytest.fixture(scope="module") def rsrc_dir(pytestconfig: pytest.Config): diff --git a/test/plugins/test_mpdstats.py b/test/plugins/test_mpdstats.py index 6f5d3f3ce..def1f77b2 100644 --- a/test/plugins/test_mpdstats.py +++ b/test/plugins/test_mpdstats.py @@ -13,6 +13,7 @@ # included in all copies or substantial portions of the Software. +from typing import Any, ClassVar from unittest.mock import ANY, Mock, call, patch from beets import util @@ -46,9 +47,8 @@ class MPDStatsTest(PluginTestCase): assert mpdstats.get_item("/some/non-existing/path") is None assert "item not found:" in log.info.call_args[0][0] - FAKE_UNKNOWN_STATE = "some-unknown-one" - STATUSES = [ - {"state": FAKE_UNKNOWN_STATE}, + STATUSES: ClassVar[list[dict[str, Any]]] = [ + {"state": "some-unknown-one"}, {"state": "pause"}, {"state": "play", "songid": 1, "time": "0:1"}, {"state": "stop"}, diff --git a/test/plugins/test_musicbrainz.py b/test/plugins/test_musicbrainz.py index 733287204..f21c03c97 100644 --- a/test/plugins/test_musicbrainz.py +++ b/test/plugins/test_musicbrainz.py @@ -15,6 +15,7 @@ """Tests for MusicBrainz API wrapper.""" import unittest +from typing import ClassVar from unittest import mock import pytest @@ -1017,7 +1018,11 @@ class TestMusicBrainzPlugin(PluginMixin): plugin = "musicbrainz" mbid = "d2a6f856-b553-40a0-ac54-a321e8e2da99" - RECORDING = {"title": "foo", "id": "bar", "length": 42} + RECORDING: ClassVar[dict[str, int | str]] = { + "title": "foo", + "id": "bar", + "length": 42, + } @pytest.fixture def plugin_config(self): diff --git a/test/plugins/test_random.py b/test/plugins/test_random.py index 9bcf8e59b..cb21edf47 100644 --- a/test/plugins/test_random.py +++ b/test/plugins/test_random.py @@ -72,8 +72,8 @@ class RandomTest(TestHelper, unittest.TestCase): print(f"{i:2d} {'*' * positions.count(i)}") return self._stats(positions) - mean1, stdev1, median1 = experiment("artist") - mean2, stdev2, median2 = experiment("track") + _, stdev1, median1 = experiment("artist") + _, stdev2, median2 = experiment("track") assert 0 == pytest.approx(median1, abs=1) assert len(self.items) // 2 == pytest.approx(median2, abs=1) assert stdev2 > stdev1 diff --git a/test/test_art_resize.py b/test/test_art_resize.py index 0ccbb0eae..55deb8cb6 100644 --- a/test/test_art_resize.py +++ b/test/test_art_resize.py @@ -136,7 +136,8 @@ class ArtResizerFileSizeTest(CleanupModulesMixin, BeetsTestCase): """ im = IMBackend() path = im.deinterlace(self.IMG_225x225) - cmd = im.identify_cmd + [ + cmd = [ + *im.identify_cmd, "-format", "%[interlace]", syspath(path, prefix=False), diff --git a/test/test_dbcore.py b/test/test_dbcore.py index 653adf298..b73bca818 100644 --- a/test/test_dbcore.py +++ b/test/test_dbcore.py @@ -19,6 +19,7 @@ import shutil import sqlite3 import unittest from tempfile import mkstemp +from typing import ClassVar import pytest @@ -57,13 +58,13 @@ class QueryFixture(dbcore.query.FieldQuery): class ModelFixture1(LibModel): _table = "test" _flex_table = "testflex" - _fields = { + _fields: ClassVar[dict[str, dbcore.types.Type]] = { "id": dbcore.types.PRIMARY_ID, "field_one": dbcore.types.INTEGER, "field_two": dbcore.types.STRING, } - _sorts = { + _sorts: ClassVar[dict[str, type[dbcore.query.FieldSort]]] = { "some_sort": SortFixture, } @@ -92,7 +93,7 @@ class DatabaseFixture1(dbcore.Database): class ModelFixture2(ModelFixture1): - _fields = { + _fields: ClassVar[dict[str, dbcore.types.Type]] = { "id": dbcore.types.PRIMARY_ID, "field_one": dbcore.types.INTEGER, "field_two": dbcore.types.INTEGER, @@ -104,7 +105,7 @@ class DatabaseFixture2(dbcore.Database): class ModelFixture3(ModelFixture1): - _fields = { + _fields: ClassVar[dict[str, dbcore.types.Type]] = { "id": dbcore.types.PRIMARY_ID, "field_one": dbcore.types.INTEGER, "field_two": dbcore.types.INTEGER, @@ -117,7 +118,7 @@ class DatabaseFixture3(dbcore.Database): class ModelFixture4(ModelFixture1): - _fields = { + _fields: ClassVar[dict[str, dbcore.types.Type]] = { "id": dbcore.types.PRIMARY_ID, "field_one": dbcore.types.INTEGER, "field_two": dbcore.types.INTEGER, @@ -133,14 +134,14 @@ class DatabaseFixture4(dbcore.Database): class AnotherModelFixture(ModelFixture1): _table = "another" _flex_table = "anotherflex" - _fields = { + _fields: ClassVar[dict[str, dbcore.types.Type]] = { "id": dbcore.types.PRIMARY_ID, "foo": dbcore.types.INTEGER, } class ModelFixture5(ModelFixture1): - _fields = { + _fields: ClassVar[dict[str, dbcore.types.Type]] = { "some_string_field": dbcore.types.STRING, "some_float_field": dbcore.types.FLOAT, "some_boolean_field": dbcore.types.BOOLEAN, @@ -411,7 +412,7 @@ class ModelTest(unittest.TestCase): def test_computed_field(self): model = ModelFixtureWithGetters() assert model.aComputedField == "thing" - with pytest.raises(KeyError, match="computed field .+ deleted"): + with pytest.raises(KeyError, match=r"computed field .+ deleted"): del model.aComputedField def test_items(self): diff --git a/test/test_library.py b/test/test_library.py index 7c0529001..4acf34746 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -1056,7 +1056,7 @@ class PathStringTest(BeetsTestCase): assert isinstance(self.i.path, bytes) def test_fetched_item_path_is_bytestring(self): - i = list(self.lib.items())[0] + i = next(iter(self.lib.items())) assert isinstance(i.path, bytes) def test_unicode_path_becomes_bytestring(self): @@ -1070,14 +1070,14 @@ class PathStringTest(BeetsTestCase): """, (self.i.id, "somepath"), ) - i = list(self.lib.items())[0] + i = next(iter(self.lib.items())) assert isinstance(i.path, bytes) def test_special_chars_preserved_in_database(self): path = "b\xe1r".encode() self.i.path = path self.i.store() - i = list(self.lib.items())[0] + i = next(iter(self.lib.items())) assert i.path == path def test_special_char_path_added_to_database(self): @@ -1086,7 +1086,7 @@ class PathStringTest(BeetsTestCase): i = item() i.path = path self.lib.add(i) - i = list(self.lib.items())[0] + i = next(iter(self.lib.items())) assert i.path == path def test_destination_returns_bytestring(self): diff --git a/test/test_plugins.py b/test/test_plugins.py index 6f7026718..53f24c13d 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -19,6 +19,7 @@ import logging import os import pkgutil import sys +from typing import ClassVar from unittest.mock import ANY, Mock, patch import pytest @@ -46,7 +47,7 @@ from beets.util import PromptChoice, displayable_path, syspath class TestPluginRegistration(PluginTestCase): class RatingPlugin(plugins.BeetsPlugin): - item_types = { + item_types: ClassVar[dict[str, types.Type]] = { "rating": types.Float(), "multi_value": types.MULTI_VALUE_DSV, } @@ -70,7 +71,9 @@ class TestPluginRegistration(PluginTestCase): def test_duplicate_type(self): class DuplicateTypePlugin(plugins.BeetsPlugin): - item_types = {"rating": types.INTEGER} + item_types: ClassVar[dict[str, types.Type]] = { + "rating": types.INTEGER + } self.register_plugin(DuplicateTypePlugin) with pytest.raises( @@ -308,7 +311,9 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): "Enter search", "enter Id", "aBort", - ) + ("Foo", "baR") + "Foo", + "baR", + ) self.importer.add_choice(Action.SKIP) self.importer.run() @@ -342,7 +347,9 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): "Enter search", "enter Id", "aBort", - ) + ("Foo", "baR") + "Foo", + "baR", + ) config["import"]["singletons"] = True self.importer.add_choice(Action.SKIP) @@ -381,7 +388,8 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): "Enter search", "enter Id", "aBort", - ) + ("baZ",) + "baZ", + ) self.importer.add_choice(Action.SKIP) self.importer.run() self.mock_input_options.assert_called_once_with( @@ -416,7 +424,8 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): "Enter search", "enter Id", "aBort", - ) + ("Foo",) + "Foo", + ) # DummyPlugin.foo() should be called once with patch.object(DummyPlugin, "foo", autospec=True) as mock_foo: @@ -458,7 +467,8 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): "Enter search", "enter Id", "aBort", - ) + ("Foo",) + "Foo", + ) # DummyPlugin.foo() should be called once with helper.control_stdin("f\n"): diff --git a/test/ui/commands/test_completion.py b/test/ui/commands/test_completion.py index f1e53f238..ee2881a0e 100644 --- a/test/ui/commands/test_completion.py +++ b/test/ui/commands/test_completion.py @@ -56,7 +56,7 @@ class CompletionTest(IOMixin, TestPluginTestCase): test_script_name = os.path.join(_common.RSRC, b"test_completion.sh") with open(test_script_name, "rb") as test_script_file: tester.stdin.writelines(test_script_file) - out, err = tester.communicate() + out, _ = tester.communicate() assert tester.returncode == 0 assert out == b"completion tests passed\n", ( "test/test_completion.sh did not execute properly. " diff --git a/test/ui/commands/test_modify.py b/test/ui/commands/test_modify.py index b9cc1524d..77d378032 100644 --- a/test/ui/commands/test_modify.py +++ b/test/ui/commands/test_modify.py @@ -190,27 +190,23 @@ class ModifyTest(BeetsTestCase): assert mediafile.initial_key is None def test_arg_parsing_colon_query(self): - (query, mods, dels) = modify_parse_args( - ["title:oldTitle", "title=newTitle"] - ) + query, mods, _ = modify_parse_args(["title:oldTitle", "title=newTitle"]) assert query == ["title:oldTitle"] assert mods == {"title": "newTitle"} def test_arg_parsing_delete(self): - (query, mods, dels) = modify_parse_args(["title:oldTitle", "title!"]) + query, _, dels = modify_parse_args(["title:oldTitle", "title!"]) assert query == ["title:oldTitle"] assert dels == ["title"] def test_arg_parsing_query_with_exclaimation(self): - (query, mods, dels) = modify_parse_args( + query, mods, _ = modify_parse_args( ["title:oldTitle!", "title=newTitle!"] ) assert query == ["title:oldTitle!"] assert mods == {"title": "newTitle!"} def test_arg_parsing_equals_in_value(self): - (query, mods, dels) = modify_parse_args( - ["title:foo=bar", "title=newTitle"] - ) + query, mods, _ = modify_parse_args(["title:foo=bar", "title=newTitle"]) assert query == ["title:foo=bar"] assert mods == {"title": "newTitle"} diff --git a/test/ui/commands/test_utils.py b/test/ui/commands/test_utils.py index bd07a27c7..075f522a7 100644 --- a/test/ui/commands/test_utils.py +++ b/test/ui/commands/test_utils.py @@ -19,7 +19,7 @@ class QueryTest(BeetsTestCase): ) item = library.Item.from_path(itempath) self.lib.add(item) - return item, itempath + return item def add_album(self, items): album = self.lib.add_album(items) @@ -47,13 +47,13 @@ class QueryTest(BeetsTestCase): self.check_do_query(2, 0, album=False) def test_query_album(self): - item, itempath = self.add_item() + item = self.add_item() self.add_album([item]) self.check_do_query(1, 1, album=True) self.check_do_query(0, 1, album=True, also_items=False) - item, itempath = self.add_item() - item2, itempath = self.add_item() + item = self.add_item() + item2 = self.add_item() self.add_album([item, item2]) self.check_do_query(3, 2, album=True) self.check_do_query(0, 2, album=True, also_items=False) diff --git a/test/ui/test_ui.py b/test/ui/test_ui.py index a37d4bb29..a0bf2e598 100644 --- a/test/ui/test_ui.py +++ b/test/ui/test_ui.py @@ -374,7 +374,7 @@ class ShowModelChangeTest(IOMixin, unittest.TestCase): def test_both_values_shown(self): self.a.title = "foo" self.b.title = "bar" - change, out = self._show() + _, out = self._show() assert "foo" in out assert "bar" in out diff --git a/test/util/test_id_extractors.py b/test/util/test_id_extractors.py index 4918b4361..e510dd5d8 100644 --- a/test/util/test_id_extractors.py +++ b/test/util/test_id_extractors.py @@ -10,26 +10,26 @@ from beets.util.id_extractors import extract_release_id [ ("spotify", "39WqpoPgZxygo6YQjehLJJ", "39WqpoPgZxygo6YQjehLJJ"), ("spotify", "blah blah", None), - ("spotify", "https://open.spotify.com/album/39WqpoPgZxygo6YQjehLJJ", "39WqpoPgZxygo6YQjehLJJ"), # noqa: E501 + ("spotify", "https://open.spotify.com/album/39WqpoPgZxygo6YQjehLJJ", "39WqpoPgZxygo6YQjehLJJ"), ("deezer", "176356382", "176356382"), ("deezer", "blah blah", None), ("deezer", "https://www.deezer.com/album/176356382", "176356382"), ("beatport", "3089651", "3089651"), ("beatport", "blah blah", None), - ("beatport", "https://www.beatport.com/release/album-name/3089651", "3089651"), # noqa: E501 - ("discogs", "http://www.discogs.com/G%C3%BCnther-Lause-Meru-Ep/release/4354798", "4354798"), # noqa: E501 - ("discogs", "http://www.discogs.com/release/4354798-G%C3%BCnther-Lause-Meru-Ep", "4354798"), # noqa: E501 - ("discogs", "http://www.discogs.com/G%C3%BCnther-4354798Lause-Meru-Ep/release/4354798", "4354798"), # noqa: E501 - ("discogs", "http://www.discogs.com/release/4354798-G%C3%BCnther-4354798Lause-Meru-Ep/", "4354798"), # noqa: E501 + ("beatport", "https://www.beatport.com/release/album-name/3089651", "3089651"), + ("discogs", "http://www.discogs.com/G%C3%BCnther-Lause-Meru-Ep/release/4354798", "4354798"), + ("discogs", "http://www.discogs.com/release/4354798-G%C3%BCnther-Lause-Meru-Ep", "4354798"), + ("discogs", "http://www.discogs.com/G%C3%BCnther-4354798Lause-Meru-Ep/release/4354798", "4354798"), + ("discogs", "http://www.discogs.com/release/4354798-G%C3%BCnther-4354798Lause-Meru-Ep/", "4354798"), ("discogs", "[r4354798]", "4354798"), ("discogs", "r4354798", "4354798"), ("discogs", "4354798", "4354798"), ("discogs", "yet-another-metadata-provider.org/foo/12345", None), ("discogs", "005b84a0-ecd6-39f1-b2f6-6eb48756b268", None), - ("musicbrainz", "28e32c71-1450-463e-92bf-e0a46446fc11", "28e32c71-1450-463e-92bf-e0a46446fc11"), # noqa: E501 + ("musicbrainz", "28e32c71-1450-463e-92bf-e0a46446fc11", "28e32c71-1450-463e-92bf-e0a46446fc11"), ("musicbrainz", "blah blah", None), - ("musicbrainz", "https://musicbrainz.org/entity/28e32c71-1450-463e-92bf-e0a46446fc11", "28e32c71-1450-463e-92bf-e0a46446fc11"), # noqa: E501 - ("bandcamp", "https://nameofartist.bandcamp.com/album/nameofalbum", "https://nameofartist.bandcamp.com/album/nameofalbum"), # noqa: E501 + ("musicbrainz", "https://musicbrainz.org/entity/28e32c71-1450-463e-92bf-e0a46446fc11", "28e32c71-1450-463e-92bf-e0a46446fc11"), + ("bandcamp", "https://nameofartist.bandcamp.com/album/nameofalbum", "https://nameofartist.bandcamp.com/album/nameofalbum"), ], ) # fmt: skip def test_extract_release_id(source, id_string, expected):