Merge remote-tracking branch 'upstream/master' into dereference-symlinks-while-hardlinking

This commit is contained in:
Emi Katagiri-Simpson 2025-11-11 07:58:10 -05:00
commit 29a5b06f67
No known key found for this signature in database
45 changed files with 1676 additions and 1494 deletions

View file

@ -78,4 +78,6 @@ d93ddf8dd43e4f9ed072a03829e287c78d2570a2
# Moved ui.commands._utils into ui.commands.utils # Moved ui.commands._utils into ui.commands.utils
25ae330044abf04045e3f378f72bbaed739fb30d 25ae330044abf04045e3f378f72bbaed739fb30d
# Refactor test_ui_command.py into multiple modules # Refactor test_ui_command.py into multiple modules
a59e41a88365e414db3282658d2aa456e0b3468a a59e41a88365e414db3282658d2aa456e0b3468a
# pyupgrade Python 3.10
301637a1609831947cb5dd90270ed46c24b1ab1b

View file

@ -20,10 +20,10 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
platform: [ubuntu-latest, windows-latest] platform: [ubuntu-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.10", "3.11", "3.12", "3.13"]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
env: env:
IS_MAIN_PYTHON: ${{ matrix.python-version == '3.9' && matrix.platform == 'ubuntu-latest' }} IS_MAIN_PYTHON: ${{ matrix.python-version == '3.10' && matrix.platform == 'ubuntu-latest' }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Install Python tools - name: Install Python tools

View file

@ -3,6 +3,10 @@ on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: "0 0 * * SUN" # run every Sunday at midnight - cron: "0 0 * * SUN" # run every Sunday at midnight
env:
PYTHON_VERSION: "3.10"
jobs: jobs:
test_integration: test_integration:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -12,7 +16,7 @@ jobs:
uses: BrandonLWhite/pipx-install-action@v1.0.3 uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6 - uses: actions/setup-python@v6
with: with:
python-version: 3.9 python-version: ${{ env.PYTHON_VERSION }}
cache: poetry cache: poetry
- name: Install dependencies - name: Install dependencies

View file

@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env: env:
PYTHON_VERSION: 3.9 PYTHON_VERSION: "3.10"
jobs: jobs:
changed-files: changed-files:

View file

@ -8,7 +8,7 @@ on:
required: true required: true
env: env:
PYTHON_VERSION: 3.9 PYTHON_VERSION: "3.10"
NEW_VERSION: ${{ inputs.version }} NEW_VERSION: ${{ inputs.version }}
NEW_TAG: v${{ inputs.version }} NEW_TAG: v${{ inputs.version }}

View file

@ -124,12 +124,12 @@ command. Instead, you can activate the virtual environment in your shell with:
$ poetry shell $ poetry shell
You should see ``(beets-py3.9)`` prefix in your shell prompt. Now you can run You should see ``(beets-py3.10)`` prefix in your shell prompt. Now you can run
commands directly, for example: commands directly, for example:
:: ::
$ (beets-py3.9) pytest $ (beets-py3.10) pytest
Additionally, poethepoet_ task runner assists us with the most common Additionally, poethepoet_ task runner assists us with the most common
operations. Formatting, linting, testing are defined as ``poe`` tasks in operations. Formatting, linting, testing are defined as ``poe`` tasks in

View file

@ -18,7 +18,7 @@ from __future__ import annotations
import warnings import warnings
from importlib import import_module from importlib import import_module
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING
from beets import config, logging from beets import config, logging
@ -117,8 +117,8 @@ SPECIAL_FIELDS = {
def _apply_metadata( def _apply_metadata(
info: Union[AlbumInfo, TrackInfo], info: AlbumInfo | TrackInfo,
db_obj: Union[Album, Item], db_obj: Album | Item,
nullable_fields: Sequence[str] = [], nullable_fields: Sequence[str] = [],
): ):
"""Set the db_obj's metadata to match the info.""" """Set the db_obj's metadata to match the info."""

View file

@ -26,9 +26,16 @@ import threading
import time import time
from abc import ABC from abc import ABC
from collections import defaultdict from collections import defaultdict
from collections.abc import Generator, Iterable, Iterator, Mapping, Sequence from collections.abc import (
Callable,
Generator,
Iterable,
Iterator,
Mapping,
Sequence,
)
from sqlite3 import Connection, sqlite_version_info from sqlite3 import Connection, sqlite_version_info
from typing import TYPE_CHECKING, Any, AnyStr, Callable, Generic from typing import TYPE_CHECKING, Any, AnyStr, Generic
from typing_extensions import TypeVar # default value support from typing_extensions import TypeVar # default value support
from unidecode import unidecode from unidecode import unidecode

View file

@ -15,7 +15,7 @@ from __future__ import annotations
import os import os
import time import time
from typing import TYPE_CHECKING, Sequence from typing import TYPE_CHECKING
from beets import config, dbcore, library, logging, plugins, util from beets import config, dbcore, library, logging, plugins, util
from beets.importer.tasks import Action from beets.importer.tasks import Action
@ -25,6 +25,8 @@ from . import stages as stagefuncs
from .state import ImportState from .state import ImportState
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence
from beets.util import PathBytes from beets.util import PathBytes
from .tasks import ImportTask from .tasks import ImportTask

View file

@ -16,7 +16,7 @@ from __future__ import annotations
import itertools import itertools
import logging import logging
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING
from beets import config, plugins from beets import config, plugins
from beets.util import MoveOperation, displayable_path, pipeline from beets.util import MoveOperation, displayable_path, pipeline
@ -30,6 +30,8 @@ from .tasks import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable
from beets import library from beets import library
from .session import ImportSession from .session import ImportSession

View file

@ -20,9 +20,10 @@ import re
import shutil import shutil
import time import time
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable, Iterable, Sequence
from enum import Enum from enum import Enum
from tempfile import mkdtemp from tempfile import mkdtemp
from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence from typing import TYPE_CHECKING, Any
import mediafile import mediafile

View file

@ -37,7 +37,7 @@ from logging import (
RootLogger, RootLogger,
StreamHandler, StreamHandler,
) )
from typing import TYPE_CHECKING, Any, Mapping, TypeVar, Union, overload from typing import TYPE_CHECKING, Any, TypeVar, Union, overload
__all__ = [ __all__ = [
"DEBUG", "DEBUG",
@ -54,6 +54,8 @@ __all__ = [
] ]
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Mapping
T = TypeVar("T") T = TypeVar("T")
from types import TracebackType from types import TracebackType

View file

@ -10,7 +10,7 @@ from __future__ import annotations
import abc import abc
import re import re
from functools import cache, cached_property from functools import cache, cached_property
from typing import TYPE_CHECKING, Generic, Literal, Sequence, TypedDict, TypeVar from typing import TYPE_CHECKING, Generic, Literal, TypedDict, TypeVar
import unidecode import unidecode
from confuse import NotFoundError from confuse import NotFoundError
@ -22,7 +22,7 @@ from beets.util.id_extractors import extract_release_id
from .plugins import BeetsPlugin, find_plugins, notify_info_yielded, send from .plugins import BeetsPlugin, find_plugins, notify_info_yielded, send
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterable from collections.abc import Iterable, Sequence
from .autotag.hooks import AlbumInfo, Item, TrackInfo from .autotag.hooks import AlbumInfo, Item, TrackInfo

View file

@ -25,7 +25,6 @@ from collections import defaultdict
from functools import cached_property, wraps from functools import cached_property, wraps
from importlib import import_module from importlib import import_module
from pathlib import Path from pathlib import Path
from types import GenericAlias
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar
import mediafile import mediafile
@ -450,9 +449,6 @@ def _get_plugin(name: str) -> BeetsPlugin | None:
for obj in reversed(namespace.__dict__.values()): for obj in reversed(namespace.__dict__.values()):
if ( if (
inspect.isclass(obj) inspect.isclass(obj)
and not isinstance(
obj, GenericAlias
) # seems to be needed for python <= 3.9 only
and issubclass(obj, BeetsPlugin) and issubclass(obj, BeetsPlugin)
and obj != BeetsPlugin and obj != BeetsPlugin
and not inspect.isabstract(obj) and not inspect.isabstract(obj)

View file

@ -32,7 +32,7 @@ import warnings
from difflib import SequenceMatcher from difflib import SequenceMatcher
from functools import cache from functools import cache
from itertools import chain from itertools import chain
from typing import Any, Callable, Literal from typing import TYPE_CHECKING, Any, Literal
import confuse import confuse
@ -42,6 +42,9 @@ from beets.dbcore import query as db_query
from beets.util import as_string from beets.util import as_string
from beets.util.functemplate import template from beets.util.functemplate import template
if TYPE_CHECKING:
from collections.abc import Callable
# On Windows platforms, use colorama to support "ANSI" terminal colors. # On Windows platforms, use colorama to support "ANSI" terminal colors.
if sys.platform == "win32": if sys.platform == "win32":
try: try:
@ -1601,7 +1604,7 @@ def _raw_main(args: list[str], lib=None) -> None:
): ):
from beets.ui.commands.config import config_edit from beets.ui.commands.config import config_edit
return config_edit() return config_edit(options)
test_lib = bool(lib) test_lib = bool(lib)
subcommands, lib = _setup(options, lib) subcommands, lib = _setup(options, lib)

View file

@ -30,7 +30,10 @@ def config_func(lib, opts, args):
# Open in editor. # Open in editor.
elif opts.edit: elif opts.edit:
config_edit() # Note: This branch *should* be unreachable
# since the normal flow should be short-circuited
# by the special case in ui._raw_main
config_edit(opts)
# Dump configuration. # Dump configuration.
else: else:
@ -41,11 +44,11 @@ def config_func(lib, opts, args):
print("Empty configuration") print("Empty configuration")
def config_edit(): def config_edit(cli_options):
"""Open a program to edit the user configuration. """Open a program to edit the user configuration.
An empty config file is created if no existing config file exists. An empty config file is created if no existing config file exists.
""" """
path = config.user_config_path() path = cli_options.config or config.user_config_path()
editor = editor_command() editor = editor_command()
try: try:
if not os.path.isfile(path): if not os.path.isfile(path):

View file

@ -29,7 +29,7 @@ import tempfile
import traceback import traceback
import warnings import warnings
from collections import Counter from collections import Counter
from collections.abc import Sequence from collections.abc import Callable, Sequence
from contextlib import suppress from contextlib import suppress
from enum import Enum from enum import Enum
from functools import cache from functools import cache
@ -41,7 +41,6 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
AnyStr, AnyStr,
Callable,
ClassVar, ClassVar,
Generic, Generic,
NamedTuple, NamedTuple,

View file

@ -26,7 +26,7 @@ import subprocess
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from enum import Enum from enum import Enum
from itertools import chain from itertools import chain
from typing import Any, ClassVar, Mapping from typing import TYPE_CHECKING, Any, ClassVar
from urllib.parse import urlencode from urllib.parse import urlencode
from beets import logging, util from beets import logging, util
@ -37,6 +37,9 @@ from beets.util import (
syspath, syspath,
) )
if TYPE_CHECKING:
from collections.abc import Mapping
PROXY_URL = "https://images.weserv.nl/" PROXY_URL = "https://images.weserv.nl/"
log = logging.getLogger("beets") log = logging.getLogger("beets")

View file

@ -105,8 +105,6 @@ def compile_func(arg_names, statements, name="_the_func", debug=False):
decorator_list=[], decorator_list=[],
) )
# The ast.Module signature changed in 3.8 to accept a list of types to
# ignore.
mod = ast.Module([func_def], []) mod = ast.Module([func_def], [])
ast.fix_missing_locations(mod) ast.fix_missing_locations(mod)

View file

@ -20,10 +20,9 @@ import os
import stat import stat
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Union
def is_hidden(path: Union[bytes, Path]) -> bool: def is_hidden(path: bytes | Path) -> bool:
""" """
Determine whether the given path is treated as a 'hidden file' by the OS. Determine whether the given path is treated as a 'hidden file' by the OS.
""" """

View file

@ -36,10 +36,13 @@ from __future__ import annotations
import queue import queue
import sys import sys
from threading import Lock, Thread from threading import Lock, Thread
from typing import Callable, Generator, TypeVar from typing import TYPE_CHECKING, TypeVar
from typing_extensions import TypeVarTuple, Unpack from typing_extensions import TypeVarTuple, Unpack
if TYPE_CHECKING:
from collections.abc import Callable, Generator
BUBBLE = "__PIPELINE_BUBBLE__" BUBBLE = "__PIPELINE_BUBBLE__"
POISON = "__PIPELINE_POISON__" POISON = "__PIPELINE_POISON__"

View file

@ -19,14 +19,7 @@ from __future__ import annotations
import json import json
import re import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import ( from typing import TYPE_CHECKING, Literal, overload
TYPE_CHECKING,
Iterable,
Iterator,
Literal,
Sequence,
overload,
)
import confuse import confuse
from requests_oauthlib import OAuth1Session from requests_oauthlib import OAuth1Session
@ -42,6 +35,8 @@ from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.metadata_plugins import MetadataSourcePlugin from beets.metadata_plugins import MetadataSourcePlugin
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterable, Iterator, Sequence
from beets.importer import ImportSession from beets.importer import ImportSession
from beets.library import Item from beets.library import Item

View file

@ -283,7 +283,7 @@ class BaseServer:
if not self.ctrl_sock: if not self.ctrl_sock:
self.ctrl_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.ctrl_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.ctrl_sock.connect((self.ctrl_host, self.ctrl_port)) self.ctrl_sock.connect((self.ctrl_host, self.ctrl_port))
self.ctrl_sock.sendall((f"{message}\n").encode("utf-8")) self.ctrl_sock.sendall((f"{message}\n").encode())
def _send_event(self, event): def _send_event(self, event):
"""Notify subscribed connections of an event.""" """Notify subscribed connections of an event."""

View file

@ -18,8 +18,8 @@ autotagger. Requires the pyacoustid library.
import re import re
from collections import defaultdict from collections import defaultdict
from collections.abc import Iterable
from functools import cached_property, partial from functools import cached_property, partial
from typing import Iterable
import acoustid import acoustid
import confuse import confuse

View file

@ -18,7 +18,7 @@ from __future__ import annotations
import collections import collections
import time import time
from typing import TYPE_CHECKING, Literal, Sequence from typing import TYPE_CHECKING, Literal
import requests import requests
@ -32,6 +32,8 @@ from beets.metadata_plugins import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence
from beets.library import Item, Library from beets.library import Item, Library
from ._typing import JSONDict from ._typing import JSONDict

View file

@ -27,7 +27,7 @@ import time
import traceback import traceback
from functools import cache from functools import cache
from string import ascii_lowercase from string import ascii_lowercase
from typing import TYPE_CHECKING, Sequence, cast from typing import TYPE_CHECKING, cast
import confuse import confuse
from discogs_client import Client, Master, Release from discogs_client import Client, Master, Release
@ -43,7 +43,7 @@ from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.metadata_plugins import MetadataSourcePlugin from beets.metadata_plugins import MetadataSourcePlugin
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable, Sequence
from beets.library import Item from beets.library import Item

View file

@ -23,7 +23,7 @@ from collections import OrderedDict
from contextlib import closing from contextlib import closing
from enum import Enum from enum import Enum
from functools import cached_property from functools import cached_property
from typing import TYPE_CHECKING, AnyStr, ClassVar, Literal, Tuple, Type from typing import TYPE_CHECKING, AnyStr, ClassVar, Literal
import confuse import confuse
import requests import requests
@ -86,7 +86,7 @@ class Candidate:
path: None | bytes = None, path: None | bytes = None,
url: None | str = None, url: None | str = None,
match: None | MetadataMatch = None, match: None | MetadataMatch = None,
size: None | Tuple[int, int] = None, size: None | tuple[int, int] = None,
): ):
self._log = log self._log = log
self.path = path self.path = path
@ -682,7 +682,7 @@ class GoogleImages(RemoteArtSource):
""" """
if not (album.albumartist and album.album): if not (album.albumartist and album.album):
return return
search_string = f"{album.albumartist},{album.album}".encode("utf-8") search_string = f"{album.albumartist},{album.album}".encode()
try: try:
response = self.request( response = self.request(
@ -1293,7 +1293,7 @@ class CoverArtUrl(RemoteArtSource):
# All art sources. The order they will be tried in is specified by the config. # All art sources. The order they will be tried in is specified by the config.
ART_SOURCES: set[Type[ArtSource]] = { ART_SOURCES: set[type[ArtSource]] = {
FileSystem, FileSystem,
CoverArtArchive, CoverArtArchive,
ITunesStore, ITunesStore,

View file

@ -19,7 +19,7 @@ class ImportSourcePlugin(BeetsPlugin):
def __init__(self): def __init__(self):
"""Initialize the plugin and read configuration.""" """Initialize the plugin and read configuration."""
super(ImportSourcePlugin, self).__init__() super().__init__()
self.config.add( self.config.add(
{ {
"suggest_removal": False, "suggest_removal": False,

View file

@ -28,7 +28,7 @@ import os
import traceback import traceback
from functools import singledispatchmethod from functools import singledispatchmethod
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING
import pylast import pylast
import yaml import yaml
@ -352,7 +352,7 @@ class LastGenrePlugin(plugins.BeetsPlugin):
combined = old + new combined = old + new
return self._resolve_genres(combined) return self._resolve_genres(combined)
def _get_genre(self, obj: LibModel) -> tuple[Union[str, None], ...]: def _get_genre(self, obj: LibModel) -> tuple[str | None, ...]:
"""Get the final genre string for an Album or Item object. """Get the final genre string for an Album or Item object.
`self.sources` specifies allowed genre sources. Starting with the first `self.sources` specifies allowed genre sources. Starting with the first

View file

@ -28,7 +28,7 @@ from html import unescape
from http import HTTPStatus from http import HTTPStatus
from itertools import groupby from itertools import groupby
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Iterable, Iterator, NamedTuple from typing import TYPE_CHECKING, NamedTuple
from urllib.parse import quote, quote_plus, urlencode, urlparse from urllib.parse import quote, quote_plus, urlencode, urlparse
import langdetect import langdetect
@ -42,6 +42,8 @@ from beets.autotag.distance import string_dist
from beets.util.config import sanitize_choices from beets.util.config import sanitize_choices
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterable, Iterator
from beets.importer import ImportTask from beets.importer import ImportTask
from beets.library import Item, Library from beets.library import Item, Library
from beets.logging import BeetsLogger as Logger from beets.logging import BeetsLogger as Logger
@ -958,7 +960,7 @@ class LyricsPlugin(RequestHandler, plugins.BeetsPlugin):
@cached_property @cached_property
def backends(self) -> list[Backend]: def backends(self) -> list[Backend]:
user_sources = self.config["sources"].get() user_sources = self.config["sources"].as_str_seq()
chosen = sanitize_choices(user_sources, self.BACKEND_BY_NAME) chosen = sanitize_choices(user_sources, self.BACKEND_BY_NAME)
if "google" in chosen and not self.config["google_API_key"].get(): if "google" in chosen and not self.config["google_API_key"].get():

View file

@ -19,7 +19,7 @@ from __future__ import annotations
import itertools import itertools
import traceback import traceback
from copy import deepcopy from copy import deepcopy
from typing import TYPE_CHECKING, Any, Iterable, Sequence from typing import TYPE_CHECKING, Any
import mediafile import mediafile
import musicbrainzngs import musicbrainzngs
@ -40,6 +40,8 @@ from beetsplug.musicbrainz import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from beets.autotag import AlbumMatch from beets.autotag import AlbumMatch
from beets.library import Item from beets.library import Item
from beetsplug._typing import JSONDict from beetsplug._typing import JSONDict

View file

@ -21,7 +21,7 @@ from collections import Counter
from contextlib import suppress from contextlib import suppress
from functools import cached_property from functools import cached_property
from itertools import product from itertools import product
from typing import TYPE_CHECKING, Any, Iterable, Sequence from typing import TYPE_CHECKING, Any
from urllib.parse import urljoin from urllib.parse import urljoin
import musicbrainzngs import musicbrainzngs
@ -34,6 +34,7 @@ from beets.metadata_plugins import MetadataSourcePlugin
from beets.util.id_extractors import extract_release_id from beets.util.id_extractors import extract_release_id
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from typing import Literal from typing import Literal
from beets.library import Item from beets.library import Item

View file

@ -28,7 +28,7 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from multiprocessing.pool import ThreadPool from multiprocessing.pool import ThreadPool
from threading import Event, Thread from threading import Event, Thread
from typing import TYPE_CHECKING, Any, Callable, TypeVar from typing import TYPE_CHECKING, Any, TypeVar
from beets import ui from beets import ui
from beets.plugins import BeetsPlugin from beets.plugins import BeetsPlugin
@ -36,7 +36,7 @@ from beets.util import command_output, displayable_path, syspath
if TYPE_CHECKING: if TYPE_CHECKING:
import optparse import optparse
from collections.abc import Sequence from collections.abc import Callable, Sequence
from logging import Logger from logging import Logger
from confuse import ConfigView from confuse import ConfigView

View file

@ -27,7 +27,7 @@ import re
import threading import threading
import time import time
import webbrowser import webbrowser
from typing import TYPE_CHECKING, Any, Literal, Sequence, Union from typing import TYPE_CHECKING, Any, Literal, Union
import confuse import confuse
import requests import requests
@ -43,6 +43,8 @@ from beets.metadata_plugins import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence
from beets.library import Library from beets.library import Library
from beetsplug._typing import JSONDict from beetsplug._typing import JSONDict

View file

@ -7,6 +7,9 @@ below!
Unreleased Unreleased
---------- ----------
Beets now requires Python 3.10 or later since support for EOL Python 3.9 has
been dropped.
New features: New features:
- :doc:`plugins/ftintitle`: Added argument for custom feat. words in ftintitle. - :doc:`plugins/ftintitle`: Added argument for custom feat. words in ftintitle.
@ -32,6 +35,10 @@ Bug fixes:
audio-features endpoint, the plugin logs a warning once and skips audio audio-features endpoint, the plugin logs a warning once and skips audio
features for all remaining tracks in the session, avoiding unnecessary API features for all remaining tracks in the session, avoiding unnecessary API
calls and rate limit exhaustion. calls and rate limit exhaustion.
- Running `beet --config <mypath> config -e` now edits `<mypath>` rather than
the default config path. :bug:`5652`
- :doc:`plugins/lyrics`: Accepts strings for lyrics sources (previously only
accepted a list of strings). :bug:`5962`
For plugin developers: For plugin developers:
@ -41,6 +48,8 @@ For plugin developers:
For packagers: For packagers:
- The minimum supported Python version is now 3.10.
Other changes: Other changes:
- The documentation chapter :doc:`dev/paths` has been moved to the "For - The documentation chapter :doc:`dev/paths` has been moved to the "For

View file

@ -1,10 +1,10 @@
Installation Installation
============ ============
Beets requires `Python 3.9 or later`_. You can install it using package Beets requires `Python 3.10 or later`_. You can install it using package
managers, pipx_, pip_ or by using package managers. managers, pipx_, pip_ or by using package managers.
.. _python 3.9 or later: https://python.org/download/ .. _python 3.10 or later: https://python.org/download/
Using ``pipx`` or ``pip`` Using ``pipx`` or ``pip``
------------------------- -------------------------

View file

@ -6,18 +6,18 @@ from __future__ import annotations
import re import re
import subprocess import subprocess
from collections.abc import Callable
from contextlib import redirect_stdout from contextlib import redirect_stdout
from datetime import datetime, timezone from datetime import datetime, timezone
from functools import partial from functools import partial
from io import StringIO from io import StringIO
from pathlib import Path from pathlib import Path
from typing import Callable, NamedTuple from typing import NamedTuple, TypeAlias
import click import click
import tomli import tomli
from packaging.version import Version, parse from packaging.version import Version, parse
from sphinx.ext import intersphinx from sphinx.ext import intersphinx
from typing_extensions import TypeAlias
from docs.conf import rst_epilog from docs.conf import rst_epilog

2948
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,6 @@ classifiers = [
"Environment :: Web Environment", "Environment :: Web Environment",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
@ -42,7 +41,7 @@ Changelog = "https://github.com/beetbox/beets/blob/master/docs/changelog.rst"
"Bug Tracker" = "https://github.com/beetbox/beets/issues" "Bug Tracker" = "https://github.com/beetbox/beets/issues"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.9,<4" python = ">=3.10,<4"
colorama = { version = "*", markers = "sys_platform == 'win32'" } colorama = { version = "*", markers = "sys_platform == 'win32'" }
confuse = ">=2.1.0" confuse = ">=2.1.0"
@ -228,7 +227,7 @@ cmd = "ruff format"
[tool.poe.tasks.format-docs] [tool.poe.tasks.format-docs]
help = "Format the documentation" help = "Format the documentation"
cmd = "docstrfmt" cmd = "docstrfmt docs *.rst"
[tool.poe.tasks.lint] [tool.poe.tasks.lint]
help = "Check the code for linting issues. Accepts ruff options." help = "Check the code for linting issues. Accepts ruff options."
@ -286,7 +285,6 @@ extend-exclude = [
"docs/api/**/*", "docs/api/**/*",
"README_kr.rst", "README_kr.rst",
] ]
files = ["docs", "*.rst"]
[tool.ruff] [tool.ruff]
target-version = "py39" target-version = "py39"
@ -305,9 +303,7 @@ select = [
"N", # pep8-naming "N", # pep8-naming
"PT", # flake8-pytest-style "PT", # flake8-pytest-style
# "RUF", # ruff # "RUF", # ruff
# "UP", # pyupgrade "UP", # pyupgrade
"UP031", # do not use percent formatting
"UP032", # use f-string instead of format call
"TCH", # flake8-type-checking "TCH", # flake8-type-checking
"W", # pycodestyle "W", # pycodestyle
] ]

View file

@ -1,7 +1,7 @@
import os import os
from http import HTTPStatus from http import HTTPStatus
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any
import pytest import pytest
from flask.testing import Client from flask.testing import Client
@ -58,9 +58,7 @@ class TestAuraResponse:
def get_response_data(self, client: Client, item): def get_response_data(self, client: Client, item):
"""Return a callback accepting `endpoint` and `params` parameters.""" """Return a callback accepting `endpoint` and `params` parameters."""
def get( def get(endpoint: str, params: dict[str, str]) -> dict[str, Any] | None:
endpoint: str, params: dict[str, str]
) -> Optional[dict[str, Any]]:
"""Add additional `params` and GET the given endpoint. """Add additional `params` and GET the given endpoint.
`include` parameter is added to every call to check that the `include` parameter is added to every call to check that the

View file

@ -14,7 +14,7 @@
"""Tests for the 'ftintitle' plugin.""" """Tests for the 'ftintitle' plugin."""
from typing import Dict, Generator, Optional, Tuple, Union from collections.abc import Generator
import pytest import pytest
@ -39,7 +39,7 @@ def env() -> Generator[FtInTitlePluginFunctional, None, None]:
def set_config( def set_config(
env: FtInTitlePluginFunctional, env: FtInTitlePluginFunctional,
cfg: Optional[Dict[str, Union[str, bool, list[str]]]], cfg: dict[str, str | bool | list[str]] | None,
) -> None: ) -> None:
cfg = {} if cfg is None else cfg cfg = {} if cfg is None else cfg
defaults = { defaults = {
@ -57,7 +57,7 @@ def add_item(
path: str, path: str,
artist: str, artist: str,
title: str, title: str,
albumartist: Optional[str], albumartist: str | None,
) -> Item: ) -> Item:
return env.add_item( return env.add_item(
path=path, path=path,
@ -250,10 +250,10 @@ def add_item(
) )
def test_ftintitle_functional( def test_ftintitle_functional(
env: FtInTitlePluginFunctional, env: FtInTitlePluginFunctional,
cfg: Optional[Dict[str, Union[str, bool, list[str]]]], cfg: dict[str, str | bool | list[str]] | None,
cmd_args: Tuple[str, ...], cmd_args: tuple[str, ...],
given: Tuple[str, str, Optional[str]], given: tuple[str, str, str | None],
expected: Tuple[str, str], expected: tuple[str, str],
) -> None: ) -> None:
set_config(env, cfg) set_config(env, cfg)
ftintitle.FtInTitlePlugin() ftintitle.FtInTitlePlugin()
@ -287,7 +287,7 @@ def test_ftintitle_functional(
def test_find_feat_part( def test_find_feat_part(
artist: str, artist: str,
albumartist: str, albumartist: str,
expected: Optional[str], expected: str | None,
) -> None: ) -> None:
assert ftintitle.find_feat_part(artist, albumartist) == expected assert ftintitle.find_feat_part(artist, albumartist) == expected
@ -307,7 +307,7 @@ def test_find_feat_part(
) )
def test_split_on_feat( def test_split_on_feat(
given: str, given: str,
expected: Tuple[str, Optional[str]], expected: tuple[str, str | None],
) -> None: ) -> None:
assert ftintitle.split_on_feat(given) == expected assert ftintitle.split_on_feat(given) == expected
@ -359,7 +359,7 @@ def test_contains_feat(given: str, expected: bool) -> None:
], ],
) )
def test_custom_words( def test_custom_words(
given: str, custom_words: Optional[list[str]], expected: bool given: str, custom_words: list[str] | None, expected: bool
) -> None: ) -> None:
if custom_words is None: if custom_words is None:
custom_words = [] custom_words = []

View file

@ -19,13 +19,13 @@ import os
import sys import sys
import unittest import unittest
from contextlib import contextmanager from contextlib import contextmanager
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING
from beets import plugins from beets import plugins
from beets.test.helper import PluginTestCase, capture_log from beets.test.helper import PluginTestCase, capture_log
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterator from collections.abc import Callable, Iterator
class HookTestCase(PluginTestCase): class HookTestCase(PluginTestCase):

View file

@ -1,6 +1,5 @@
import datetime import datetime
import os import os
import os.path
from beets.library import Album, Item from beets.library import Album, Item
from beets.test.helper import PluginTestCase from beets.test.helper import PluginTestCase

View file

@ -14,7 +14,7 @@
"""Various tests for querying the library database.""" """Various tests for querying the library database."""
from mock import patch from unittest.mock import patch
import beets.library import beets.library
from beets import config, dbcore from beets import config, dbcore

View file

@ -128,3 +128,11 @@ class ConfigCommandTest(BeetsTestCase):
with patch("os.execlp") as execlp: with patch("os.execlp") as execlp:
self.run_command("config", "-e") self.run_command("config", "-e")
execlp.assert_called_once_with("myeditor", "myeditor", self.config_path) execlp.assert_called_once_with("myeditor", "myeditor", self.config_path)
def test_edit_config_with_custom_config_path(self):
os.environ["EDITOR"] = "myeditor"
with patch("os.execlp") as execlp:
self.run_command("--config", self.cli_config_path, "config", "-e")
execlp.assert_called_once_with(
"myeditor", "myeditor", self.cli_config_path
)