From f0a6059685f32d171c967d06a1a2c3a9563376f8 Mon Sep 17 00:00:00 2001 From: Trey Turner Date: Sat, 23 Aug 2025 08:37:22 -0500 Subject: [PATCH 1/3] feat(FtInTitle): support tracks by artists != album artist --- beetsplug/ftintitle.py | 56 +++++++++++++++++-------------- docs/changelog.rst | 3 ++ test/plugins/test_ftintitle.py | 60 +++++++++++++++++++++++++++++++--- 3 files changed, 89 insertions(+), 30 deletions(-) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index f0f088099..e17d7bc1c 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -26,14 +26,16 @@ if TYPE_CHECKING: from beets.library import Item -def split_on_feat(artist: str) -> tuple[str, str | None]: +def split_on_feat( + artist: str, for_artist: bool = True +) -> tuple[str, str | None]: """Given an artist string, split the "main" artist from any artist on the right-hand side of a string like "feat". Return the main artist, which is always a string, and the featuring artist, which may be a string or None if none is present. """ # split on the first "feat". - regex = re.compile(plugins.feat_tokens(), re.IGNORECASE) + regex = re.compile(plugins.feat_tokens(for_artist), re.IGNORECASE) parts = tuple(s.strip() for s in regex.split(artist, 1)) if len(parts) == 1: return parts[0], None @@ -53,32 +55,35 @@ def contains_feat(title: str) -> bool: ) -def find_feat_part(artist: str, albumartist: str) -> str | None: +def find_feat_part(artist: str, albumartist: str | None) -> str | None: """Attempt to find featured artists in the item's artist fields and return the results. Returns None if no featured artist found. """ - # Look for the album artist in the artist field. If it's not - # present, give up. - albumartist_split = artist.split(albumartist, 1) - if len(albumartist_split) <= 1: - return None + # Handle a wider variety of extraction cases if the album artist is + # contained within the track artist. + if albumartist and albumartist in artist: + albumartist_split = artist.split(albumartist, 1) - # If the last element of the split (the right-hand side of the - # album artist) is nonempty, then it probably contains the - # featured artist. - elif albumartist_split[1] != "": - # Extract the featured artist from the right-hand side. - _, feat_part = split_on_feat(albumartist_split[1]) - return feat_part + # If the last element of the split (the right-hand side of the + # album artist) is nonempty, then it probably contains the + # featured artist. + if albumartist_split[1] != "": + # Extract the featured artist from the right-hand side. + _, feat_part = split_on_feat(albumartist_split[1]) + return feat_part - # Otherwise, if there's nothing on the right-hand side, look for a - # featuring artist on the left-hand side. - else: - lhs, rhs = split_on_feat(albumartist_split[0]) - if lhs: - return lhs + # Otherwise, if there's nothing on the right-hand side, + # look for a featuring artist on the left-hand side. + else: + lhs, _ = split_on_feat(albumartist_split[0]) + if lhs: + return lhs - return None + # Fall back to conservative handling of the track artist without relying + # on albumartist, which covers compilations using a 'Various Artists' + # albumartist and album tracks by a guest artist featuring a third artist. + _, feat_part = split_on_feat(artist, False) + return feat_part class FtInTitlePlugin(plugins.BeetsPlugin): @@ -153,8 +158,9 @@ class FtInTitlePlugin(plugins.BeetsPlugin): "artist: {.artist} (Not changing due to keep_in_artist)", item ) else: - self._log.info("artist: {0.artist} -> {0.albumartist}", item) - item.artist = item.albumartist + track_artist, _ = split_on_feat(item.artist) + self._log.info("artist: {0.artist} -> {1}", item, track_artist) + item.artist = track_artist if item.artist_sort: # Just strip the featured artist from the sort name. @@ -187,7 +193,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin): # Check whether there is a featured artist on this track and the # artist field does not exactly match the album artist field. In # that case, we attempt to move the featured artist to the title. - if not albumartist or albumartist == artist: + if albumartist and artist == albumartist: return False _, featured = split_on_feat(artist) diff --git a/docs/changelog.rst b/docs/changelog.rst index 63a8fe339..c2d6ea1cf 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -161,6 +161,9 @@ Other changes: Autogenerated API references are now located in the ``docs/api`` subdirectory. - :doc:`/plugins/substitute`: Fix rST formatting for example cases so that each case is shown on separate lines. +- :doc:`/plugins/ftintitle`: Process items whose albumartist is not contained in + the artist field, including compilations using Various Artists as an + albumartist and album tracks by guest artists featuring a third artist. - Refactored library.py file by splitting it into multiple modules within the beets/library directory. - Added a test to check that all plugins can be imported without errors. diff --git a/test/plugins/test_ftintitle.py b/test/plugins/test_ftintitle.py index 572431b45..e049fe32a 100644 --- a/test/plugins/test_ftintitle.py +++ b/test/plugins/test_ftintitle.py @@ -40,6 +40,43 @@ class FtInTitlePluginFunctional(PluginTestCase): self.config["ftintitle"]["auto"] = auto self.config["ftintitle"]["keep_in_artist"] = keep_in_artist + def test_functional_no_featured_artist(self): + item = self._ft_add_item("/", "Alice", "Song 1", "Alice") + self.run_command("ftintitle") + item.load() + assert item["artist"] == "Alice" + assert item["title"] == "Song 1" + + def test_functional_no_albumartist(self): + self._ft_set_config("feat {0}") + item = self._ft_add_item("/", "Alice ft. Bob", "Song 1", None) + self.run_command("ftintitle") + item.load() + assert item["artist"] == "Alice" + assert item["title"] == "Song 1 feat Bob" + + def test_functional_no_albumartist_no_feature(self): + item = self._ft_add_item("/", "Alice", "Song 1", None) + self.run_command("ftintitle") + item.load() + assert item["artist"] == "Alice" + assert item["title"] == "Song 1" + + def test_functional_guest_artist(self): + self._ft_set_config("featuring {0}") + item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "George") + self.run_command("ftintitle") + item.load() + assert item["artist"] == "Alice" + assert item["title"] == "Song 1 featuring Bob" + + def test_functional_guest_artist_no_feature(self): + item = self._ft_add_item("/", "Alice", "Song 1", "George") + self.run_command("ftintitle") + item.load() + assert item["artist"] == "Alice" + assert item["title"] == "Song 1" + def test_functional_drop(self): item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "Alice") self.run_command("ftintitle", "-d") @@ -47,12 +84,25 @@ class FtInTitlePluginFunctional(PluginTestCase): assert item["artist"] == "Alice" assert item["title"] == "Song 1" - def test_functional_not_found(self): + def test_functional_drop_no_featured_artist(self): + item = self._ft_add_item("/", "Alice", "Song 1", "Alice") + self.run_command("ftintitle", "-d") + item.load() + assert item["artist"] == "Alice" + assert item["title"] == "Song 1" + + def test_functional_drop_guest_artist(self): item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "George") self.run_command("ftintitle", "-d") item.load() - # item should be unchanged - assert item["artist"] == "Alice ft Bob" + assert item["artist"] == "Alice" + assert item["title"] == "Song 1" + + def test_functional_drop_guest_artist_no_feature(self): + item = self._ft_add_item("/", "Alice", "Song 1", "George") + self.run_command("ftintitle", "-d") + item.load() + assert item["artist"] == "Alice" assert item["title"] == "Song 1" def test_functional_custom_format(self): @@ -147,7 +197,7 @@ class FtInTitlePluginTest(unittest.TestCase): { "artist": "Alice ft. Carol", "album_artist": "Bob", - "feat_part": None, + "feat_part": "Carol", }, ] @@ -155,7 +205,7 @@ class FtInTitlePluginTest(unittest.TestCase): feat_part = ftintitle.find_feat_part( test_case["artist"], test_case["album_artist"] ) - assert feat_part == test_case["feat_part"] + assert feat_part == test_case["feat_part"], f"failed: {test_case}" def test_split_on_feat(self): parts = ftintitle.split_on_feat("Alice ft. Bob") From 6ad7c5489c1d1c1d6ca7e86b32e0f3497796bd48 Mon Sep 17 00:00:00 2001 From: Trey Turner Date: Thu, 18 Sep 2025 23:00:30 -0500 Subject: [PATCH 2/3] test(ftintitle): parameterize tests --- test/plugins/test_ftintitle.py | 435 +++++++++++++++++---------------- 1 file changed, 225 insertions(+), 210 deletions(-) diff --git a/test/plugins/test_ftintitle.py b/test/plugins/test_ftintitle.py index e049fe32a..2b560e01c 100644 --- a/test/plugins/test_ftintitle.py +++ b/test/plugins/test_ftintitle.py @@ -14,8 +14,11 @@ """Tests for the 'ftintitle' plugin.""" -import unittest +from typing import Dict, Optional +from _pytest.fixtures import SubRequest +import pytest +from beets.library.models import Item from beets.test.helper import PluginTestCase from beetsplug import ftintitle @@ -23,219 +26,231 @@ from beetsplug import ftintitle class FtInTitlePluginFunctional(PluginTestCase): plugin = "ftintitle" - def _ft_add_item(self, path, artist, title, aartist): - return self.add_item( - path=path, - artist=artist, - artist_sort=artist, - title=title, - albumartist=aartist, - ) - def _ft_set_config( - self, ftformat, drop=False, auto=True, keep_in_artist=False - ): - self.config["ftintitle"]["format"] = ftformat - self.config["ftintitle"]["drop"] = drop - self.config["ftintitle"]["auto"] = auto - self.config["ftintitle"]["keep_in_artist"] = keep_in_artist - - def test_functional_no_featured_artist(self): - item = self._ft_add_item("/", "Alice", "Song 1", "Alice") - self.run_command("ftintitle") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1" - - def test_functional_no_albumartist(self): - self._ft_set_config("feat {0}") - item = self._ft_add_item("/", "Alice ft. Bob", "Song 1", None) - self.run_command("ftintitle") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1 feat Bob" - - def test_functional_no_albumartist_no_feature(self): - item = self._ft_add_item("/", "Alice", "Song 1", None) - self.run_command("ftintitle") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1" - - def test_functional_guest_artist(self): - self._ft_set_config("featuring {0}") - item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "George") - self.run_command("ftintitle") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1 featuring Bob" - - def test_functional_guest_artist_no_feature(self): - item = self._ft_add_item("/", "Alice", "Song 1", "George") - self.run_command("ftintitle") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1" - - def test_functional_drop(self): - item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "Alice") - self.run_command("ftintitle", "-d") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1" - - def test_functional_drop_no_featured_artist(self): - item = self._ft_add_item("/", "Alice", "Song 1", "Alice") - self.run_command("ftintitle", "-d") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1" - - def test_functional_drop_guest_artist(self): - item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "George") - self.run_command("ftintitle", "-d") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1" - - def test_functional_drop_guest_artist_no_feature(self): - item = self._ft_add_item("/", "Alice", "Song 1", "George") - self.run_command("ftintitle", "-d") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1" - - def test_functional_custom_format(self): - self._ft_set_config("feat. {}") - item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "Alice") - self.run_command("ftintitle") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1 feat. Bob" - - self._ft_set_config("featuring {}") - item = self._ft_add_item("/", "Alice feat. Bob", "Song 1", "Alice") - self.run_command("ftintitle") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1 featuring Bob" - - self._ft_set_config("with {}") - item = self._ft_add_item("/", "Alice feat Bob", "Song 1", "Alice") - self.run_command("ftintitle") - item.load() - assert item["artist"] == "Alice" - assert item["title"] == "Song 1 with Bob" - - def test_functional_keep_in_artist(self): - self._ft_set_config("feat. {}", keep_in_artist=True) - item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "Alice") - self.run_command("ftintitle") - item.load() - assert item["artist"] == "Alice ft Bob" - assert item["title"] == "Song 1 feat. Bob" - - item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "Alice") - self.run_command("ftintitle", "-d") - item.load() - assert item["artist"] == "Alice ft Bob" - assert item["title"] == "Song 1" +@pytest.fixture +def env(request: SubRequest) -> FtInTitlePluginFunctional: + case = FtInTitlePluginFunctional(methodName="runTest") + case.setUp() + request.addfinalizer(case.tearDown) + return case -class FtInTitlePluginTest(unittest.TestCase): - def setUp(self): - """Set up configuration""" - ftintitle.FtInTitlePlugin() +def set_config( + env: FtInTitlePluginFunctional, cfg: Optional[Dict[str, str | bool]] +) -> None: + cfg = {} if cfg is None else cfg + defaults = { + "drop": False, + "auto": True, + "keep_in_artist": False, + } + env.config["ftintitle"].set(defaults) + env.config["ftintitle"].set(cfg) - def test_find_feat_part(self): - test_cases = [ - { - "artist": "Alice ft. Bob", - "album_artist": "Alice", - "feat_part": "Bob", - }, - { - "artist": "Alice feat Bob", - "album_artist": "Alice", - "feat_part": "Bob", - }, - { - "artist": "Alice featuring Bob", - "album_artist": "Alice", - "feat_part": "Bob", - }, - { - "artist": "Alice & Bob", - "album_artist": "Alice", - "feat_part": "Bob", - }, - { - "artist": "Alice and Bob", - "album_artist": "Alice", - "feat_part": "Bob", - }, - { - "artist": "Alice With Bob", - "album_artist": "Alice", - "feat_part": "Bob", - }, - { - "artist": "Alice defeat Bob", - "album_artist": "Alice", - "feat_part": None, - }, - { - "artist": "Alice & Bob", - "album_artist": "Bob", - "feat_part": "Alice", - }, - { - "artist": "Alice ft. Bob", - "album_artist": "Bob", - "feat_part": "Alice", - }, - { - "artist": "Alice ft. Carol", - "album_artist": "Bob", - "feat_part": "Carol", - }, - ] - for test_case in test_cases: - feat_part = ftintitle.find_feat_part( - test_case["artist"], test_case["album_artist"] - ) - assert feat_part == test_case["feat_part"], f"failed: {test_case}" +def add_item( + env: FtInTitlePluginFunctional, + path: str, + artist: str, + title: str, + albumartist: str, +) -> Item: + return env.add_item( + path=path, + artist=artist, + artist_sort=artist, + title=title, + albumartist=albumartist, + ) - def test_split_on_feat(self): - parts = ftintitle.split_on_feat("Alice ft. Bob") - assert parts == ("Alice", "Bob") - parts = ftintitle.split_on_feat("Alice feat Bob") - assert parts == ("Alice", "Bob") - parts = ftintitle.split_on_feat("Alice feat. Bob") - assert parts == ("Alice", "Bob") - parts = ftintitle.split_on_feat("Alice featuring Bob") - assert parts == ("Alice", "Bob") - parts = ftintitle.split_on_feat("Alice & Bob") - assert parts == ("Alice", "Bob") - parts = ftintitle.split_on_feat("Alice and Bob") - assert parts == ("Alice", "Bob") - parts = ftintitle.split_on_feat("Alice With Bob") - assert parts == ("Alice", "Bob") - parts = ftintitle.split_on_feat("Alice defeat Bob") - assert parts == ("Alice defeat Bob", None) - def test_contains_feat(self): - assert ftintitle.contains_feat("Alice ft. Bob") - assert ftintitle.contains_feat("Alice feat. Bob") - assert ftintitle.contains_feat("Alice feat Bob") - assert ftintitle.contains_feat("Alice featuring Bob") - assert ftintitle.contains_feat("Alice (ft. Bob)") - assert ftintitle.contains_feat("Alice (feat. Bob)") - assert ftintitle.contains_feat("Alice [ft. Bob]") - assert ftintitle.contains_feat("Alice [feat. Bob]") - assert not ftintitle.contains_feat("Alice defeat Bob") - assert not ftintitle.contains_feat("Aliceft.Bob") - assert not ftintitle.contains_feat("Alice (defeat Bob)") - assert not ftintitle.contains_feat("Live and Let Go") - assert not ftintitle.contains_feat("Come With Me") +@pytest.mark.parametrize( + "cfg, cmd_args, input, expected", + [ + pytest.param( + None, + ("ftintitle",), + ("Alice", "Song 1", "Alice"), + ("Alice", "Song 1"), + id="no-featured-artist", + ), + pytest.param( + {"format": "feat {0}"}, + ("ftintitle",), + ("Alice ft. Bob", "Song 1", None), + ("Alice", "Song 1 feat Bob"), + id="no-albumartist-custom-format", + ), + pytest.param( + None, + ("ftintitle",), + ("Alice", "Song 1", None), + ("Alice", "Song 1"), + id="no-albumartist-no-feature", + ), + pytest.param( + {"format": "featuring {0}"}, + ("ftintitle",), + ("Alice ft Bob", "Song 1", "George"), + ("Alice", "Song 1 featuring Bob"), + id="guest-artist-custom-format", + ), + pytest.param( + None, + ("ftintitle",), + ("Alice", "Song 1", "George"), + ("Alice", "Song 1"), + id="guest-artist-no-feature", + ), + # ---- drop (-d) variants ---- + pytest.param( + None, + ("ftintitle", "-d"), + ("Alice ft Bob", "Song 1", "Alice"), + ("Alice", "Song 1"), + id="drop-self-ft", + ), + pytest.param( + None, + ("ftintitle", "-d"), + ("Alice", "Song 1", "Alice"), + ("Alice", "Song 1"), + id="drop-self-no-ft", + ), + pytest.param( + None, + ("ftintitle", "-d"), + ("Alice ft Bob", "Song 1", "George"), + ("Alice", "Song 1"), + id="drop-guest-ft", + ), + pytest.param( + None, + ("ftintitle", "-d"), + ("Alice", "Song 1", "George"), + ("Alice", "Song 1"), + id="drop-guest-no-ft", + ), + # ---- custom format variants ---- + pytest.param( + {"format": "feat. {}"}, + ("ftintitle",), + ("Alice ft Bob", "Song 1", "Alice"), + ("Alice", "Song 1 feat. Bob"), + id="custom-format-feat-dot", + ), + pytest.param( + {"format": "featuring {}"}, + ("ftintitle",), + ("Alice feat. Bob", "Song 1", "Alice"), + ("Alice", "Song 1 featuring Bob"), + id="custom-format-featuring", + ), + pytest.param( + {"format": "with {}"}, + ("ftintitle",), + ("Alice feat Bob", "Song 1", "Alice"), + ("Alice", "Song 1 with Bob"), + id="custom-format-with", + ), + # ---- keep_in_artist variants ---- + pytest.param( + {"format": "feat. {}", "keep_in_artist": True}, + ("ftintitle",), + ("Alice ft Bob", "Song 1", "Alice"), + ("Alice ft Bob", "Song 1 feat. Bob"), + id="keep-in-artist-add-to-title", + ), + pytest.param( + {"format": "feat. {}", "keep_in_artist": True}, + ("ftintitle", "-d"), + ("Alice ft Bob", "Song 1", "Alice"), + ("Alice ft Bob", "Song 1"), + id="keep-in-artist-drop-from-title", + ), + ], +) +def test_ftintitle_functional( + env: FtInTitlePluginFunctional, + cfg: Optional[Dict[str, str | bool]], + cmd_args: tuple[str, ...], + input: tuple[str, str, str], + expected: tuple[str, str], +) -> None: + set_config(env, cfg) + ftintitle.FtInTitlePlugin() + + artist, title, albumartist = input + item = add_item(env, "/", artist, title, albumartist) + + env.run_command(*cmd_args) + item.load() + + expected_artist, expected_title = expected + assert item["artist"] == expected_artist + assert item["title"] == expected_title + + +@pytest.mark.parametrize( + "artist,albumartist,expected", + [ + ("Alice ft. Bob", "Alice", "Bob"), + ("Alice feat Bob", "Alice", "Bob"), + ("Alice featuring Bob", "Alice", "Bob"), + ("Alice & Bob", "Alice", "Bob"), + ("Alice and Bob", "Alice", "Bob"), + ("Alice With Bob", "Alice", "Bob"), + ("Alice defeat Bob", "Alice", None), + ("Alice & Bob", "Bob", "Alice"), + ("Alice ft. Bob", "Bob", "Alice"), + ("Alice ft. Carol", "Bob", "Carol"), + ], +) +def test_find_feat_part( + artist: str, + albumartist: str, + expected: Optional[str], +) -> None: + assert ftintitle.find_feat_part(artist, albumartist) == expected + + +@pytest.mark.parametrize( + "input,expected", + [ + ("Alice ft. Bob", ("Alice", "Bob")), + ("Alice feat Bob", ("Alice", "Bob")), + ("Alice feat. Bob", ("Alice", "Bob")), + ("Alice featuring Bob", ("Alice", "Bob")), + ("Alice & Bob", ("Alice", "Bob")), + ("Alice and Bob", ("Alice", "Bob")), + ("Alice With Bob", ("Alice", "Bob")), + ("Alice defeat Bob", ("Alice defeat Bob", None)), + ], +) +def test_split_on_feat( + input: str, + expected: tuple[str, Optional[str]], +) -> None: + assert ftintitle.split_on_feat(input) == expected + + +@pytest.mark.parametrize( + "input,expected", + [ + ("Alice ft. Bob", True), + ("Alice feat. Bob", True), + ("Alice feat Bob", True), + ("Alice featuring Bob", True), + ("Alice (ft. Bob)", True), + ("Alice (feat. Bob)", True), + ("Alice [ft. Bob]", True), + ("Alice [feat. Bob]", True), + ("Alice defeat Bob", False), + ("Aliceft.Bob", False), + ("Alice (defeat Bob)", False), + ("Live and Let Go", False), + ("Come With Me", False), + ], +) +def test_contains_feat(input: str, expected: bool) -> None: + assert ftintitle.contains_feat(input) is expected From 042b5d64ebceb22cbcf5dbdf1745b7d9f0125b80 Mon Sep 17 00:00:00 2001 From: Trey Turner Date: Thu, 18 Sep 2025 23:18:01 -0500 Subject: [PATCH 3/3] test(ftintitle): fix flake, massage mypy --- test/plugins/test_ftintitle.py | 42 ++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/test/plugins/test_ftintitle.py b/test/plugins/test_ftintitle.py index 2b560e01c..005318b11 100644 --- a/test/plugins/test_ftintitle.py +++ b/test/plugins/test_ftintitle.py @@ -14,8 +14,8 @@ """Tests for the 'ftintitle' plugin.""" -from typing import Dict, Optional -from _pytest.fixtures import SubRequest +from typing import Dict, Generator, Optional, Tuple, Union + import pytest from beets.library.models import Item @@ -28,15 +28,17 @@ class FtInTitlePluginFunctional(PluginTestCase): @pytest.fixture -def env(request: SubRequest) -> FtInTitlePluginFunctional: +def env() -> Generator[FtInTitlePluginFunctional, None, None]: case = FtInTitlePluginFunctional(methodName="runTest") case.setUp() - request.addfinalizer(case.tearDown) - return case + try: + yield case + finally: + case.tearDown() def set_config( - env: FtInTitlePluginFunctional, cfg: Optional[Dict[str, str | bool]] + env: FtInTitlePluginFunctional, cfg: Optional[Dict[str, Union[str, bool]]] ) -> None: cfg = {} if cfg is None else cfg defaults = { @@ -53,7 +55,7 @@ def add_item( path: str, artist: str, title: str, - albumartist: str, + albumartist: Optional[str], ) -> Item: return env.add_item( path=path, @@ -65,7 +67,7 @@ def add_item( @pytest.mark.parametrize( - "cfg, cmd_args, input, expected", + "cfg, cmd_args, given, expected", [ pytest.param( None, @@ -172,15 +174,15 @@ def add_item( ) def test_ftintitle_functional( env: FtInTitlePluginFunctional, - cfg: Optional[Dict[str, str | bool]], - cmd_args: tuple[str, ...], - input: tuple[str, str, str], - expected: tuple[str, str], + cfg: Optional[Dict[str, Union[str, bool]]], + cmd_args: Tuple[str, ...], + given: Tuple[str, str, Optional[str]], + expected: Tuple[str, str], ) -> None: set_config(env, cfg) ftintitle.FtInTitlePlugin() - artist, title, albumartist = input + artist, title, albumartist = given item = add_item(env, "/", artist, title, albumartist) env.run_command(*cmd_args) @@ -215,7 +217,7 @@ def test_find_feat_part( @pytest.mark.parametrize( - "input,expected", + "given,expected", [ ("Alice ft. Bob", ("Alice", "Bob")), ("Alice feat Bob", ("Alice", "Bob")), @@ -228,14 +230,14 @@ def test_find_feat_part( ], ) def test_split_on_feat( - input: str, - expected: tuple[str, Optional[str]], + given: str, + expected: Tuple[str, Optional[str]], ) -> None: - assert ftintitle.split_on_feat(input) == expected + assert ftintitle.split_on_feat(given) == expected @pytest.mark.parametrize( - "input,expected", + "given,expected", [ ("Alice ft. Bob", True), ("Alice feat. Bob", True), @@ -252,5 +254,5 @@ def test_split_on_feat( ("Come With Me", False), ], ) -def test_contains_feat(input: str, expected: bool) -> None: - assert ftintitle.contains_feat(input) is expected +def test_contains_feat(given: str, expected: bool) -> None: + assert ftintitle.contains_feat(given) is expected