Skip to content

Commit 8a43133

Browse files
authored
Replace custom unittest-like methods with assertions (#5854)
## Replace custom assertion methods with standard assertions This PR is part of `unittest` -> `pytest` migration #5361 and removes custom assertion methods from the test suite and replaces them with standard Python assertions. ### Key Changes - Removed custom assertion methods - Updated path handling to use `pathlib.Path` wherever this was relevant to the methods being replaced - Simplified some of the tests structure
2 parents a815305 + 0dd6cb3 commit 8a43133

24 files changed

+688
-1075
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
- if: ${{ env.IS_MAIN_PYTHON != 'true' }}
5353
name: Test without coverage
5454
run: |
55-
poetry install --extras=autobpm --extras=lyrics
55+
poetry install --extras=autobpm --extras=lyrics --extras=embedart
5656
poe test
5757
5858
- if: ${{ env.IS_MAIN_PYTHON == 'true' }}

beets/library/models.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ class LibModel(dbcore.Model["Library"]):
4545
def writable_media_fields(cls) -> set[str]:
4646
return set(MediaFile.fields()) & cls._fields.keys()
4747

48+
@property
49+
def filepath(self) -> Path:
50+
"""The path to the entity as pathlib.Path."""
51+
return Path(os.fsdecode(self.path))
52+
4853
def _template_funcs(self):
4954
funcs = DefaultTemplateFunctions(self, self._db).functions()
5055
funcs.update(plugins.template_funcs())
@@ -207,6 +212,8 @@ class Album(LibModel):
207212
Reflects the library's "albums" table, including album art.
208213
"""
209214

215+
artpath: bytes
216+
210217
_table = "albums"
211218
_flex_table = "album_attributes"
212219
_always_dirty = True
@@ -331,6 +338,11 @@ def relation_join(cls) -> str:
331338
f"ON {cls._table}.id = {cls._relation._table}.album_id"
332339
)
333340

341+
@property
342+
def art_filepath(self) -> Path | None:
343+
"""The path to album's cover picture as pathlib.Path."""
344+
return Path(os.fsdecode(self.artpath)) if self.artpath else None
345+
334346
@classmethod
335347
def _getters(cls):
336348
# In addition to plugin-provided computed fields, also expose
@@ -748,11 +760,6 @@ def relation_join(cls) -> str:
748760
f"ON {cls._table}.album_id = {cls._relation._table}.id"
749761
)
750762

751-
@property
752-
def filepath(self) -> Path:
753-
"""The path to the item's file as pathlib.Path."""
754-
return Path(os.fsdecode(self.path))
755-
756763
@property
757764
def _cached_album(self):
758765
"""The Album object that this item belongs to, if any, or

beets/test/_common.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -111,34 +111,6 @@ def import_session(lib=None, loghandler=None, paths=[], query=[], cli=False):
111111
return cls(lib, loghandler, paths, query)
112112

113113

114-
class Assertions:
115-
"""A mixin with additional unit test assertions."""
116-
117-
def assertExists(self, path):
118-
assert os.path.exists(syspath(path)), f"file does not exist: {path!r}"
119-
120-
def assertNotExists(self, path):
121-
assert not os.path.exists(syspath(path)), f"file exists: {path!r}"
122-
123-
def assertIsFile(self, path):
124-
self.assertExists(path)
125-
assert os.path.isfile(syspath(path)), (
126-
"path exists, but is not a regular file: {!r}".format(path)
127-
)
128-
129-
def assertIsDir(self, path):
130-
self.assertExists(path)
131-
assert os.path.isdir(syspath(path)), (
132-
"path exists, but is not a directory: {!r}".format(path)
133-
)
134-
135-
def assert_equal_path(self, a, b):
136-
"""Check that two paths are equal."""
137-
a_bytes, b_bytes = util.normpath(a), util.normpath(b)
138-
139-
assert a_bytes == b_bytes, f"{a_bytes=} != {b_bytes=}"
140-
141-
142114
# Mock I/O.
143115

144116

beets/test/helper.py

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,49 @@ def config(self) -> beets.IncludeLazyConfig:
163163
)
164164

165165

166-
class TestHelper(_common.Assertions, ConfigMixin):
166+
class IOMixin:
167+
@cached_property
168+
def io(self) -> _common.DummyIO:
169+
return _common.DummyIO()
170+
171+
def setUp(self):
172+
super().setUp()
173+
self.io.install()
174+
175+
def tearDown(self):
176+
super().tearDown()
177+
self.io.restore()
178+
179+
180+
class TestHelper(ConfigMixin):
167181
"""Helper mixin for high-level cli and plugin tests.
168182
169183
This mixin provides methods to isolate beets' global state provide
170184
fixtures.
171185
"""
172186

187+
resource_path = Path(os.fsdecode(_common.RSRC)) / "full.mp3"
188+
173189
db_on_disk: ClassVar[bool] = False
174190

191+
@cached_property
192+
def temp_dir_path(self) -> Path:
193+
return Path(self.create_temp_dir())
194+
195+
@cached_property
196+
def temp_dir(self) -> bytes:
197+
return util.bytestring_path(self.temp_dir_path)
198+
199+
@cached_property
200+
def lib_path(self) -> Path:
201+
lib_path = self.temp_dir_path / "libdir"
202+
lib_path.mkdir(exist_ok=True)
203+
return lib_path
204+
205+
@cached_property
206+
def libdir(self) -> bytes:
207+
return bytestring_path(self.lib_path)
208+
175209
# TODO automate teardown through hook registration
176210

177211
def setup_beets(self):
@@ -194,8 +228,7 @@ def setup_beets(self):
194228
195229
Make sure you call ``teardown_beets()`` afterwards.
196230
"""
197-
self.create_temp_dir()
198-
temp_dir_str = os.fsdecode(self.temp_dir)
231+
temp_dir_str = str(self.temp_dir_path)
199232
self.env_patcher = patch.dict(
200233
"os.environ",
201234
{
@@ -205,22 +238,16 @@ def setup_beets(self):
205238
)
206239
self.env_patcher.start()
207240

208-
self.libdir = os.path.join(self.temp_dir, b"libdir")
209-
os.mkdir(syspath(self.libdir))
210-
self.config["directory"] = os.fsdecode(self.libdir)
241+
self.config["directory"] = str(self.lib_path)
211242

212243
if self.db_on_disk:
213244
dbpath = util.bytestring_path(self.config["library"].as_filename())
214245
else:
215246
dbpath = ":memory:"
216247
self.lib = Library(dbpath, self.libdir)
217248

218-
# Initialize, but don't install, a DummyIO.
219-
self.io = _common.DummyIO()
220-
221249
def teardown_beets(self):
222250
self.env_patcher.stop()
223-
self.io.restore()
224251
self.lib._close()
225252
self.remove_temp_dir()
226253

@@ -384,16 +411,12 @@ def run_with_output(self, *args):
384411

385412
# Safe file operations
386413

387-
def create_temp_dir(self, **kwargs):
388-
"""Create a temporary directory and assign it into
389-
`self.temp_dir`. Call `remove_temp_dir` later to delete it.
390-
"""
391-
temp_dir = mkdtemp(**kwargs)
392-
self.temp_dir = util.bytestring_path(temp_dir)
414+
def create_temp_dir(self, **kwargs) -> str:
415+
return mkdtemp(**kwargs)
393416

394417
def remove_temp_dir(self):
395418
"""Delete the temporary directory created by `create_temp_dir`."""
396-
shutil.rmtree(syspath(self.temp_dir))
419+
shutil.rmtree(self.temp_dir_path)
397420

398421
def touch(self, path, dir=None, content=""):
399422
"""Create a file at `path` with given content.
@@ -514,7 +537,6 @@ class ImportHelper(TestHelper):
514537
autotagging library and several assertions for the library.
515538
"""
516539

517-
resource_path = syspath(os.path.join(_common.RSRC, b"full.mp3"))
518540
default_import_config = {
519541
"autotag": True,
520542
"copy": True,
@@ -531,7 +553,7 @@ class ImportHelper(TestHelper):
531553

532554
@cached_property
533555
def import_path(self) -> Path:
534-
import_path = Path(os.fsdecode(self.temp_dir)) / "import"
556+
import_path = self.temp_dir_path / "import"
535557
import_path.mkdir(exist_ok=True)
536558
return import_path
537559

@@ -599,7 +621,7 @@ def prepare_album_for_import(
599621
]
600622

601623
def prepare_albums_for_import(self, count: int = 1) -> None:
602-
album_dirs = Path(os.fsdecode(self.import_dir)).glob("album_*")
624+
album_dirs = self.import_path.glob("album_*")
603625
base_idx = int(str(max(album_dirs, default="0")).split("_")[-1]) + 1
604626

605627
for album_id in range(base_idx, count + base_idx):
@@ -623,21 +645,6 @@ def setup_importer(
623645
def setup_singleton_importer(self, **kwargs) -> ImportSession:
624646
return self.setup_importer(singletons=True, **kwargs)
625647

626-
def assert_file_in_lib(self, *segments):
627-
"""Join the ``segments`` and assert that this path exists in the
628-
library directory.
629-
"""
630-
self.assertExists(os.path.join(self.libdir, *segments))
631-
632-
def assert_file_not_in_lib(self, *segments):
633-
"""Join the ``segments`` and assert that this path does not
634-
exist in the library directory.
635-
"""
636-
self.assertNotExists(os.path.join(self.libdir, *segments))
637-
638-
def assert_lib_dir_empty(self):
639-
assert not os.listdir(syspath(self.libdir))
640-
641648

642649
class AsIsImporterMixin:
643650
def setUp(self):
@@ -759,7 +766,7 @@ def _add_choice_input(self):
759766
self._add_choice_input()
760767

761768

762-
class TerminalImportMixin(ImportHelper):
769+
class TerminalImportMixin(IOMixin, ImportHelper):
763770
"""Provides_a terminal importer for the import session."""
764771

765772
io: _common.DummyIO

beets/ui/commands.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2122,12 +2122,20 @@ def modify_func(lib, opts, args):
21222122

21232123

21242124
def move_items(
2125-
lib, dest, query, copy, album, pretend, confirm=False, export=False
2125+
lib,
2126+
dest_path: util.PathLike,
2127+
query,
2128+
copy,
2129+
album,
2130+
pretend,
2131+
confirm=False,
2132+
export=False,
21262133
):
21272134
"""Moves or copies items to a new base directory, given by dest. If
21282135
dest is None, then the library's base directory is used, making the
21292136
command "consolidate" files.
21302137
"""
2138+
dest = os.fsencode(dest_path) if dest_path else dest_path
21312139
items, albums = _do_query(lib, query, album, False)
21322140
objs = albums if album else items
21332141
num_objs = len(objs)

0 commit comments

Comments
 (0)