Replace capture_output with io.getoutput

This commit is contained in:
Šarūnas Nejus 2026-01-16 21:40:19 +00:00
parent cbfec8de66
commit 9372371004
No known key found for this signature in database
20 changed files with 94 additions and 123 deletions

View file

@ -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:

View file

@ -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()

View file

@ -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

View file

@ -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`.

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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`.

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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):

View file

@ -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(

View file

@ -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(),

View file

@ -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"):

View file

@ -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

View file

@ -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)

View file

@ -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.
"""