Skip to content

Commit 09476bd

Browse files
Titlecase Plugin Improvements (#6220)
- Add preserving strings that are all lowercase or all upper case - Fix spelling of 'separator' in config, docs and code - Move most of the logging for the plugin to debug to keep log cleaner. Improvements I found a need for in my daily use with the plugin. - [x] Documentation. (If you've added a new command-line flag, for example, find the appropriate page under `docs/` to describe it.) - [x] Changelog. (Skipping as the plugin has not been released yet) - [x] Tests. (Very much encouraged but not strictly required.)
2 parents ffede9d + e039df4 commit 09476bd

File tree

3 files changed

+75
-17
lines changed

3 files changed

+75
-17
lines changed

beetsplug/titlecase.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ def __init__(self) -> None:
4747
"preserve": [],
4848
"fields": [],
4949
"replace": [],
50-
"seperators": [],
50+
"separators": [],
5151
"force_lowercase": False,
5252
"small_first_last": True,
5353
"the_artist": True,
54+
"all_caps": False,
55+
"all_lowercase": False,
5456
"after_choice": False,
5557
}
5658
)
@@ -60,14 +62,16 @@ def __init__(self) -> None:
6062
preserve - Provide a list of strings with specific case requirements.
6163
fields - Fields to apply titlecase to.
6264
replace - List of pairs, first is the target, second is the replacement
63-
seperators - Other characters to treat like periods.
64-
force_lowercase - Lowercases the string before titlecasing.
65+
separators - Other characters to treat like periods.
66+
force_lowercase - Lowercase the string before titlecase.
6567
small_first_last - If small characters should be cased at the start of strings.
6668
the_artist - If the plugin infers the field to be an artist field
6769
(e.g. the field contains "artist")
6870
It will capitalize a lowercase The, helpful for the artist names
6971
that start with 'The', like 'The Who' or 'The Talking Heads' when
70-
they are not at the start of a string. Superceded by preserved phrases.
72+
they are not at the start of a string. Superseded by preserved phrases.
73+
all_caps - If the alphabet in the string is all uppercase, do not modify.
74+
all_lowercase - If the alphabet in the string is all lowercase, do not modify.
7175
"""
7276
# Register template function
7377
self.template_funcs["titlecase"] = self.titlecase
@@ -121,17 +125,25 @@ def preserve(self) -> PreservedText:
121125
return preserved
122126

123127
@cached_property
124-
def seperators(self) -> re.Pattern[str] | None:
125-
if seperators := "".join(
126-
dict.fromkeys(self.config["seperators"].as_str_seq())
128+
def separators(self) -> re.Pattern[str] | None:
129+
if separators := "".join(
130+
dict.fromkeys(self.config["separators"].as_str_seq())
127131
):
128-
return re.compile(rf"(.*?[{re.escape(seperators)}]+)(\s*)(?=.)")
132+
return re.compile(rf"(.*?[{re.escape(separators)}]+)(\s*)(?=.)")
129133
return None
130134

131135
@cached_property
132136
def small_first_last(self) -> bool:
133137
return self.config["small_first_last"].get(bool)
134138

139+
@cached_property
140+
def all_caps(self) -> bool:
141+
return self.config["all_caps"].get(bool)
142+
143+
@cached_property
144+
def all_lowercase(self) -> bool:
145+
return self.config["all_lowercase"].get(bool)
146+
135147
@cached_property
136148
def the_artist_regexp(self) -> re.Pattern[str]:
137149
return re.compile(r"\bthe\b")
@@ -180,15 +192,15 @@ def titlecase_fields(self, item: Item | Info) -> None:
180192
]
181193
if cased_list != init_field:
182194
setattr(item, field, cased_list)
183-
self._log.info(
195+
self._log.debug(
184196
f"{field}: {', '.join(init_field)} ->",
185197
f"{', '.join(cased_list)}",
186198
)
187199
elif isinstance(init_field, str):
188200
cased: str = self.titlecase(init_field, field)
189201
if cased != init_field:
190202
setattr(item, field, cased)
191-
self._log.info(f"{field}: {init_field} -> {cased}")
203+
self._log.debug(f"{field}: {init_field} -> {cased}")
192204
else:
193205
self._log.debug(f"{field}: no string present")
194206
else:
@@ -197,15 +209,20 @@ def titlecase_fields(self, item: Item | Info) -> None:
197209
def titlecase(self, text: str, field: str = "") -> str:
198210
"""Titlecase the given text."""
199211
# Check we should split this into two substrings.
200-
if self.seperators:
201-
if len(splits := self.seperators.findall(text)):
212+
if self.separators:
213+
if len(splits := self.separators.findall(text)):
202214
split_cased = "".join(
203215
[self.titlecase(s[0], field) + s[1] for s in splits]
204216
)
205217
# Add on the remaining portion
206218
return split_cased + self.titlecase(
207219
text[len(split_cased) :], field
208220
)
221+
# Check if A-Z is all uppercase or all lowercase
222+
if self.all_lowercase and text.islower():
223+
return text
224+
elif self.all_caps and text.isupper():
225+
return text
209226
# Any necessary replacements go first, mainly punctuation.
210227
titlecased = text.lower() if self.force_lowercase else text
211228
for pair in self.replace:

docs/plugins/titlecase.rst

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ Default
5757
fields: []
5858
preserve: []
5959
replace: []
60-
seperators: []
60+
separators: []
6161
force_lowercase: no
6262
small_first_last: yes
6363
the_artist: yes
64+
all_lowercase: no
65+
all_caps: no
6466
after_choice: no
6567
6668
.. conf:: auto
@@ -120,7 +122,7 @@ Default
120122
- "": '"'
121123
- "": '"'
122124
123-
.. conf:: seperators
125+
.. conf:: separators
124126
:default: []
125127

126128
A list of characters to treat as markers of new sentences. Helpful for split titles
@@ -146,6 +148,19 @@ Default
146148
capitalized. Useful for bands with `The` as part of the proper name,
147149
like ``Amyl and The Sniffers``.
148150

151+
.. conf:: all_caps
152+
:default: no
153+
154+
If the letters a-Z in a string are all caps, do not modify the string. Useful
155+
if you encounter a lot of acronyms.
156+
157+
.. conf:: all_lowercase
158+
:default: no
159+
160+
If the letters a-Z in a string are all lowercase, do not modify the string.
161+
Useful if you encounter a lot of stylized lowercase spellings, but otherwise
162+
want titlecase applied.
163+
149164
.. conf:: after_choice
150165
:default: no
151166

test/plugins/test_titlecase.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def test_preserve(self):
112112
assert TitlecasePlugin().titlecase(word.upper()) == word
113113
assert TitlecasePlugin().titlecase(word.lower()) == word
114114

115-
def test_seperators(self):
115+
def test_separators(self):
116116
testcases = [
117117
([], "it / a / in / of / to / the", "It / a / in / of / to / The"),
118118
(["/"], "it / the test", "It / The Test"),
@@ -129,8 +129,34 @@ def test_seperators(self):
129129
),
130130
]
131131
for testcase in testcases:
132-
seperators, given, expected = testcase
133-
with self.configure_plugin({"seperators": seperators}):
132+
separators, given, expected = testcase
133+
with self.configure_plugin({"separators": separators}):
134+
assert TitlecasePlugin().titlecase(given) == expected
135+
136+
def test_all_caps(self):
137+
testcases = [
138+
(True, "Unaffected", "Unaffected"),
139+
(True, "RBMK1000", "RBMK1000"),
140+
(False, "RBMK1000", "Rbmk1000"),
141+
(True, "P A R I S!", "P A R I S!"),
142+
(True, "pillow dub...", "Pillow Dub..."),
143+
(False, "P A R I S!", "P a R I S!"),
144+
]
145+
for testcase in testcases:
146+
all_caps, given, expected = testcase
147+
with self.configure_plugin({"all_caps": all_caps}):
148+
assert TitlecasePlugin().titlecase(given) == expected
149+
150+
def test_all_lowercase(self):
151+
testcases = [
152+
(True, "Unaffected", "Unaffected"),
153+
(True, "RBMK1000", "Rbmk1000"),
154+
(True, "pillow dub...", "pillow dub..."),
155+
(False, "pillow dub...", "Pillow Dub..."),
156+
]
157+
for testcase in testcases:
158+
all_lowercase, given, expected = testcase
159+
with self.configure_plugin({"all_lowercase": all_lowercase}):
134160
assert TitlecasePlugin().titlecase(given) == expected
135161

136162
def test_received_info_handler(self):

0 commit comments

Comments
 (0)