From 5677f9beee189791a7d2eef91a07a9bc3ef635a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 13 Jul 2025 20:06:34 +0100 Subject: [PATCH 01/12] Fix breaking issues --- beets/dbcore/query.py | 3 ++- beets/ui/commands.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index ae8e0ddf6..49d7f6428 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -28,6 +28,7 @@ from re import Pattern from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union from beets import util +from beets.util.units import raw_seconds_short if TYPE_CHECKING: from beets.dbcore.db import AnyModel, Model @@ -892,7 +893,7 @@ class DurationQuery(NumericQuery): if not s: return None try: - return util.raw_seconds_short(s) + return raw_seconds_short(s) except ValueError: try: return float(s) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 7b22c2462..12a8d6875 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1343,7 +1343,7 @@ def import_func(lib, opts, args: list[str]): if opts.library: query = args - paths = [] + byte_paths = [] else: query = None paths = args From 21459c70ee0c303d8ad51a8fb62df65e52a81e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 13 Jul 2025 20:35:46 +0100 Subject: [PATCH 02/12] importer: provides search_ids into lookup_candidates explicitly --- beets/importer/stages.py | 4 +--- beets/importer/tasks.py | 30 ++++++++++++++---------------- test/test_importer.py | 8 ++++---- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/beets/importer/stages.py b/beets/importer/stages.py index 5b3540db4..137bfcbc4 100644 --- a/beets/importer/stages.py +++ b/beets/importer/stages.py @@ -143,9 +143,7 @@ def lookup_candidates(session: ImportSession, task: ImportTask): # Restrict the initial lookup to IDs specified by the user via the -m # option. Currently all the IDs are passed onto the tasks directly. - task.search_ids = session.config["search_ids"].as_str_seq() - - task.lookup_candidates() + task.lookup_candidates(session.config["search_ids"].as_str_seq()) @pipeline.stage diff --git a/beets/importer/tasks.py b/beets/importer/tasks.py index 441224b6b..e4b94bf95 100644 --- a/beets/importer/tasks.py +++ b/beets/importer/tasks.py @@ -32,6 +32,8 @@ from beets.dbcore.query import PathQuery from .state import ImportState if TYPE_CHECKING: + from beets.autotag.match import Recommendation + from .session import ImportSession # Global logger. @@ -159,6 +161,7 @@ class ImportTask(BaseImportTask): cur_album: str | None = None cur_artist: str | None = None candidates: Sequence[autotag.AlbumMatch | autotag.TrackMatch] = [] + rec: Recommendation | None = None def __init__( self, @@ -167,11 +170,9 @@ class ImportTask(BaseImportTask): items: Iterable[library.Item] | None, ): super().__init__(toppath, paths, items) - self.rec = None self.should_remove_duplicates = False self.should_merge_duplicates = False self.is_album = True - self.search_ids = [] # user-supplied candidate IDs. def set_choice( self, choice: Action | autotag.AlbumMatch | autotag.TrackMatch @@ -356,18 +357,15 @@ class ImportTask(BaseImportTask): tasks = [t for inner in tasks for t in inner] return tasks - def lookup_candidates(self): - """Retrieve and store candidates for this album. User-specified - candidate IDs are stored in self.search_ids: if present, the - initial lookup is restricted to only those IDs. + def lookup_candidates(self, search_ids: list[str]) -> None: + """Retrieve and store candidates for this album. + + If User-specified ``search_ids`` list is not empty, the lookup is + restricted to only those IDs. """ - artist, album, prop = autotag.tag_album( - self.items, search_ids=self.search_ids + self.cur_artist, self.cur_album, (self.candidates, self.rec) = ( + autotag.tag_album(self.items, search_ids=search_ids) ) - self.cur_artist = artist - self.cur_album = album - self.candidates = prop.candidates - self.rec = prop.recommendation def find_duplicates(self, lib: library.Library): """Return a list of albums from `lib` with the same artist and @@ -695,10 +693,10 @@ class SingletonImportTask(ImportTask): for item in self.imported_items(): plugins.send("item_imported", lib=lib, item=item) - def lookup_candidates(self): - prop = autotag.tag_item(self.item, search_ids=self.search_ids) - self.candidates = prop.candidates - self.rec = prop.recommendation + def lookup_candidates(self, search_ids: list[str]) -> None: + self.candidates, self.rec = autotag.tag_item( + self.item, search_ids=search_ids + ) def find_duplicates(self, lib): """Return a list of items from `lib` that have the same artist diff --git a/test/test_importer.py b/test/test_importer.py index 14b163f73..c1768df3e 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -1627,9 +1627,9 @@ class ImportIdTest(ImportTestCase): task = importer.ImportTask( paths=self.import_dir, toppath="top path", items=[_common.item()] ) - task.search_ids = [self.ID_RELEASE_0, self.ID_RELEASE_1] - task.lookup_candidates() + task.lookup_candidates([self.ID_RELEASE_0, self.ID_RELEASE_1]) + assert {"VALID_RELEASE_0", "VALID_RELEASE_1"} == { c.info.album for c in task.candidates } @@ -1639,9 +1639,9 @@ class ImportIdTest(ImportTestCase): task = importer.SingletonImportTask( toppath="top path", item=_common.item() ) - task.search_ids = [self.ID_RECORDING_0, self.ID_RECORDING_1] - task.lookup_candidates() + task.lookup_candidates([self.ID_RECORDING_0, self.ID_RECORDING_1]) + assert {"VALID_RECORDING_0", "VALID_RECORDING_1"} == { c.info.title for c in task.candidates } From d3c64d8506370d0fcdfd42d679caa520fe43fb94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 13 Jul 2025 22:58:48 +0100 Subject: [PATCH 03/12] Add missing types to importer and pipeline --- beets/importer/stages.py | 1 + beets/importer/tasks.py | 65 ++++++++++++++++++++++------------------ beets/util/__init__.py | 1 + beets/util/pipeline.py | 4 ++- 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/beets/importer/stages.py b/beets/importer/stages.py index 137bfcbc4..24ff815f3 100644 --- a/beets/importer/stages.py +++ b/beets/importer/stages.py @@ -70,6 +70,7 @@ def query_tasks(session: ImportSession): Instead of finding files from the filesystem, a query is used to match items from the library. """ + task: ImportTask if session.config["singletons"]: # Search for items. for item in session.lib.items(session.query): diff --git a/beets/importer/tasks.py b/beets/importer/tasks.py index e4b94bf95..abe2ca8a9 100644 --- a/beets/importer/tasks.py +++ b/beets/importer/tasks.py @@ -22,7 +22,7 @@ import time from collections import defaultdict from enum import Enum from tempfile import mkdtemp -from typing import TYPE_CHECKING, Callable, Iterable, Sequence +from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence import mediafile @@ -367,7 +367,7 @@ class ImportTask(BaseImportTask): autotag.tag_album(self.items, search_ids=search_ids) ) - def find_duplicates(self, lib: library.Library): + def find_duplicates(self, lib: library.Library) -> list[library.Album]: """Return a list of albums from `lib` with the same artist and album name as the task. """ @@ -698,7 +698,7 @@ class SingletonImportTask(ImportTask): self.item, search_ids=search_ids ) - def find_duplicates(self, lib): + def find_duplicates(self, lib: library.Library) -> list[library.Item]: # type: ignore[override] # Need splitting Singleton and Album tasks into separate classes """Return a list of items from `lib` that have the same artist and title as the task. """ @@ -800,6 +800,11 @@ class SentinelImportTask(ImportTask): pass +ArchiveHandler = tuple[ + Callable[[util.StrPath], bool], Callable[[util.StrPath], Any] +] + + class ArchiveImportTask(SentinelImportTask): """An import task that represents the processing of an archive. @@ -825,13 +830,13 @@ class ArchiveImportTask(SentinelImportTask): if not os.path.isfile(path): return False - for path_test, _ in cls.handlers(): + for path_test, _ in cls.handlers: if path_test(os.fsdecode(path)): return True return False - @classmethod - def handlers(cls): + @util.cached_classproperty + def handlers(cls) -> list[ArchiveHandler]: """Returns a list of archive handlers. Each handler is a `(path_test, ArchiveClass)` tuple. `path_test` @@ -839,28 +844,27 @@ class ArchiveImportTask(SentinelImportTask): handled by `ArchiveClass`. `ArchiveClass` is a class that implements the same interface as `tarfile.TarFile`. """ - if not hasattr(cls, "_handlers"): - cls._handlers: list[tuple[Callable, ...]] = [] - from zipfile import ZipFile, is_zipfile + _handlers: list[ArchiveHandler] = [] + from zipfile import ZipFile, is_zipfile - cls._handlers.append((is_zipfile, ZipFile)) - import tarfile + _handlers.append((is_zipfile, ZipFile)) + import tarfile - cls._handlers.append((tarfile.is_tarfile, tarfile.open)) - try: - from rarfile import RarFile, is_rarfile - except ImportError: - pass - else: - cls._handlers.append((is_rarfile, RarFile)) - try: - from py7zr import SevenZipFile, is_7zfile - except ImportError: - pass - else: - cls._handlers.append((is_7zfile, SevenZipFile)) + _handlers.append((tarfile.is_tarfile, tarfile.open)) + try: + from rarfile import RarFile, is_rarfile + except ImportError: + pass + else: + _handlers.append((is_rarfile, RarFile)) + try: + from py7zr import SevenZipFile, is_7zfile + except ImportError: + pass + else: + _handlers.append((is_7zfile, SevenZipFile)) - return cls._handlers + return _handlers def cleanup(self, copy=False, delete=False, move=False): """Removes the temporary directory the archive was extracted to.""" @@ -877,7 +881,7 @@ class ArchiveImportTask(SentinelImportTask): """ assert self.toppath is not None, "toppath must be set" - for path_test, handler_class in self.handlers(): + for path_test, handler_class in self.handlers: if path_test(os.fsdecode(self.toppath)): break else: @@ -923,7 +927,7 @@ class ImportTaskFactory: self.imported = 0 # "Real" tasks created. self.is_archive = ArchiveImportTask.is_archive(util.syspath(toppath)) - def tasks(self): + def tasks(self) -> Iterable[ImportTask]: """Yield all import tasks for music found in the user-specified path `self.toppath`. Any necessary sentinel tasks are also produced. @@ -1112,7 +1116,10 @@ def albums_in_dir(path: util.PathBytes): a list of Items that is probably an album. Specifically, any folder containing any media files is an album. """ - collapse_pat = collapse_paths = collapse_items = None + collapse_paths: list[util.PathBytes] = [] + collapse_items: list[util.PathBytes] = [] + collapse_pat = None + ignore: list[str] = config["ignore"].as_str_seq() ignore_hidden: bool = config["ignore_hidden"].get(bool) @@ -1137,7 +1144,7 @@ def albums_in_dir(path: util.PathBytes): # proceed to process the current one. if collapse_items: yield collapse_paths, collapse_items - collapse_pat = collapse_paths = collapse_items = None + collapse_pat, collapse_paths, collapse_items = None, [], [] # Check whether this directory looks like the *first* directory # in a multi-disc sequence. There are two indicators: the file diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 00c9ce05d..a4b6ef3d6 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -63,6 +63,7 @@ MAX_FILENAME_LENGTH = 200 WINDOWS_MAGIC_PREFIX = "\\\\?\\" T = TypeVar("T") PathLike = Union[str, bytes, Path] +StrPath = Union[str, Path] Replacements = Sequence[tuple[Pattern[str], str]] # Here for now to allow for a easy replace later on diff --git a/beets/util/pipeline.py b/beets/util/pipeline.py index cebde0f23..140407f04 100644 --- a/beets/util/pipeline.py +++ b/beets/util/pipeline.py @@ -48,6 +48,8 @@ POISON = "__PIPELINE_POISON__" DEFAULT_QUEUE_SIZE = 16 +Tq = TypeVar("Tq") + def _invalidate_queue(q, val=None, sync=True): """Breaks a Queue such that it never blocks, always has size 1, @@ -91,7 +93,7 @@ def _invalidate_queue(q, val=None, sync=True): q.mutex.release() -class CountedQueue(queue.Queue): +class CountedQueue(queue.Queue[Tq]): """A queue that keeps track of the number of threads that are still feeding into it. The queue is poisoned when all threads are finished with the queue. From 816d06f160e3057690b98139f02eedced6debd70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 13 Jul 2025 23:18:32 +0100 Subject: [PATCH 04/12] Fix plugin types --- beetsplug/autobpm.py | 7 +++++-- beetsplug/lastgenre/__init__.py | 2 +- beetsplug/replaygain.py | 13 +++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/beetsplug/autobpm.py b/beetsplug/autobpm.py index 9c953f711..46d7e672a 100644 --- a/beetsplug/autobpm.py +++ b/beetsplug/autobpm.py @@ -15,10 +15,10 @@ from __future__ import annotations -from collections.abc import Iterable from typing import TYPE_CHECKING import librosa +import numpy as np from beets.plugins import BeetsPlugin from beets.ui import Subcommand, should_write @@ -76,7 +76,10 @@ class AutoBPMPlugin(BeetsPlugin): self._log.error("Failed to measure BPM for {}: {}", path, exc) continue - bpm = round(tempo[0] if isinstance(tempo, Iterable) else tempo) + bpm = round( + float(tempo[0] if isinstance(tempo, np.ndarray) else tempo) + ) + item["bpm"] = bpm self._log.info("Computed BPM for {}: {}", path, bpm) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index b67f1fae2..dbab96cf8 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -401,7 +401,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): label = "album" if not new_genres and "artist" in self.sources: - new_genres = None + new_genres = [] if isinstance(obj, library.Item): new_genres = self.fetch_artist_genre(obj) label = "artist" diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 00b651d99..96c854314 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -1161,7 +1161,9 @@ class ExceptionWatcher(Thread): Once an exception occurs, raise it and execute a callback. """ - def __init__(self, queue: queue.Queue, callback: Callable[[], None]): + def __init__( + self, queue: queue.Queue[Exception], callback: Callable[[], None] + ): self._queue = queue self._callback = callback self._stopevent = Event() @@ -1197,7 +1199,9 @@ BACKENDS: dict[str, type[Backend]] = {b.NAME: b for b in BACKEND_CLASSES} class ReplayGainPlugin(BeetsPlugin): """Provides ReplayGain analysis.""" - def __init__(self): + pool: ThreadPool | None = None + + def __init__(self) -> None: super().__init__() # default backend is 'command' for backward-compatibility. @@ -1261,9 +1265,6 @@ class ReplayGainPlugin(BeetsPlugin): except (ReplayGainError, FatalReplayGainError) as e: raise ui.UserError(f"replaygain initialization failed: {e}") - # Start threadpool lazily. - self.pool = None - def should_use_r128(self, item: Item) -> bool: """Checks the plugin setting to decide whether the calculation should be done using the EBU R128 standard and use R128_ tags instead. @@ -1420,7 +1421,7 @@ class ReplayGainPlugin(BeetsPlugin): """Open a `ThreadPool` instance in `self.pool`""" if self.pool is None and self.backend_instance.do_parallel: self.pool = ThreadPool(threads) - self.exc_queue: queue.Queue = queue.Queue() + self.exc_queue: queue.Queue[Exception] = queue.Queue() signal.signal(signal.SIGINT, self._interrupt) From 72003ba1929ae697f833add53dd169494cb1a38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 13 Jul 2025 23:19:51 +0100 Subject: [PATCH 05/12] Let mypy failure block CI --- .github/workflows/lint.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c9b66f402..7900d247d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -105,7 +105,6 @@ jobs: - name: Type check code uses: liskin/gh-problem-matcher-wrap@v3 - continue-on-error: true with: linters: mypy run: poe check-types --show-column-numbers --no-error-summary ${{ needs.changed-files.outputs.changed_python_files }} From a5bbe574908438b722f0bd0a9853f3cd7e84bf72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Tue, 15 Jul 2025 09:12:17 +0100 Subject: [PATCH 06/12] Fix types in test_player --- beetsplug/bpd/__init__.py | 39 +++++++++++---------------- test/plugins/test_player.py | 53 +++++++------------------------------ 2 files changed, 24 insertions(+), 68 deletions(-) diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index 435368e35..a2ad2835c 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -30,7 +30,7 @@ from typing import TYPE_CHECKING import beets import beets.ui -from beets import dbcore, vfs +from beets import dbcore, logging, vfs from beets.library import Item from beets.plugins import BeetsPlugin from beets.util import as_string, bluelet @@ -38,6 +38,17 @@ from beets.util import as_string, bluelet if TYPE_CHECKING: from beets.dbcore.query import Query +log = logging.getLogger(__name__) + + +try: + from . import gstplayer +except ImportError as e: + raise ImportError( + "Gstreamer Python bindings not found." + ' Install "gstreamer1.0" and "python-gi" or similar package to use BPD.' + ) from e + PROTOCOL_VERSION = "0.16.0" BUFSIZE = 1024 @@ -94,11 +105,6 @@ SUBSYSTEMS = [ ] -# Gstreamer import error. -class NoGstreamerError(Exception): - pass - - # Error-handling, exceptions, parameter parsing. @@ -1099,14 +1105,6 @@ class Server(BaseServer): """ def __init__(self, library, host, port, password, ctrl_port, log): - try: - from beetsplug.bpd import gstplayer - except ImportError as e: - # This is a little hacky, but it's the best I know for now. - if e.args[0].endswith(" gst"): - raise NoGstreamerError() - else: - raise log.info("Starting server...") super().__init__(host, port, password, ctrl_port, log) self.lib = library @@ -1616,16 +1614,9 @@ class BPDPlugin(BeetsPlugin): def start_bpd(self, lib, host, port, password, volume, ctrl_port): """Starts a BPD server.""" - try: - server = Server(lib, host, port, password, ctrl_port, self._log) - server.cmd_setvol(None, volume) - server.run() - except NoGstreamerError: - self._log.error("Gstreamer Python bindings not found.") - self._log.error( - 'Install "gstreamer1.0" and "python-gi"' - "or similar package to use BPD." - ) + server = Server(lib, host, port, password, ctrl_port, self._log) + server.cmd_setvol(None, volume) + server.run() def commands(self): cmd = beets.ui.Subcommand( diff --git a/test/plugins/test_player.py b/test/plugins/test_player.py index a7c613d8f..16e424d7e 100644 --- a/test/plugins/test_player.py +++ b/test/plugins/test_player.py @@ -14,19 +14,15 @@ """Tests for BPD's implementation of the MPD protocol.""" -import importlib.util import multiprocessing as mp import os import socket -import sys import tempfile import threading import time import unittest from contextlib import contextmanager - -# Mock GstPlayer so that the forked process doesn't attempt to import gi: -from unittest import mock +from unittest.mock import MagicMock, patch import confuse import pytest @@ -34,43 +30,8 @@ import yaml from beets.test.helper import PluginTestCase from beets.util import bluelet -from beetsplug import bpd -gstplayer = importlib.util.module_from_spec( - importlib.util.find_spec("beetsplug.bpd.gstplayer") -) - - -def _gstplayer_play(*_): - bpd.gstplayer._GstPlayer.playing = True - return mock.DEFAULT - - -gstplayer._GstPlayer = mock.MagicMock( - spec_set=[ - "time", - "volume", - "playing", - "run", - "play_file", - "pause", - "stop", - "seek", - "play", - "get_decoders", - ], - **{ - "playing": False, - "volume": 0, - "time.return_value": (0, 0), - "play_file.side_effect": _gstplayer_play, - "play.side_effect": _gstplayer_play, - "get_decoders.return_value": {"default": ({"audio/mpeg"}, {"mp3"})}, - }, -) -gstplayer.GstPlayer = lambda _: gstplayer._GstPlayer -sys.modules["beetsplug.bpd.gstplayer"] = gstplayer -bpd.gstplayer = gstplayer +bpd = pytest.importorskip("beetsplug.bpd") class CommandParseTest(unittest.TestCase): @@ -256,7 +217,7 @@ def implements(commands, fail=False): bluelet_listener = bluelet.Listener -@mock.patch("beets.util.bluelet.Listener") +@patch("beets.util.bluelet.Listener") def start_server(args, assigned_port, listener_patch): """Start the bpd server, writing the port to `assigned_port`.""" @@ -938,7 +899,7 @@ class BPDPlaylistsTest(BPDTestHelper): response = client.send_command("load", "anything") self._assert_failed(response, bpd.ERROR_NO_EXIST) - @unittest.skip + @unittest.expectedFailure def test_cmd_playlistadd(self): with self.run_bpd() as client: self._bpd_add(client, self.item1, playlist="anything") @@ -1128,7 +1089,7 @@ class BPDConnectionTest(BPDTestHelper): self._assert_ok(response) assert self.TAGTYPES == set(response.data["tagtype"]) - @unittest.skip + @unittest.expectedFailure def test_tagtypes_mask(self): with self.run_bpd() as client: response = client.send_command("tagtypes", "clear") @@ -1169,6 +1130,10 @@ class BPDReflectionTest(BPDTestHelper): fail=True, ) + @patch( + "beetsplug.bpd.gstplayer.GstPlayer.get_decoders", + MagicMock(return_value={"default": ({"audio/mpeg"}, {"mp3"})}), + ) def test_cmd_decoders(self): with self.run_bpd() as client: response = client.send_command("decoders") From e1162b7e0f00e46e6743f15adae738a0dc490360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Tue, 15 Jul 2025 09:12:57 +0100 Subject: [PATCH 07/12] Rename test_player -> test_bpd --- test/plugins/{test_player.py => test_bpd.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/plugins/{test_player.py => test_bpd.py} (100%) diff --git a/test/plugins/test_player.py b/test/plugins/test_bpd.py similarity index 100% rename from test/plugins/test_player.py rename to test/plugins/test_bpd.py From 46318a113c6b2ac0452cfc0e54c28bfba61065af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Tue, 15 Jul 2025 09:36:44 +0100 Subject: [PATCH 08/12] Install gstreamer1.0-plugins-base, python3-gst-1.0 for bpd --- .github/workflows/ci.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 390878372..f86cc103e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,7 @@ jobs: if: matrix.platform == 'ubuntu-latest' run: | sudo apt update - sudo apt install ffmpeg gobject-introspection libcairo2-dev libgirepository-2.0-dev pandoc imagemagick + sudo apt install ffmpeg gobject-introspection gstreamer1.0-plugins-base python3-gst-1.0 libcairo2-dev libgirepository-2.0-dev pandoc imagemagick - name: Get changed lyrics files id: lyrics-update diff --git a/pyproject.toml b/pyproject.toml index ea69240d5..39c543307 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,7 +124,7 @@ aura = ["flask", "flask-cors", "Pillow"] autobpm = ["librosa", "resampy"] # badfiles # mp3val and flac beatport = ["requests-oauthlib"] -bpd = ["PyGObject"] # python-gi and GStreamer 1.0+ +bpd = ["PyGObject"] # gobject-introspection, gstreamer1.0-plugins-base, python3-gst-1.0 chroma = ["pyacoustid"] # chromaprint or fpcalc # convert # ffmpeg docs = ["pydata-sphinx-theme", "sphinx"] From 2ac612b696949faf4b57740cc8acd715a727e83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Tue, 15 Jul 2025 10:06:55 +0100 Subject: [PATCH 09/12] Post coverage status comment --- codecov.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/codecov.yml b/codecov.yml index c4b333ad3..c899db06a 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,5 +1,6 @@ -# Don't post a comment on pull requests. -comment: off +comment: + layout: "condensed_header, condensed_files" + require_changes: true # Sets non-blocking status checks # https://docs.codecov.com/docs/commit-status#informational @@ -11,7 +12,7 @@ coverage: patch: default: informational: true - changes: no + changes: false github_checks: annotations: false From 3495a7ef364fbb4d5ba36d75eb39003fa03dfcb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Wed, 16 Jul 2025 14:00:29 +0100 Subject: [PATCH 10/12] Update mypy config Remove 'files' configuration from the config to make sure that mypy runs quicker when invoked from the editor (where ideally it should only check the file being edited). Remove 'disallow_any_generics' from the config since we already have this configured centrally. And finally, do not allow variable redefinitions. --- setup.cfg | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index e999b55d3..0b50485ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ markers = data_file = .reports/coverage/data branch = true relative_files = true -omit = +omit = beets/test/* beetsplug/_typing.py @@ -34,7 +34,6 @@ exclude_also = show_contexts = true [mypy] -files = beets,beetsplug,test,extra,docs allow_any_generics = false # FIXME: Would be better to actually type the libraries (if under our control), # or write our own stubs. For now, silence errors @@ -46,11 +45,8 @@ explicit_package_bases = true # config for all files. [[mypy-beets.plugins]] disallow_untyped_decorators = true -disallow_any_generics = true check_untyped_defs = true -allow_redefinition = true [[mypy-beets.metadata_plugins]] disallow_untyped_decorators = true -disallow_any_generics = true check_untyped_defs = true From 7509843517e70810fce1b16d80451ba8561fba38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Wed, 16 Jul 2025 14:06:06 +0100 Subject: [PATCH 11/12] Fix a generic type error and warnings in spotify --- beets/dbcore/types.py | 2 +- beetsplug/spotify.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/dbcore/types.py b/beets/dbcore/types.py index 30cabf42f..1b8434a0b 100644 --- a/beets/dbcore/types.py +++ b/beets/dbcore/types.py @@ -292,7 +292,7 @@ class DelimitedString(BaseString[list[str], list[str]]): containing delimiter-separated values. """ - model_type = list + model_type = list[str] def __init__(self, delimiter: str): self.delimiter = delimiter diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 7a4f4ec52..fa5dc5c52 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -162,7 +162,7 @@ class SpotifyPlugin( """Get the path to the JSON file for storing the OAuth token.""" return self.config["tokenfile"].get(confuse.Filename(in_app_dir=True)) - def _authenticate(self): + def _authenticate(self) -> None: """Request an access token via the Client Credentials Flow: https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow """ From ada672b759ee2356ac991c46398a7167e33efea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Wed, 16 Jul 2025 14:30:03 +0100 Subject: [PATCH 12/12] Do not install not required apt deps in ci --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f86cc103e..baeb52f18 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,7 @@ jobs: if: matrix.platform == 'ubuntu-latest' run: | sudo apt update - sudo apt install ffmpeg gobject-introspection gstreamer1.0-plugins-base python3-gst-1.0 libcairo2-dev libgirepository-2.0-dev pandoc imagemagick + sudo apt install --yes --no-install-recommends ffmpeg gobject-introspection gstreamer1.0-plugins-base python3-gst-1.0 libcairo2-dev libgirepository-2.0-dev pandoc imagemagick - name: Get changed lyrics files id: lyrics-update