From 9372371004526b69028a8a88d1690ab094c50449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Fri, 16 Jan 2026 21:40:19 +0000 Subject: [PATCH] Replace capture_output with io.getoutput --- beets/test/helper.py | 65 ++++++++++-------------------- test/plugins/test_bareasc.py | 16 +++----- test/plugins/test_convert.py | 2 +- test/plugins/test_edit.py | 4 +- test/plugins/test_export.py | 4 +- test/plugins/test_fetchart.py | 4 +- test/plugins/test_info.py | 4 +- test/plugins/test_lastgenre.py | 4 +- test/plugins/test_limit.py | 4 +- test/plugins/test_mbsubmit.py | 27 ++++++------- test/plugins/test_missing.py | 19 ++++----- test/plugins/test_play.py | 2 +- test/plugins/test_smartplaylist.py | 4 +- test/plugins/test_types_plugin.py | 4 +- test/test_metasync.py | 4 +- test/test_plugins.py | 3 +- test/ui/commands/test_config.py | 4 +- test/ui/commands/test_list.py | 37 +++++++++-------- test/ui/commands/test_write.py | 4 +- test/ui/test_ui.py | 2 +- 20 files changed, 94 insertions(+), 123 deletions(-) diff --git a/beets/test/helper.py b/beets/test/helper.py index 3f6b18336..bd3dc59d6 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -15,9 +15,6 @@ """This module includes various helpers that provide fixtures, capture information or mock the environment. -- `capture_stdout` context managers allow one to interact with the user - interface. - - `has_program` checks the presence of a command on the system. - The `ImportSessionFixture` allows one to run importer code while @@ -38,7 +35,6 @@ from contextlib import contextmanager from dataclasses import dataclass from enum import Enum from functools import cached_property -from io import StringIO from pathlib import Path from tempfile import gettempdir, mkdtemp, mkstemp from typing import Any, ClassVar @@ -84,25 +80,6 @@ def capture_log(logger="beets"): log.removeHandler(capture) -@contextmanager -def capture_stdout(): - """Save stdout in a StringIO. - - >>> with capture_stdout() as output: - ... print('spam') - ... - >>> output.getvalue() - 'spam' - """ - org = sys.stdout - sys.stdout = capture = StringIO() - try: - yield sys.stdout - finally: - sys.stdout = org - print(capture.getvalue()) - - def has_program(cmd, args=["--version"]): """Returns `True` if `cmd` can be executed.""" full_cmd = [cmd, *args] @@ -148,12 +125,31 @@ NEEDS_REFLINK = unittest.skipUnless( ) +class RunMixin: + def run_command(self, *args, **kwargs): + """Run a beets command with an arbitrary amount of arguments. The + Library` defaults to `self.lib`, but can be overridden with + the keyword argument `lib`. + """ + sys.argv = ["beet"] # avoid leakage from test suite args + lib = None + if hasattr(self, "lib"): + lib = self.lib + lib = kwargs.get("lib", lib) + beets.ui._raw_main(list(args), lib) + + @pytest.mark.usefixtures("io") -class IOMixin: +class IOMixin(RunMixin): io: _common.DummyIO + def run_with_output(self, *args): + self.io.getoutput() + self.run_command(*args) + return self.io.getoutput() -class TestHelper(ConfigMixin): + +class TestHelper(RunMixin, ConfigMixin): """Helper mixin for high-level cli and plugin tests. This mixin provides methods to isolate beets' global state provide @@ -366,25 +362,6 @@ class TestHelper(ConfigMixin): return path - # Running beets commands - - def run_command(self, *args, **kwargs): - """Run a beets command with an arbitrary amount of arguments. The - Library` defaults to `self.lib`, but can be overridden with - the keyword argument `lib`. - """ - sys.argv = ["beet"] # avoid leakage from test suite args - lib = None - if hasattr(self, "lib"): - lib = self.lib - lib = kwargs.get("lib", lib) - beets.ui._raw_main(list(args), lib) - - def run_with_output(self, *args): - with capture_stdout() as out: - self.run_command(*args) - return out.getvalue() - # Safe file operations def create_temp_dir(self, **kwargs) -> str: diff --git a/test/plugins/test_bareasc.py b/test/plugins/test_bareasc.py index e699a3dcf..a661ae7aa 100644 --- a/test/plugins/test_bareasc.py +++ b/test/plugins/test_bareasc.py @@ -4,10 +4,10 @@ """Tests for the 'bareasc' plugin.""" from beets import logging -from beets.test.helper import PluginTestCase, capture_stdout +from beets.test.helper import IOMixin, PluginTestCase -class BareascPluginTest(PluginTestCase): +class BareascPluginTest(IOMixin, PluginTestCase): """Test bare ASCII query matching.""" plugin = "bareasc" @@ -65,16 +65,12 @@ class BareascPluginTest(PluginTestCase): def test_bareasc_list_output(self): """Bare-ASCII version of list command - check output.""" - with capture_stdout() as output: - self.run_command("bareasc", "with accents") + self.run_command("bareasc", "with accents") - assert "Antonin Dvorak" in output.getvalue() + assert "Antonin Dvorak" in self.io.getoutput() def test_bareasc_format_output(self): """Bare-ASCII version of list -f command - check output.""" - with capture_stdout() as output: - self.run_command( - "bareasc", "with accents", "-f", "$artist:: $title" - ) + self.run_command("bareasc", "with accents", "-f", "$artist:: $title") - assert "Antonin Dvorak:: with accents\n" == output.getvalue() + assert "Antonin Dvorak:: with accents\n" == self.io.getoutput() diff --git a/test/plugins/test_convert.py b/test/plugins/test_convert.py index de2650617..13dbea084 100644 --- a/test/plugins/test_convert.py +++ b/test/plugins/test_convert.py @@ -29,8 +29,8 @@ from beets.test import _common from beets.test.helper import ( AsIsImporterMixin, ImportHelper, - PluginTestCase, IOMixin, + PluginTestCase, capture_log, ) from beetsplug import convert diff --git a/test/plugins/test_edit.py b/test/plugins/test_edit.py index 94ab34a03..06c7cad74 100644 --- a/test/plugins/test_edit.py +++ b/test/plugins/test_edit.py @@ -74,7 +74,7 @@ class ModifyFileMocker: f.write(contents) -class EditMixin(IOMixin, PluginMixin): +class EditMixin(PluginMixin): """Helper containing some common functionality used for the Edit tests.""" plugin = "edit" @@ -120,7 +120,7 @@ class EditMixin(IOMixin, PluginMixin): @_common.slow_test() @patch("beets.library.Item.write") -class EditCommandTest(EditMixin, BeetsTestCase): +class EditCommandTest(IOMixin, EditMixin, BeetsTestCase): """Black box tests for `beetsplug.edit`. Command line interaction is simulated using mocked stdin, and yaml editing via an external editor is simulated using `ModifyFileMocker`. diff --git a/test/plugins/test_export.py b/test/plugins/test_export.py index f37a0d2a7..3c795b2dc 100644 --- a/test/plugins/test_export.py +++ b/test/plugins/test_export.py @@ -19,10 +19,10 @@ import re # used to test csv format from xml.etree import ElementTree from xml.etree.ElementTree import Element -from beets.test.helper import PluginTestCase +from beets.test.helper import IOMixin, PluginTestCase -class ExportPluginTest(PluginTestCase): +class ExportPluginTest(IOMixin, PluginTestCase): plugin = "export" def setUp(self): diff --git a/test/plugins/test_fetchart.py b/test/plugins/test_fetchart.py index 96d882e9a..f347ed66a 100644 --- a/test/plugins/test_fetchart.py +++ b/test/plugins/test_fetchart.py @@ -18,10 +18,10 @@ import os import sys from beets import util -from beets.test.helper import PluginTestCase +from beets.test.helper import IOMixin, PluginTestCase -class FetchartCliTest(PluginTestCase): +class FetchartCliTest(IOMixin, PluginTestCase): plugin = "fetchart" def setUp(self): diff --git a/test/plugins/test_info.py b/test/plugins/test_info.py index c1b3fc941..3ad4d0884 100644 --- a/test/plugins/test_info.py +++ b/test/plugins/test_info.py @@ -15,11 +15,11 @@ from mediafile import MediaFile -from beets.test.helper import PluginTestCase +from beets.test.helper import IOMixin, PluginTestCase from beets.util import displayable_path -class InfoTest(PluginTestCase): +class InfoTest(IOMixin, PluginTestCase): plugin = "info" def test_path(self): diff --git a/test/plugins/test_lastgenre.py b/test/plugins/test_lastgenre.py index 026001e38..7c9deee35 100644 --- a/test/plugins/test_lastgenre.py +++ b/test/plugins/test_lastgenre.py @@ -19,11 +19,11 @@ from unittest.mock import Mock, patch import pytest from beets.test import _common -from beets.test.helper import PluginTestCase +from beets.test.helper import IOMixin, PluginTestCase from beetsplug import lastgenre -class LastGenrePluginTest(PluginTestCase): +class LastGenrePluginTest(IOMixin, PluginTestCase): plugin = "lastgenre" def setUp(self): diff --git a/test/plugins/test_limit.py b/test/plugins/test_limit.py index d77e47ca8..8f227d41e 100644 --- a/test/plugins/test_limit.py +++ b/test/plugins/test_limit.py @@ -13,10 +13,10 @@ """Tests for the 'limit' plugin.""" -from beets.test.helper import PluginTestCase +from beets.test.helper import IOMixin, PluginTestCase -class LimitPluginTest(PluginTestCase): +class LimitPluginTest(IOMixin, PluginTestCase): """Unit tests for LimitPlugin Note: query prefix tests do not work correctly with `run_with_output`. diff --git a/test/plugins/test_mbsubmit.py b/test/plugins/test_mbsubmit.py index fb275462a..48426fd7d 100644 --- a/test/plugins/test_mbsubmit.py +++ b/test/plugins/test_mbsubmit.py @@ -17,7 +17,6 @@ from beets.test.helper import ( AutotagImportTestCase, PluginMixin, TerminalImportMixin, - capture_stdout, ) @@ -33,11 +32,10 @@ class MBSubmitPluginTest( def test_print_tracks_output(self): """Test the output of the "print tracks" choice.""" - with capture_stdout() as output: - self.io.addinput("p") - self.io.addinput("s") - # Print tracks; Skip - self.importer.run() + self.io.addinput("p") + self.io.addinput("s") + # Print tracks; Skip + self.importer.run() # Manually build the string for comparing the output. tracklist = ( @@ -45,20 +43,19 @@ class MBSubmitPluginTest( "01. Tag Track 1 - Tag Artist (0:01)\n" "02. Tag Track 2 - Tag Artist (0:01)" ) - assert tracklist in output.getvalue() + assert tracklist in self.io.getoutput() def test_print_tracks_output_as_tracks(self): """Test the output of the "print tracks" choice, as singletons.""" - with capture_stdout() as output: - self.io.addinput("t") - self.io.addinput("s") - self.io.addinput("p") - self.io.addinput("s") - # as Tracks; Skip; Print tracks; Skip - self.importer.run() + self.io.addinput("t") + self.io.addinput("s") + self.io.addinput("p") + self.io.addinput("s") + # as Tracks; Skip; Print tracks; Skip + self.importer.run() # Manually build the string for comparing the output. tracklist = ( "Open files with Picard? 02. Tag Track 2 - Tag Artist (0:01)" ) - assert tracklist in output.getvalue() + assert tracklist in self.io.getoutput() diff --git a/test/plugins/test_missing.py b/test/plugins/test_missing.py index d12f2b4cf..812ed5fa3 100644 --- a/test/plugins/test_missing.py +++ b/test/plugins/test_missing.py @@ -3,20 +3,23 @@ import uuid import pytest from beets.library import Album -from beets.test.helper import PluginMixin, TestHelper +from beets.test.helper import IOMixin, PluginMixin, TestHelper @pytest.fixture -def helper(): +def helper(request): helper = TestHelper() helper.setup_beets() - yield helper + request.instance.lib = helper.lib + + yield helper.teardown_beets() -class TestMissingAlbums(PluginMixin): +@pytest.mark.usefixtures("helper") +class TestMissingAlbums(IOMixin, PluginMixin): plugin = "missing" album_in_lib = Album( album="Album", @@ -47,15 +50,13 @@ class TestMissingAlbums(PluginMixin): ], ) def test_missing_artist_albums( - self, requests_mock, helper, release_from_mb, expected_output + self, requests_mock, release_from_mb, expected_output ): - helper.lib.add(self.album_in_lib) + self.lib.add(self.album_in_lib) requests_mock.get( f"/ws/2/release-group?artist={self.album_in_lib.mb_albumartistid}", json={"release-groups": [release_from_mb]}, ) with self.configure_plugin({}): - assert ( - helper.run_with_output("missing", "--album") == expected_output - ) + assert self.run_with_output("missing", "--album") == expected_output diff --git a/test/plugins/test_play.py b/test/plugins/test_play.py index e67c8cddf..53de21233 100644 --- a/test/plugins/test_play.py +++ b/test/plugins/test_play.py @@ -21,7 +21,7 @@ from unittest.mock import ANY, patch import pytest -from beets.test.helper import CleanupModulesMixin, PluginTestCase, IOMixin +from beets.test.helper import CleanupModulesMixin, IOMixin, PluginTestCase from beets.ui import UserError from beets.util import open_anything from beetsplug.play import PlayPlugin diff --git a/test/plugins/test_smartplaylist.py b/test/plugins/test_smartplaylist.py index 8ec2c74ce..7cc712330 100644 --- a/test/plugins/test_smartplaylist.py +++ b/test/plugins/test_smartplaylist.py @@ -24,7 +24,7 @@ import pytest from beets import config from beets.dbcore.query import FixedFieldSort, MultipleSort, NullSort from beets.library import Album, Item, parse_query_string -from beets.test.helper import BeetsTestCase, PluginTestCase +from beets.test.helper import BeetsTestCase, IOMixin, PluginTestCase from beets.ui import UserError from beets.util import CHAR_REPLACE, syspath from beetsplug.smartplaylist import SmartPlaylistPlugin @@ -458,7 +458,7 @@ class SmartPlaylistTest(BeetsTestCase): assert content.count(b"/item2.mp3") == 1 -class SmartPlaylistCLITest(PluginTestCase): +class SmartPlaylistCLITest(IOMixin, PluginTestCase): plugin = "smartplaylist" def setUp(self): diff --git a/test/plugins/test_types_plugin.py b/test/plugins/test_types_plugin.py index 41807b80d..24fb577f7 100644 --- a/test/plugins/test_types_plugin.py +++ b/test/plugins/test_types_plugin.py @@ -19,10 +19,10 @@ from datetime import datetime import pytest from confuse import ConfigValueError -from beets.test.helper import PluginTestCase +from beets.test.helper import IOMixin, PluginTestCase -class TypesPluginTest(PluginTestCase): +class TypesPluginTest(IOMixin, PluginTestCase): plugin = "types" def test_integer_modify_and_query(self): diff --git a/test/test_metasync.py b/test/test_metasync.py index 13c003a1c..aeb38545b 100644 --- a/test/test_metasync.py +++ b/test/test_metasync.py @@ -20,7 +20,7 @@ from datetime import datetime from beets.library import Item from beets.test import _common -from beets.test.helper import PluginTestCase +from beets.test.helper import IOMixin, PluginTestCase def _parsetime(s): @@ -31,7 +31,7 @@ def _is_windows(): return platform.system() == "Windows" -class MetaSyncTest(PluginTestCase): +class MetaSyncTest(IOMixin, PluginTestCase): plugin = "metasync" itunes_library_unix = os.path.join(_common.RSRC, b"itunes_library_unix.xml") itunes_library_windows = os.path.join( diff --git a/test/test_plugins.py b/test/test_plugins.py index c23ea0c7a..4786b12b4 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -38,6 +38,7 @@ from beets.test import helper from beets.test.helper import ( AutotagStub, ImportHelper, + IOMixin, PluginMixin, PluginTestCase, TerminalImportMixin, @@ -45,7 +46,7 @@ from beets.test.helper import ( from beets.util import PromptChoice, displayable_path, syspath -class TestPluginRegistration(PluginTestCase): +class TestPluginRegistration(IOMixin, PluginTestCase): class RatingPlugin(plugins.BeetsPlugin): item_types: ClassVar[dict[str, types.Type]] = { "rating": types.Float(), diff --git a/test/ui/commands/test_config.py b/test/ui/commands/test_config.py index c1215ef43..cd83b919f 100644 --- a/test/ui/commands/test_config.py +++ b/test/ui/commands/test_config.py @@ -5,10 +5,10 @@ import pytest import yaml from beets import config, ui -from beets.test.helper import BeetsTestCase +from beets.test.helper import BeetsTestCase, IOMixin -class ConfigCommandTest(BeetsTestCase): +class ConfigCommandTest(IOMixin, BeetsTestCase): def setUp(self): super().setUp() for k in ("VISUAL", "EDITOR"): diff --git a/test/ui/commands/test_list.py b/test/ui/commands/test_list.py index a63a56ad1..372d75410 100644 --- a/test/ui/commands/test_list.py +++ b/test/ui/commands/test_list.py @@ -1,9 +1,9 @@ from beets.test import _common -from beets.test.helper import BeetsTestCase, capture_stdout +from beets.test.helper import BeetsTestCase, IOMixin from beets.ui.commands.list import list_items -class ListTest(BeetsTestCase): +class ListTest(IOMixin, BeetsTestCase): def setUp(self): super().setUp() self.item = _common.item() @@ -12,13 +12,12 @@ class ListTest(BeetsTestCase): self.lib.add_album([self.item]) def _run_list(self, query="", album=False, path=False, fmt=""): - with capture_stdout() as stdout: - list_items(self.lib, query, album, fmt) - return stdout + list_items(self.lib, query, album, fmt) + return self.io.getoutput() def test_list_outputs_item(self): stdout = self._run_list() - assert "the title" in stdout.getvalue() + assert "the title" in stdout def test_list_unicode_query(self): self.item.title = "na\xefve" @@ -26,44 +25,44 @@ class ListTest(BeetsTestCase): self.lib._connection().commit() stdout = self._run_list(["na\xefve"]) - out = stdout.getvalue() + out = stdout assert "na\xefve" in out def test_list_item_path(self): stdout = self._run_list(fmt="$path") - assert stdout.getvalue().strip() == "xxx/yyy" + assert stdout.strip() == "xxx/yyy" def test_list_album_outputs_something(self): stdout = self._run_list(album=True) - assert len(stdout.getvalue()) > 0 + assert len(stdout) > 0 def test_list_album_path(self): stdout = self._run_list(album=True, fmt="$path") - assert stdout.getvalue().strip() == "xxx" + assert stdout.strip() == "xxx" def test_list_album_omits_title(self): stdout = self._run_list(album=True) - assert "the title" not in stdout.getvalue() + assert "the title" not in stdout def test_list_uses_track_artist(self): stdout = self._run_list() - assert "the artist" in stdout.getvalue() - assert "the album artist" not in stdout.getvalue() + assert "the artist" in stdout + assert "the album artist" not in stdout def test_list_album_uses_album_artist(self): stdout = self._run_list(album=True) - assert "the artist" not in stdout.getvalue() - assert "the album artist" in stdout.getvalue() + assert "the artist" not in stdout + assert "the album artist" in stdout def test_list_item_format_artist(self): stdout = self._run_list(fmt="$artist") - assert "the artist" in stdout.getvalue() + assert "the artist" in stdout def test_list_item_format_multiple(self): stdout = self._run_list(fmt="$artist - $album - $year") - assert "the artist - the album - 0001" == stdout.getvalue().strip() + assert "the artist - the album - 0001" == stdout.strip() def test_list_album_format(self): stdout = self._run_list(album=True, fmt="$genre") - assert "the genre" in stdout.getvalue() - assert "the album" not in stdout.getvalue() + assert "the genre" in stdout + assert "the album" not in stdout diff --git a/test/ui/commands/test_write.py b/test/ui/commands/test_write.py index 312b51dd2..7197cbded 100644 --- a/test/ui/commands/test_write.py +++ b/test/ui/commands/test_write.py @@ -1,7 +1,7 @@ -from beets.test.helper import BeetsTestCase +from beets.test.helper import BeetsTestCase, IOMixin -class WriteTest(BeetsTestCase): +class WriteTest(IOMixin, BeetsTestCase): def write_cmd(self, *args): return self.run_with_output("write", *args) diff --git a/test/ui/test_ui.py b/test/ui/test_ui.py index f96d2c76a..577954a85 100644 --- a/test/ui/test_ui.py +++ b/test/ui/test_ui.py @@ -398,7 +398,7 @@ class PluginTest(TestPluginTestCase): self.run_command("test", lib=None) -class CommonOptionsParserCliTest(BeetsTestCase): +class CommonOptionsParserCliTest(IOMixin, BeetsTestCase): """Test CommonOptionsParser and formatting LibModel formatting on 'list' command. """