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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions beetsplug/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import codecs
import os
import shlex
import subprocess
Expand Down Expand Up @@ -427,7 +426,7 @@ def _edit_yaml(self, old_str: str) -> tuple[list[JSONDict], str] | None:
while True:
edit(new.name, self._log)

with codecs.open(new.name, encoding="utf-8") as f:
with open(new.name, encoding="utf-8") as f:
new_str = f.read()
if new_str == old_str:
ui.print_("No changes; aborting.")
Expand Down
3 changes: 1 addition & 2 deletions beetsplug/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import codecs
import csv
import json
import os
Expand Down Expand Up @@ -208,7 +207,7 @@ def __init__(
self.encoding = encoding
# creates a file object to write/append or sets to stdout
self.out_stream = (
codecs.open(self.path, self.mode, self.encoding)
open(self.path, self.mode, encoding=self.encoding, newline="")
if self.path
else sys.stdout
)
Expand Down
4 changes: 4 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ cache_dir = /tmp/pytest_cache
console_output_style = count
# pretty-print test names in the Codecov U
junit_family = legacy
# hide 3rd-party warnings that we cannot fix
filterwarnings =
ignore:'asyncio.AbstractEventLoopPolicy' is deprecated:DeprecationWarning
ignore:'asyncio.get_event_loop_policy' is deprecated:DeprecationWarning
addopts =
# show all skipped/failed/xfailed tests in the summary except passed
-ra
Expand Down
6 changes: 4 additions & 2 deletions test/autotag/test_distance.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ def test_update(self, dist):

class TestTrackDistance:
@pytest.fixture(scope="class")
def info(self):
@classmethod
def info(cls):
return TrackInfo(title="title", artist="artist")

@pytest.mark.parametrize(
Expand All @@ -173,7 +174,8 @@ def test_track_distance(self, info, title, artist, expected_penalty):

class TestAlbumDistance:
@pytest.fixture(scope="class")
def items(self):
@classmethod
def items(cls):
return [
Item(
title=title,
Expand Down
3 changes: 3 additions & 0 deletions test/autotag/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
from beets.test.helper import BeetsTestCase
from beets.util import AttrDict

# hide deprecation warnings for the legacy string fields under test
pytestmark = pytest.mark.filterwarnings(r"ignore::DeprecationWarning")

str_field_deprecation = pytest.warns(
DeprecationWarning, match="The 'genre' field is deprecated"
)
Expand Down
25 changes: 13 additions & 12 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,19 @@ def do_not_log_sources(monkeypatch):
monkeypatch.setattr("beets.config.log_sources", lambda _: None)


@pytest.fixture(autouse=True)
def restore_log_level() -> Iterator[None]:
"""Reset the global log level, which the CLI sets and never restores.

See https://github.com/beetbox/beets/actions/runs/38253501906/job/114825507731.
"""
log = logging.getLogger("beets")
default_level, level = log.default_level, log.level
yield
log.default_level = default_level
log.setLevel(level)


@pytest.fixture(autouse=True)
def clear_cached_classproperty():
cached_classproperty.cache.clear()
Expand Down Expand Up @@ -248,18 +261,6 @@ def is_importable():
# Inheriting from TestHelper gives each test function isolated state. Use the
# fixtures below instead when a broader scope is safe and the suite benefits
# from reusing the same helper instance.
@pytest.fixture(scope="session")
def session_helper():
"""Share beets test state across the full test session.

Use this for suites that tolerate shared library contents and global
configuration. Tests should target specific records rather than assume a
completely fresh overall state.
"""
with TestHelper() as helper:
yield helper


@pytest.fixture(scope="module")
def module_helper():
"""Share beets test state within one test module.
Expand Down
22 changes: 14 additions & 8 deletions test/dbcore/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def helper(class_helper):

class TestGet:
@pytest.fixture(scope="class")
def lib(self, helper):
@classmethod
def lib(cls, helper):
album_items = [
helper.create_item(
title="first",
Expand Down Expand Up @@ -258,7 +259,8 @@ def test_legacy_field(self, caplog, lib, q, legacy_field):

class TestMatch:
@pytest.fixture(scope="class")
def item(self):
@classmethod
def item(cls):
return _common.item(
album="the album",
disc=6,
Expand Down Expand Up @@ -453,17 +455,18 @@ class TestQuery:
SINGLE = "singleton"

@pytest.fixture(scope="class")
def lib(self, helper):
@classmethod
def lib(cls, helper):
helper.add_album(
title=self.ALBUM,
title=cls.ALBUM,
comp=True,
flexbool=True,
bpm=120,
flexint=2,
rg_track_gain=0,
)
helper.add_item(
title=self.SINGLE, comp=False, flexbool=False, rg_track_gain=None
title=cls.SINGLE, comp=False, flexbool=False, rg_track_gain=None
)

with pytest.MonkeyPatch.context() as monkeypatch:
Expand Down Expand Up @@ -523,7 +526,8 @@ def test_value_type(self, lib, q, expected_titles):

class TestDefaultSearchFields:
@pytest.fixture(scope="class")
def lib(self, helper):
@classmethod
def lib(cls, helper):
helper.add_album(
title="title",
album="album",
Expand Down Expand Up @@ -552,7 +556,8 @@ class TestRelatedQueries:
"""Test album-level queries with track-level filters and vice-versa."""

@pytest.fixture(scope="class")
def lib(self, helper):
@classmethod
def lib(cls, helper):
for album_idx in range(1, 3):
album_name = f"Album{album_idx}"
items = [
Expand Down Expand Up @@ -606,7 +611,8 @@ class TestHasCoverArtQuery:
"""Test has_cover_art computed field for detecting embedded cover art."""

@pytest.fixture(scope="class")
def lib(self, helper):
@classmethod
def lib(cls, helper):
item_with = helper.add_item_fixture()
item_with.title = "with_art"

Expand Down
3 changes: 2 additions & 1 deletion test/dbcore/test_sort.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ class TestCaseSensitivity:
"""

@pytest.fixture(autouse=True, scope="class")
def setup(self, helper):
@classmethod
def setup(cls, helper):
helper.lib.add(Album(album="album", albumartist="bar"))
helper.lib.add(Album(album="Album", albumartist="Bar"))
helper.add_item(artist="artist", flex1="flex1", track=10)
Expand Down
10 changes: 6 additions & 4 deletions test/plugins/test_art.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,12 @@ def requests_mock_all(self, requests_mock):
return

@pytest.fixture(autouse=True, scope="class")
def cleanup(self):
@classmethod
def cleanup(cls):
try:
yield
finally:
for module in self.modules:
for module in cls.modules:
clean_module_tempdir(module)

@pytest.fixture
Expand Down Expand Up @@ -1027,11 +1028,12 @@ class AlbumArtOperationMixin(UseThePlugin):
IMAGE_WIDTH_HEIGHT_DIFF = IMAGE_WIDTH - IMAGE_HEIGHT

@pytest.fixture(autouse=True, scope="class")
def fs_mock(self, cleanup):
@classmethod
def fs_mock(cls, cleanup):
def fs_source_get(_self, album, settings, paths):
if paths:
yield fetchart.Candidate(
logger, source_name=_self.ID, path=self.IMAGE_PATH
logger, source_name=_self.ID, path=cls.IMAGE_PATH
)

with patch("beetsplug.fetchart.FileSystem.get", fs_source_get):
Expand Down
25 changes: 12 additions & 13 deletions test/plugins/test_aura.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,19 @@
from flask.testing import Client


@pytest.fixture(scope="session")
def helper(session_helper):
"""Keep the helper temp dir alive past the module-scoped Flask app.

``create_app`` opens a configured SQLite library before tests replace it
with ``helper.lib``. On Windows, module teardown can otherwise try to
delete ``library.db`` while the app still holds a file handle. The API
assertions filter known data, so sharing state for the session is harmless.
"""
return session_helper
@pytest.fixture(scope="module")
def helper(module_helper):
return module_helper


@pytest.fixture(scope="module")
def app(helper):
from beetsplug.aura import create_app

app = create_app()
# Close the on-disk library opened by ``create_app``: we do not use it, and
# its open file handle stops Windows removing the temp dir during teardown.
app.config["lib"]._close()
app.config["lib"] = helper.lib
return app

Expand Down Expand Up @@ -87,7 +83,8 @@ def get(endpoint: str, params: dict[str, str]) -> dict[str, Any] | None:
return get

@pytest.fixture(scope="class")
def track_document(self, item, album):
@classmethod
def track_document(cls, item, album):
return {
"type": "track",
"id": str(item.id),
Expand All @@ -108,7 +105,8 @@ def track_document(self, item, album):
}

@pytest.fixture(scope="class")
def artist_document(self, item):
@classmethod
def artist_document(cls, item):
return {
"type": "artist",
"id": item.artist,
Expand All @@ -119,7 +117,8 @@ def artist_document(self, item):
}

@pytest.fixture(scope="class")
def album_document(self, album):
@classmethod
def album_document(cls, album):
return {
"type": "album",
"id": str(album.id),
Expand Down
9 changes: 9 additions & 0 deletions test/plugins/test_bpd.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,22 @@

SERVER_START_TIMEOUT = 10

# The BPD server subprocess relies on inheriting the parent's patched
# `gstplayer` and `unittest.mock` state, so it has to be forked rather than
# spawned (the Python 3.14 default on Linux is `forkserver`).
if "fork" in mp.get_all_start_methods():
try:
mp.set_start_method("fork", force=True)
except RuntimeError:
# Already set, which is fine
pass

# Under `pytest-xdist` the forking parent is multi-threaded, which Python 3.14
# warns about. We only fork to start a short-lived server, so it is harmless.
pytestmark = pytest.mark.filterwarnings(
"ignore:This process .* is multi-threaded:DeprecationWarning"
)


class CommandParseTest(unittest.TestCase):
def test_no_args(self):
Expand Down
15 changes: 7 additions & 8 deletions test/plugins/test_edit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import codecs
from typing import ClassVar
from unittest.mock import patch

Expand Down Expand Up @@ -45,18 +44,18 @@ def overwrite_contents(self, filename, log):
`self.contents` is empty, the file remains unchanged.
"""
if self.contents:
with codecs.open(filename, "w", encoding="utf-8") as f:
with open(filename, "w", encoding="utf-8") as f:
f.write(self.contents)

def replace_contents(self, filename, log):
"""Modify `filename`, reading its contents and replacing the strings
specified in `self.replacements`.
"""
with codecs.open(filename, "r", encoding="utf-8") as f:
with open(filename, encoding="utf-8") as f:
contents = f.read()
for old, new_ in self.replacements.items():
contents = contents.replace(old, new_)
with codecs.open(filename, "w", encoding="utf-8") as f:
with open(filename, "w", encoding="utf-8") as f:
f.write(contents)


Expand Down Expand Up @@ -513,7 +512,7 @@ def test_importer_edit_candidate_keeps_rejected_albumfields_in_tracks(self):
seen_docs = {}

def inspect_and_edit(filename, log):
with codecs.open(filename, encoding="utf-8") as f:
with open(filename, encoding="utf-8") as f:
docs = load(f.read())

headers = [doc for doc in docs if "id" not in doc]
Expand All @@ -522,7 +521,7 @@ def inspect_and_edit(filename, log):
seen_docs["tracks"] = tracks
tracks[0]["title"] = "Issue 6953 edited title"

with codecs.open(filename, "w", encoding="utf-8") as f:
with open(filename, "w", encoding="utf-8") as f:
f.write(dump(docs))

with patch("beetsplug.edit.edit", side_effect=inspect_and_edit):
Expand Down Expand Up @@ -571,12 +570,12 @@ def test_importer_edit_album_header_reordered(self):
self.config["edit"]["albumfields"] = "album"

def reorder_and_edit(filename, log):
with codecs.open(filename, encoding="utf-8") as f:
with open(filename, encoding="utf-8") as f:
docs = load(f.read())
header = next(d for d in docs if "id" not in d)
tracks = [d for d in docs if "id" in d]
header["album"] = "Modified Album"
with codecs.open(filename, "w", encoding="utf-8") as f:
with open(filename, "w", encoding="utf-8") as f:
f.write(dump([*tracks, header]))

with patch("beetsplug.edit.edit", side_effect=reorder_and_edit):
Expand Down
2 changes: 1 addition & 1 deletion test/plugins/test_listenbrainz.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def _make_zip(files: dict[str, str]) -> bytes:


class TestListenBrainzPlugin(ConfigMixin):
@pytest.fixture(scope="class")
@pytest.fixture
def plugin(self) -> ListenBrainzPlugin:
self.config["listenbrainz"]["token"] = "test_token"
self.config["listenbrainz"]["username"] = "test_user"
Expand Down
Loading
Loading