Rewrite tests, add cached_property decorators, add seperator feature

This commit is contained in:
Henry 2025-11-22 00:16:33 -08:00
parent df1ef40790
commit 83c16cbb5d
4 changed files with 432 additions and 327 deletions

View file

@ -17,7 +17,8 @@ Title case logic is derived from the python-titlecase library.
Provides a template function and a tag modification function.""" Provides a template function and a tag modification function."""
import re import re
from typing import Optional from functools import cached_property
from typing import TypedDict
from titlecase import titlecase from titlecase import titlecase
@ -31,26 +32,22 @@ __author__ = "henryoberholtzer@gmail.com"
__version__ = "1.0" __version__ = "1.0"
class TitlecasePlugin(BeetsPlugin): class PreservedText(TypedDict):
preserve: dict[str, str] = {} words: dict[str, str]
preserve_phrases: dict[str, re.Pattern[str]] = {} phrases: dict[str, re.Pattern[str]]
force_lowercase: bool = True
fields_to_process: set[str] = set()
the_artist: bool = True
the_artist_regexp = re.compile(r"\bthe\b")
class TitlecasePlugin(BeetsPlugin):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
# Register template function
self.template_funcs["titlecase"] = self.titlecase # type: ignore
self.config.add( self.config.add(
{ {
"auto": True, "auto": True,
"preserve": [], "preserve": [],
"fields": [], "fields": [],
"replace": [], "replace": [],
"seperators": [],
"force_lowercase": False, "force_lowercase": False,
"small_first_last": True, "small_first_last": True,
"the_artist": True, "the_artist": True,
@ -63,6 +60,7 @@ class TitlecasePlugin(BeetsPlugin):
preserve - Provide a list of strings with specific case requirements. preserve - Provide a list of strings with specific case requirements.
fields - Fields to apply titlecase to. fields - Fields to apply titlecase to.
replace - List of pairs, first is the target, second is the replacement replace - List of pairs, first is the target, second is the replacement
seperators - Other characters to treat like periods.
force_lowercase - Lowercases the string before titlecasing. force_lowercase - Lowercases the string before titlecasing.
small_first_last - If small characters should be cased at the start of strings. small_first_last - If small characters should be cased at the start of strings.
the_artist - If the plugin infers the field to be an artist field the_artist - If the plugin infers the field to be an artist field
@ -71,6 +69,8 @@ class TitlecasePlugin(BeetsPlugin):
that start with 'The', like 'The Who' or 'The Talking Heads' when that start with 'The', like 'The Who' or 'The Talking Heads' when
they are not at the start of a string. Superceded by preserved phrases. they are not at the start of a string. Superceded by preserved phrases.
""" """
# Register template function
self.template_funcs["titlecase"] = self.titlecase # type: ignore
# Register UI subcommands # Register UI subcommands
self._command = ui.Subcommand( self._command = ui.Subcommand(
@ -78,8 +78,7 @@ class TitlecasePlugin(BeetsPlugin):
help="Apply titlecasing to metadata specified in config.", help="Apply titlecasing to metadata specified in config.",
) )
self._get_config() if self.config["auto"].get(bool):
if self.config["auto"]:
if self.config["after_choice"].get(bool): if self.config["after_choice"].get(bool):
self.import_stages = [self.imported] self.import_stages = [self.imported]
else: else:
@ -90,37 +89,56 @@ class TitlecasePlugin(BeetsPlugin):
"albuminfo_received", self.received_info_handler "albuminfo_received", self.received_info_handler
) )
def _get_config(self): @cached_property
self.force_lowercase = self.config["force_lowercase"].get(bool) def force_lowercase(self) -> bool:
self.replace = self.config["replace"].as_pairs() return self.config["force_lowercase"].get(bool)
self.the_artist = self.config["the_artist"].get(bool)
self._preserve_words(self.config["preserve"].as_str_seq())
self._initialize_fields(
self.config["fields"].as_str_seq(),
)
def _initialize_fields(self, fields: list[str]) -> None: @cached_property
"""Creates the set for fields to process in tagging.""" def replace(self) -> list[tuple[str, str]]:
if fields: return self.config["replace"].as_pairs()
self.fields_to_process = set(fields)
self._log.debug(
f"set fields to process: {', '.join(self.fields_to_process)}"
)
else:
self._log.debug("no fields specified!")
def _preserve_words(self, preserve: list[str]) -> None: @cached_property
for word in preserve: def the_artist(self) -> bool:
if " " in word: return self.config["the_artist"].get(bool)
self.preserve_phrases[word] = re.compile(
rf"\b{re.escape(word)}\b", re.IGNORECASE @cached_property
def fields_to_process(self) -> set[str]:
fields = set(self.config["fields"].as_str_seq())
self._log.debug(f"fields: {', '.join(fields)}")
return fields
@cached_property
def preserve(self) -> PreservedText:
strings = self.config["preserve"].as_str_seq()
preserved: PreservedText = {"words": {}, "phrases": {}}
for s in strings:
if " " in s:
preserved["phrases"][s] = re.compile(
rf"\b{re.escape(s)}\b", re.IGNORECASE
) )
else: else:
self.preserve[word.upper()] = word preserved["words"][s.upper()] = s
return preserved
def _preserved(self, word, **kwargs) -> Optional[str]: @cached_property
def seperators(self) -> re.Pattern[str] | None:
if seperators := "".join(
dict.fromkeys(self.config["seperators"].as_str_seq())
):
return re.compile(rf"(.*?[{re.escape(seperators)}]+)(\s*)(?=.)")
return None
@cached_property
def small_first_last(self) -> bool:
return self.config["small_first_last"].get(bool)
@cached_property
def the_artist_regexp(self) -> re.Pattern[str]:
return re.compile(r"\bthe\b")
def titlecase_callback(self, word, **kwargs) -> str | None:
"""Callback function for words to preserve case of.""" """Callback function for words to preserve case of."""
if preserved_word := self.preserve.get(word.upper(), ""): if preserved_word := self.preserve["words"].get(word.upper(), ""):
return preserved_word return preserved_word
return None return None
@ -146,7 +164,7 @@ class TitlecasePlugin(BeetsPlugin):
self._command.func = func self._command.func = func
return [self._command] return [self._command]
def titlecase_fields(self, item: Item | Info): def titlecase_fields(self, item: Item | Info) -> None:
"""Applies titlecase to fields, except """Applies titlecase to fields, except
those excluded by the default exclusions and the those excluded by the default exclusions and the
set exclude lists. set exclude lists.
@ -178,6 +196,17 @@ class TitlecasePlugin(BeetsPlugin):
def titlecase(self, text: str, field: str = "") -> str: def titlecase(self, text: str, field: str = "") -> str:
"""Titlecase the given text.""" """Titlecase the given text."""
# Check we should split this into two substrings.
if self.seperators:
if len(splits := self.seperators.findall(text)):
print(splits)
split_cased = "".join(
[self.titlecase(s[0], field) + s[1] for s in splits]
)
# Add on the remaining portion
return split_cased + self.titlecase(
text[len(split_cased) :], field
)
# Any necessary replacements go first, mainly punctuation. # Any necessary replacements go first, mainly punctuation.
titlecased = text.lower() if self.force_lowercase else text titlecased = text.lower() if self.force_lowercase else text
for pair in self.replace: for pair in self.replace:
@ -186,14 +215,14 @@ class TitlecasePlugin(BeetsPlugin):
# General titlecase operation # General titlecase operation
titlecased = titlecase( titlecased = titlecase(
titlecased, titlecased,
small_first_last=self.config["small_first_last"], small_first_last=self.small_first_last,
callback=self._preserved, callback=self.titlecase_callback,
) )
# Apply "The Artist" feature # Apply "The Artist" feature
if self.the_artist and "artist" in field: if self.the_artist and "artist" in field:
titlecased = self.the_artist_regexp.sub("The", titlecased) titlecased = self.the_artist_regexp.sub("The", titlecased)
# More complicated phrase replacements. # More complicated phrase replacements.
for phrase, regexp in self.preserve_phrases.items(): for phrase, regexp in self.preserve["phrases"].items():
titlecased = regexp.sub(phrase, titlecased) titlecased = regexp.sub(phrase, titlecased)
return titlecased return titlecased

View file

@ -26,6 +26,8 @@ New features:
- :doc:`plugins/mbpseudo`: Add a new `mbpseudo` plugin to proactively receive - :doc:`plugins/mbpseudo`: Add a new `mbpseudo` plugin to proactively receive
MusicBrainz pseudo-releases as recommendations during import. MusicBrainz pseudo-releases as recommendations during import.
- Added support for Python 3.13. - Added support for Python 3.13.
- :doc:`plugins/titlecase`: Add the `titlecase` plugin to allow users to
resolve differences in metadata source styles.
Bug fixes: Bug fixes:

View file

@ -54,9 +54,10 @@ Default
titlecase: titlecase:
auto: yes auto: yes
fields: fields: []
preserve: preserve: []
replace: replace: []
seperators: []
force_lowercase: no force_lowercase: no
small_first_last: yes small_first_last: yes
the_artist: yes the_artist: yes
@ -68,41 +69,62 @@ Default
Whether to automatically apply titlecase to new imports. Whether to automatically apply titlecase to new imports.
.. conf:: fields .. conf:: fields
:default: []
A list of fields to apply the titlecase logic to. You must specify the fields A list of fields to apply the titlecase logic to. You must specify the fields
you want to have modified in order for titlecase to apply changes to metadata. you want to have modified in order for titlecase to apply changes to metadata.
A good starting point is below, which will titlecase artists, album and track titles. A good starting point is below, which will titlecase album titles, track titles, and all artist fields.
.. code-block:: yaml .. code-block:: yaml
fields: titlecase:
- album fields:
- albumartist - album
- albumartist_credit - title
- albumartist_sort - albumartist
- albumartists - albumartist_credit
- albumartists_credit - albumartist_sort
- albumartists_sort - albumartists
- artist - albumartists_credit
- artist_credit - albumartists_sort
- artist_sort - artist
- artists - artist_credit
- artists_credit - artist_sort
- artists_sort - artists
- title - artists_credit
- artists_sort
.. conf:: preserve .. conf:: preserve
:default: []
List of words and phrases to preserve the case of. Without specifying ``DJ`` on List of words and phrases to preserve the case of. Without specifying ``DJ`` on
the list, titlecase will format it as ``Dj``, or specify ``The Beatles`` to make sure the list, titlecase will format it as ``Dj``, or specify ``The Beatles`` to make sure
``With The Beatles`` is not capitalized as ``With the Beatles``. ``With The Beatles`` is not capitalized as ``With the Beatles``.
.. conf:: replace .. conf:: replace
:default: []
The replace function takes place before any titlecasing occurs, and is intended to The replace function takes place before any titlecasing occurs, and is intended to
help normalize differences in puncuation styles. It accepts a list of tuples, with help normalize differences in puncuation styles. It accepts a list of tuples, with
the first being the target, and the second being the replacement the first being the target, and the second being the replacement.
An example configuration that enforces one style of quotation mark is below.
.. code-block:: yaml
titlecase:
replace:
- "": "'"
- "": "'"
- "“": '"'
- "”": '"'
.. conf:: seperators
:default: []
A list of characters to treat as markers of new sentences. Helpful for split titles
that might otherwise have a lowercase letter at the start of the second string.
.. conf:: force_lowercase .. conf:: force_lowercase
:default: no :default: no
@ -111,23 +133,26 @@ Default
problems with all caps acronyms titlecase would otherwise recognize. problems with all caps acronyms titlecase would otherwise recognize.
.. conf:: small_first_last .. conf:: small_first_last
:default: yes
An option from the base titlecase library. Controls capitalizing small words at the start An option from the base titlecase library. Controls capitalizing small words at the start
of a sentence. With this turned off ``a`` and similar words will not be capitalized of a sentence. With this turned off ``a`` and similar words will not be capitalized
under any circumstance. under any circumstance.
.. conf:: the_artist .. conf:: the_artist
:default: yes
If a field name contains ``artist``, then any lowercase ``the`` will be If a field name contains ``artist``, then any lowercase ``the`` will be
capitalized. Useful for bands with `The` as part of the proper name, capitalized. Useful for bands with `The` as part of the proper name,
like ``Amyl and The Sniffers``. like ``Amyl and The Sniffers``.
.. conf:: after_choice .. conf:: after_choice
:default: no
By default, titlecase runs on the candidates that are received, adjusting them before By default, titlecase runs on the candidates that are received, adjusting them before
you make your selection and creating different weight calculations. If you'd rather you make your selection and creating different weight calculations. If you'd rather
see the data as recieved from the database, set this to true to run after you make see the data as recieved from the database, set this to true to run after you make
your tag choice. your tag choice.
Dangerous Fields Dangerous Fields
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~

View file

@ -14,96 +14,12 @@
"""Tests for the 'titlecase' plugin""" """Tests for the 'titlecase' plugin"""
import pytest
from beets.autotag.hooks import AlbumInfo, TrackInfo from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.library import Item from beets.library import Item
from beets.test.helper import PluginMixin from beets.test.helper import PluginTestCase
from beetsplug.titlecase import TitlecasePlugin from beetsplug.titlecase import TitlecasePlugin
titlecase_fields_testcases = [
@pytest.mark.parametrize(
"given, expected",
[
("a", "A"),
("PENDULUM", "Pendulum"),
("Aaron-carl", "Aaron-Carl"),
("LTJ bukem", "LTJ Bukem"),
(
"Freaky chakra Vs. Single Cell orchestra",
"Freaky Chakra vs. Single Cell Orchestra",
),
("(original mix)", "(Original Mix)"),
("ALL CAPS TITLE", "All Caps Title"),
],
)
def test_basic_titlecase(given, expected):
"""Assert that general behavior is as expected."""
assert TitlecasePlugin().titlecase(given) == expected
to_preserve = [
"easyFun",
"A.D.O.R",
"D'Angelo",
"ABBA",
"LaTeX",
"O.R.B",
"PinkPantheress",
]
@pytest.mark.parametrize("name", to_preserve)
def test_preserved_words(name):
"""Test using given strings to preserve case"""
t = TitlecasePlugin()
t._preserve_words(to_preserve)
assert t.titlecase(name.lower()) == name
assert t.titlecase(name.upper()) == name
def phrases_with_preserved_strings(phrases: list[str]) -> list[tuple[str, str]]:
def template(x):
return f"Example Phrase: Or {x} in Context!"
return [(template(p.lower()), template(p)) for p in phrases]
@pytest.mark.parametrize(
"given, expected", phrases_with_preserved_strings(to_preserve)
)
def test_preserved_phrases(given, expected):
t = TitlecasePlugin()
t._preserve_words(to_preserve)
assert t.titlecase(given.lower()) == expected
item_test_cases = [
(
{
"preserve": ["D'Angelo"],
"replace": [("", "'")],
"fields": ["artist", "albumartist", "mb_albumid"],
"force_lowercase": False,
"small_first_last": True,
},
Item(
artist="dangelo and the vanguard",
mb_albumid="ab140e13-7b36-402a-a528-b69e3dee38a8",
albumartist="dangelo",
format="CD",
album="the black messiah",
title="Till It's Done (Tutu)",
),
Item(
artist="D'Angelo and The Vanguard",
mb_albumid="Ab140e13-7b36-402a-A528-B69e3dee38a8",
albumartist="D'Angelo",
format="CD",
album="the black messiah",
title="Till It's Done (Tutu)",
),
),
( (
{ {
"fields": [ "fields": [
@ -115,7 +31,6 @@ item_test_cases = [
"year", "year",
], ],
"force_lowercase": True, "force_lowercase": True,
"small_first_last": True,
}, },
Item( Item(
artist="OPHIDIAN", artist="OPHIDIAN",
@ -134,179 +49,291 @@ item_test_cases = [
title="Khameleon", title="Khameleon",
), ),
), ),
(
{
"the_artist": True,
"preserve": ["PANTHER"],
"fields": ["artist", "artists", "discogs_artistid"],
"force_lowercase": False,
"small_first_last": True,
},
Item(
artist="pinkpantheress",
artists=["pinkpantheress", "artist_two"],
artists_ids=["aBcDeF32", "aBcDeF12"],
discogs_artistid=21,
),
Item(
artist="Pinkpantheress",
artists=["Pinkpantheress", "Artist_Two"],
artists_ids=["aBcDeF32", "aBcDeF12"],
discogs_artistid=21,
),
),
(
{
"the_artist": True,
"preserve": ["A Day in the Park"],
"fields": [
"artists",
"artist",
"artists_sorttitle",
"artists_ids",
],
},
Item(
artists_sort=["b-52s, the"],
artist="a day in the park",
artists=[
"vinylgroover & the red head",
"a day in the park",
"amyl and the sniffers",
],
artists_ids=["aBcDeF32", "aBcDeF12"],
),
Item(
artists_sort=["B-52s, The"],
artist="A Day in the Park",
artists=[
"Vinylgroover & The Red Head",
"A Day in The Park",
"Amyl and The Sniffers",
],
artists_ids=["ABcDeF32", "ABcDeF12"],
),
),
(
{
"the_artist": False,
"preserve": ["A Day in the Park"],
"fields": [
"artists",
"artist",
"artists_sorttitle",
"artists_ids",
],
},
Item(
artists_sort=["b-52s, the"],
artist="a day in the park",
artists=[
"vinylgroover & the red head",
"a day in the park",
"amyl and the sniffers",
],
artists_ids=["aBcDeF32", "aBcDeF12"],
),
Item(
artists_sort=["B-52s, The"],
artist="A Day in the Park",
artists=[
"Vinylgroover & the Red Head",
"A Day in the Park",
"Amyl and the Sniffers",
],
artists_ids=["ABcDeF32", "ABcDeF12"],
),
),
] ]
info_test_cases = [
( class TestTitlecasePlugin(PluginTestCase):
TrackInfo( plugin = "titlecase"
album="test album", preload_plugin = False
artist_credit="test artist credit",
artists=["artist one", "artist two"], def test_basic_titlecase(self):
), """Check that default behavior is as expected."""
TrackInfo( testcases = [
album="Test Album", ("a", "A"),
artist_credit="Test Artist Credit", ("PENDULUM", "Pendulum"),
artists=["Artist One", "Artist Two"], ("Aaron-carl", "Aaron-Carl"),
), ("LTJ bukem", "LTJ Bukem"),
), ("(original mix)", "(Original Mix)"),
( ("ALL CAPS TITLE", "All Caps Title"),
AlbumInfo( ]
tracks=[ for testcase in testcases:
given, expected = testcase
assert TitlecasePlugin().titlecase(given) == expected
def test_small_first_last(self):
"""Check the behavior for supporting small first last"""
testcases = [
(True, "In a Silent Way", "In a Silent Way"),
(False, "In a Silent Way", "in a Silent Way"),
]
for testcase in testcases:
sfl, given, expected = testcase
cfg = {"small_first_last": sfl}
with self.configure_plugin(cfg):
assert TitlecasePlugin().titlecase(given) == expected
def test_preserve(self):
"""Test using given strings to preserve case"""
preserve_list = [
"easyFun",
"A.D.O.R",
"D'Angelo",
"ABBA",
"LaTeX",
"O.R.B",
"PinkPantheress",
"THE PSYCHIC ED RUSH",
"LTJ Bukem",
]
for word in preserve_list:
with self.configure_plugin({"preserve": preserve_list}):
assert TitlecasePlugin().titlecase(word.upper()) == word
assert TitlecasePlugin().titlecase(word.lower()) == word
def test_seperators(self):
testcases = [
([], "it / a / in / of / to / the", "It / a / in / of / to / The"),
(["/"], "it / the test", "It / The Test"),
(
["/"],
"it / a / in / of / to / the",
"It / A / In / Of / To / The",
),
(["/"], "//it/a/in/of/to/the", "//It/A/In/Of/To/The"),
(
["/", ";", "|"],
"it ; a / in | of / to | the",
"It ; A / In | Of / To | The",
),
]
for testcase in testcases:
seperators, given, expected = testcase
with self.configure_plugin({"seperators": seperators}):
assert TitlecasePlugin().titlecase(given) == expected
def test_received_info_handler(self):
testcases = [
(
TrackInfo( TrackInfo(
album="test album", album="test album",
artist_credit="test artist credit", artist_credit="test artist credit",
artists=["artist one", "artist two"], artists=["artist one", "artist two"],
) ),
],
album="test album",
artist_credit="test artist credit",
artists=["artist one", "artist two"],
),
AlbumInfo(
tracks=[
TrackInfo( TrackInfo(
album="Test Album", album="Test Album",
artist_credit="Test Artist Credit", artist_credit="Test Artist Credit",
artists=["Artist One", "Artist Two"], artists=["Artist One", "Artist Two"],
) ),
], ),
album="Test Album", (
artist_credit="Test Artist Credit", AlbumInfo(
artists=["Artist One", "Artist Two"], tracks=[
), TrackInfo(
), album="test album",
] artist_credit="test artist credit",
artists=["artist one", "artist two"],
)
],
album="test album",
artist_credit="test artist credit",
artists=["artist one", "artist two"],
),
AlbumInfo(
tracks=[
TrackInfo(
album="Test Album",
artist_credit="Test Artist Credit",
artists=["Artist One", "Artist Two"],
)
],
album="Test Album",
artist_credit="Test Artist Credit",
artists=["Artist One", "Artist Two"],
),
),
]
cfg = {"fields": ["album", "artist_credit", "artists"]}
for testcase in testcases:
given, expected = testcase
with self.configure_plugin(cfg):
TitlecasePlugin().received_info_handler(given)
assert given == expected
def test_titlecase_fields(self):
testcases = [
# Test with preserve, replace, and mb_albumid
# Test with the_artist
(
{
"preserve": ["D'Angelo"],
"replace": [("", "'")],
"fields": ["artist", "albumartist", "mb_albumid"],
},
Item(
artist="dangelo and the vanguard",
mb_albumid="ab140e13-7b36-402a-a528-b69e3dee38a8",
albumartist="dangelo",
format="CD",
album="the black messiah",
title="Till It's Done (Tutu)",
),
Item(
artist="D'Angelo and The Vanguard",
mb_albumid="Ab140e13-7b36-402a-A528-B69e3dee38a8",
albumartist="D'Angelo",
format="CD",
album="the black messiah",
title="Till It's Done (Tutu)",
),
),
# Test with force_lowercase, preserve, and an incorrect field
(
{
"force_lowercase": True,
"fields": [
"artist",
"albumartist",
"format",
"title",
"year",
"label",
"format",
"INCORRECT_FIELD",
],
"preserve": ["CD"],
},
Item(
artist="OPHIDIAN",
albumartist="OphiDIAN",
format="cd",
year=2003,
album="BLACKBOX",
title="KhAmElEoN",
label="enzyme records",
),
Item(
artist="Ophidian",
albumartist="Ophidian",
format="CD",
year=2003,
album="Blackbox",
title="Khameleon",
label="Enzyme Records",
),
),
# Test with no changes
(
{
"fields": [
"artist",
"artists",
"albumartist",
"format",
"title",
"year",
"label",
"format",
"INCORRECT_FIELD",
],
"preserve": ["CD"],
},
Item(
artist="Ophidian",
artists=["Ophidian"],
albumartist="Ophidian",
format="CD",
year=2003,
album="Blackbox",
title="Khameleon",
label="Enzyme Records",
),
Item(
artist="Ophidian",
artists=["Ophidian"],
albumartist="Ophidian",
format="CD",
year=2003,
album="Blackbox",
title="Khameleon",
label="Enzyme Records",
),
),
# Test with the_artist disabled
(
{
"the_artist": False,
"fields": [
"artist",
"artists_sort",
],
},
Item(
artists_sort=["b-52s, the"],
artist="a day in the park",
),
Item(
artists_sort=["B-52s, The"],
artist="A Day in the Park",
),
),
# Test to make sure preserve and the_artist
# dont target the middle of sentences
# show that The artist applies to any field
# with artist mentioned
(
{
"preserve": ["PANTHER"],
"fields": ["artist", "artists", "artists_ids"],
},
Item(
artist="pinkpantheress",
artists=["pinkpantheress", "artist_two"],
artists_ids=["the the", "the the"],
),
Item(
artist="Pinkpantheress",
artists=["Pinkpantheress", "Artist_two"],
artists_ids=["The The", "The The"],
),
),
]
for testcase in testcases:
cfg, given, expected = testcase
with self.configure_plugin(cfg):
TitlecasePlugin().titlecase_fields(given)
assert given.artist == expected.artist
assert given.artists == expected.artists
assert given.artists_sort == expected.artists_sort
assert given.albumartist == expected.albumartist
assert given.artists_ids == expected.artists_ids
assert given.format == expected.format
assert given.year == expected.year
assert given.title == expected.title
assert given.label == expected.label
class TitlecasePluginMethodTests(PluginMixin): def test_cli_write(self):
plugin = "titlecase" given = Item(
preload_plugin = False album="retrodelica 2: back 2 the future",
artist="blue planet corporation",
def test_small_first_last(self): title="generator",
with self.configure_plugin({"small_first_last": False}): )
assert ( expected = Item(
TitlecasePlugin().titlecase("A Simple Trial") album="Retrodelica 2: Back 2 the Future",
== "a Simple Trial" artist="Blue Planet Corporation",
) title="Generator",
with self.configure_plugin({"small_first_last": True}): )
assert ( cfg = {"fields": ["album", "artist", "title"]}
TitlecasePlugin().titlecase("A simple Trial") with self.configure_plugin(cfg):
== "A Simple Trial"
)
def test_field_list(self):
fields = ["album", "albumartist"]
with self.configure_plugin({"fields": fields}):
t = TitlecasePlugin()
for field in fields:
assert field in t.fields_to_process
@pytest.mark.parametrize("given, expected", info_test_cases)
def test_received_info_handler(self, given, expected):
with self.configure_plugin(
{"fields": ["album", "artist_credit", "artists"]}
):
TitlecasePlugin().received_info_handler(given)
assert given == expected
@pytest.mark.parametrize("config, given, expected", item_test_cases)
class TitlecasePluginTest(PluginMixin):
plugin = "titlecase"
preload_plugin = False
def test_titlecase_fields(self, config, given, expected):
with self.configure_plugin(config):
TitlecasePlugin.titlecase_fields(given)
assert given == expected
def test_cli(self, config, given, expected):
with self.configure_plugin(config):
given.add(self.lib) given.add(self.lib)
self.run_command("titlecase") self.run_command("titlecase")
output = self.run_with_output("ls") output = self.run_with_output("ls")
@ -315,3 +342,25 @@ class TitlecasePluginTest(PluginMixin):
== f"{expected.artist} - {expected.album} - {expected.title}\n" == f"{expected.artist} - {expected.album} - {expected.title}\n"
) )
self.run_command("remove", expected.artist, "-f") self.run_command("remove", expected.artist, "-f")
def test_cli_no_write(self):
given = Item(
album="retrodelica 2: back 2 the future",
artist="blue planet corporation",
title="generator",
)
expected = Item(
album="retrodelica 2: back 2 the future",
artist="blue planet corporation",
title="generator",
)
cfg = {"fields": ["album", "artist", "title"]}
with self.configure_plugin(cfg):
given.add(self.lib)
self.run_command("-p", "titlecase")
output = self.run_with_output("ls")
assert (
output
== f"{expected.artist} - {expected.album} - {expected.title}\n"
)
self.run_command("remove", expected.artist, "-f")