-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace most str.format()
uses with f-strings
#5337
base: master
Are you sure you want to change the base?
Conversation
Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry. |
return f"{m.group(2)} {m.group(1)}" | ||
|
||
str1 = re.sub(SD_END_REPLACE, replacer, str1) | ||
str2 = re.sub(SD_END_REPLACE, replacer, str2) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neither the old option nor your version is incredibly readable. However I think I prefer the old version, because "endswith" is more readable. Could you maybe change var m
to something more descriptive? And maybe change replacer
to something that sounds more like a function? Like do_replace?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair. I think a more intuitive implementation would be based on str.rsplit()
or str.rpartition()
using ,
as a delimiter, and checking that the final component is one of the three words. If that doesn't sound good, I can improve what I have here: m
-> match_info
and replacer
-> move_article_to_front
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree with @RollingStar actually, I think your regex is clearer. Maybe I'm just more familiar/confident with regex? One thing I would note is that it might be better, if we stay with this approach, to make the regex case-insensitive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've rewritten this as a much more explicit and intuitive function which uses str.rsplit
to separate a title from an article. @RollingStar, I hope this makes it more readable! Also, @Serene-Arc, the inputs are lower-cased here, so case sensitivity is not an issue.
setup_sql += "ALTER TABLE {} ADD COLUMN {} {};\n".format( | ||
table, name, typ.sql | ||
setup_sql += ( | ||
f"ALTER TABLE {table} ADD COLUMN {name} {typ.sql};\n" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flagging here that we always want to be attentive when changing SQL commands.
@@ -151,6 +151,7 @@ def __init__(self, field_name: str, pattern: P, fast: bool = True): | |||
self.fast = fast | |||
|
|||
def col_clause(self) -> Tuple[str, Sequence[SQLiteType]]: | |||
# TODO: Avoid having to insert raw text into SQL clauses. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good edit
Thank you for the monster commit. Hopefully you had automation to aid you. I only looked at the first few chunks (about as far as my final comment) but it passes the smell test. I would prefer having the SQL changes in their own commit. Anything with SQL should probably have more attention on it than an f-string. |
Thanks for reviewing! This is a pretty big change and I am worried I slipped up somewhere, so I'm happy to hang on to the PR until somebody can look at the full diff. PyCharm helped me find the usages of
That's understandable, messing with SQL can be dangerous. The tests pass, which I hope adds some degree of confidence to these changes. I haven't fundamentally changed any SQL statements, though -- they should operate exactly the same as before. I can refactor them out to a separate commit, but I think that should wait until somebody reviews the entire diff. |
I'll review the entire diff and get back to you. |
beets/dbcore/db.py
Outdated
""" | ||
if fields is None: | ||
fields = self._fields | ||
fields = self._fields.keys() | ||
fields = set(fields) - {"id"} | ||
db = self._check_db() | ||
|
||
# Build assignments for query. | ||
assignments = [] | ||
subvars = [] | ||
for key in fields: | ||
if key != "id" and key in self._dirty: | ||
self._dirty.remove(key) | ||
assignments.append(key + "=?") | ||
value = self._type(key).to_sql(self[key]) | ||
subvars.append(value) | ||
dirty_fields = list(fields & self._dirty) | ||
self._dirty -= fields | ||
assignments = ",".join(f"{k}=?" for k in dirty_fields) | ||
subvars = [self._type(k).to_sql(self[k]) for k in dirty_fields] | ||
|
||
with db.transaction() as tx: | ||
# Main table update. | ||
if assignments: | ||
query = "UPDATE {} SET {} WHERE id=?".format( | ||
self._table, ",".join(assignments) | ||
) | ||
query = f"UPDATE {self._table} SET {assignments} WHERE id=?" | ||
subvars.append(self.id) | ||
tx.mutate(query, subvars) | ||
|
||
# Modified/added flexible attributes. | ||
for key, value in self._values_flex.items(): | ||
if key in self._dirty: | ||
self._dirty.remove(key) | ||
tx.mutate( | ||
"INSERT INTO {} " | ||
"(entity_id, key, value) " | ||
"VALUES (?, ?, ?);".format(self._flex_table), | ||
(self.id, key, value), | ||
) | ||
flex_fields = set(self._values_flex.keys()) | ||
dirty_flex_fields = list(flex_fields & self._dirty) | ||
self._dirty -= flex_fields | ||
for key in dirty_flex_fields: | ||
tx.mutate( | ||
f"INSERT INTO {self._flex_table} " | ||
"(entity_id, key, value) " | ||
"VALUES (?, ?, ?);", | ||
(self.id, key, self._values_flex[key]), | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does result in the same values, but not the same ordering because of the conversion to sets. If that will cause problems should be double and triple-checks since this is ultimately being made into SQL commands.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I forgot about the order changing! I don't think it'll cause issues, but I can actually think of a nicer layout for this implementation in terms of not in
that does preserve the order. I'll implement it in its own commit, we'll see what's preferable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I've rewritten the code now. I'm confident the SQL assignments do not care about the order fields are set in, so I've stuck with sets. I traced where the fields
parameter can come from -- it looks like they are user-set (beet update -f FIELDS
), in which case we really shouldn't be relying on the order anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonderful, I'll leave this issue open until I double check myself. Should always have multiple eyes on stuff like this.
return f"{m.group(2)} {m.group(1)}" | ||
|
||
str1 = re.sub(SD_END_REPLACE, replacer, str1) | ||
str2 = re.sub(SD_END_REPLACE, replacer, str2) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree with @RollingStar actually, I think your regex is clearer. Maybe I'm just more familiar/confident with regex? One thing I would note is that it might be better, if we stay with this approach, to make the regex case-insensitive.
The logic is a bit easier to follow now. See: <beetbox#5337 (comment)>
joined = urljoin( | ||
"{hostname}:{port}".format(hostname=hostname, port=port), endpoint | ||
) | ||
joined = urljoin(f"{hostname}:{port}", endpoint) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this is a URL we're formatting too, I think using str.format
would be completely unnecessary here. The actual variable names are perfect and this is just much shorter.
The new version doesn't rely on regular expressions, provides more intuitive names, and will probably be easier to maintain. See: <beetbox#5337 (comment)>
In cases where the values being filled in did not intuitively describe what they represented as URL components, it became difficult to figure out the structure of the URL. See: <beetbox#5337 (comment)>
@Serene-Arc, I think the primary remaining blocker is a thorough review of the SQL function change. I've fixed the URL formatting and the "title, article" implementation as well. Don't worry if you're on vacation, but I hope you're able to get to it soon. |
@bal-e I'll try and do it over the next day or two! The PR is looking pretty good though. |
See: <beetbox#5337 (comment)> diff --git c/beets/autotag/mb.py i/beets/autotag/mb.py index e16643d7..f152b567 100644 --- c/beets/autotag/mb.py +++ i/beets/autotag/mb.py @@ -66,7 +66,9 @@ class MusicBrainzAPIError(util.HumanReadableException): super().__init__(reason, verb, tb) def get_message(self): - return f"{self._reasonstr()} in {self.verb} with query {self.query!r}" + return ( + f"{self._reasonstr()} in {self.verb} with query {repr(self.query)}" + ) log = logging.getLogger("beets") diff --git c/beets/dbcore/db.py i/beets/dbcore/db.py index 55ba6f11..5aa75aa5 100755 --- c/beets/dbcore/db.py +++ i/beets/dbcore/db.py @@ -397,7 +397,7 @@ class Model(ABC): def __repr__(self) -> str: name = type(self).__name__ - fields = ", ".join(f"{k}={v!r}" for k, v in dict(self).items()) + fields = ", ".join(f"{k}={repr(v)}" for k, v in dict(self).items()) return f"{name}({fields})" def clear_dirty(self): @@ -558,12 +558,12 @@ class Model(ABC): def __getattr__(self, key): if key.startswith("_"): - raise AttributeError(f"model has no attribute {key!r}") + raise AttributeError(f"model has no attribute {repr(key)}") else: try: return self[key] except KeyError: - raise AttributeError(f"no such field {key!r}") + raise AttributeError(f"no such field {repr(key)}") def __setattr__(self, key, value): if key.startswith("_"): diff --git c/beets/dbcore/query.py i/beets/dbcore/query.py index 357b5685..6e94ddd5 100644 --- c/beets/dbcore/query.py +++ i/beets/dbcore/query.py @@ -171,7 +171,7 @@ class FieldQuery(Query, Generic[P]): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.field_name!r}, {self.pattern!r}, " + f"{self.__class__.__name__}({repr(self.field_name)}, {repr(self.pattern)}, " f"fast={self.fast})" ) @@ -210,7 +210,9 @@ class NoneQuery(FieldQuery[None]): return obj.get(self.field_name) is None def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.field_name!r}, {self.fast})" + return ( + f"{self.__class__.__name__}({repr(self.field_name)}, {self.fast})" + ) class StringFieldQuery(FieldQuery[P]): @@ -503,7 +505,7 @@ class CollectionQuery(Query): return clause, subvals def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.subqueries!r})" + return f"{self.__class__.__name__}({repr(self.subqueries)})" def __eq__(self, other) -> bool: return super().__eq__(other) and self.subqueries == other.subqueries @@ -548,7 +550,7 @@ class AnyFieldQuery(CollectionQuery): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.pattern!r}, {self.fields!r}, " + f"{self.__class__.__name__}({repr(self.pattern)}, {repr(self.fields)}, " f"{self.query_class.__name__})" ) @@ -619,7 +621,7 @@ class NotQuery(Query): return not self.subquery.match(obj) def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.subquery!r})" + return f"{self.__class__.__name__}({repr(self.subquery)})" def __eq__(self, other) -> bool: return super().__eq__(other) and self.subquery == other.subquery @@ -975,7 +977,7 @@ class MultipleSort(Sort): return items def __repr__(self): - return f"{self.__class__.__name__}({self.sorts!r})" + return f"{self.__class__.__name__}({repr(self.sorts)})" def __hash__(self): return hash(tuple(self.sorts)) @@ -1015,7 +1017,7 @@ class FieldSort(Sort): def __repr__(self) -> str: return ( f"{self.__class__.__name__}" - f"({self.field!r}, ascending={self.ascending!r})" + f"({repr(self.field)}, ascending={repr(self.ascending)})" ) def __hash__(self) -> int: diff --git c/beets/library.py i/beets/library.py index 77d24ecd..a9adc13d 100644 --- c/beets/library.py +++ i/beets/library.py @@ -156,7 +156,7 @@ class PathQuery(dbcore.FieldQuery[bytes]): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.field!r}, {self.pattern!r}, " + f"{self.__class__.__name__}({repr(self.field)}, {repr(self.pattern)}, " f"fast={self.fast}, case_sensitive={self.case_sensitive})" ) @@ -735,7 +735,7 @@ class Item(LibModel): # can even deadlock due to the database lock. name = type(self).__name__ keys = self.keys(with_album=False) - fields = (f"{k}={self[k]!r}" for k in keys) + fields = (f"{k}={repr(self[k])}" for k in keys) return f"{name}({', '.join(fields)})" def keys(self, computed=False, with_album=True): @@ -1578,7 +1578,7 @@ def parse_query_string(s, model_cls): The string is split into components using shell-like syntax. """ - message = f"Query is not unicode: {s!r}" + message = f"Query is not unicode: {repr(s)}" assert isinstance(s, str), message try: parts = shlex.split(s) diff --git c/beets/test/_common.py i/beets/test/_common.py index c12838e2..0bc1baf8 100644 --- c/beets/test/_common.py +++ i/beets/test/_common.py @@ -152,7 +152,7 @@ class Assertions: """A mixin with additional unit test assertions.""" def assertExists(self, path): # noqa - assert os.path.exists(syspath(path)), f"file does not exist: {path!r}" + assert os.path.exists(syspath(path)), f"file does not exist: {repr(path)}" def assertNotExists(self, path): # noqa assert not os.path.exists(syspath(path)), f"file exists: {repr(path)}" @@ -186,7 +186,7 @@ class InputException(Exception): def __str__(self): msg = "Attempt to read with no input provided." if self.output is not None: - msg += f" Output: {self.output!r}" + msg += f" Output: {repr(self.output)}" return msg diff --git c/beets/ui/commands.py i/beets/ui/commands.py index 3042ca77..a717c94c 100755 --- c/beets/ui/commands.py +++ i/beets/ui/commands.py @@ -213,7 +213,7 @@ def get_singleton_disambig_fields(info: hooks.TrackInfo) -> Sequence[str]: out = [] chosen_fields = config["match"]["singleton_disambig_fields"].as_str_seq() calculated_values = { - "index": f"Index {info.index!s}", + "index": f"Index {str(info.index)}", "track_alt": f"Track {info.track_alt}", "album": ( f"[{info.album}]" diff --git c/beets/util/__init__.py i/beets/util/__init__.py index aa94b6d2..a0f13fa1 100644 --- c/beets/util/__init__.py +++ i/beets/util/__init__.py @@ -104,7 +104,7 @@ class HumanReadableException(Exception): elif hasattr(self.reason, "strerror"): # i.e., EnvironmentError return self.reason.strerror else: - return f'"{self.reason!s}"' + return f'"{str(self.reason)}"' def get_message(self): """Create the human-readable description of the error, sans diff --git c/beets/util/functemplate.py i/beets/util/functemplate.py index 35f60b7d..f149d370 100644 --- c/beets/util/functemplate.py +++ i/beets/util/functemplate.py @@ -166,7 +166,7 @@ class Call: self.original = original def __repr__(self): - return f"Call({self.ident!r}, {self.args!r}, {self.original!r})" + return f"Call({repr(self.ident)}, {repr(self.args)}, {repr(self.original)})" def evaluate(self, env): """Evaluate the function call in the environment, returning a diff --git c/beetsplug/bpd/__init__.py i/beetsplug/bpd/__init__.py index d6c75380..3336702c 100644 --- c/beetsplug/bpd/__init__.py +++ i/beetsplug/bpd/__init__.py @@ -1142,7 +1142,7 @@ class Server(BaseServer): pass for tagtype, field in self.tagtype_map.items(): - info_lines.append(f"{tagtype}: {getattr(item, field)!s}") + info_lines.append(f"{tagtype}: {str(getattr(item, field))}") return info_lines @@ -1301,7 +1301,7 @@ class Server(BaseServer): yield ( f"bitrate: {item.bitrate / 1000}", - f"audio: {item.samplerate!s}:{item.bitdepth!s}:{item.channels!s}", + f"audio: {str(item.samplerate)}:{str(item.bitdepth)}:{str(item.channels)}", ) (pos, total) = self.player.time() diff --git c/beetsplug/edit.py i/beetsplug/edit.py index 61f2020a..20430255 100644 --- c/beetsplug/edit.py +++ i/beetsplug/edit.py @@ -47,7 +47,9 @@ def edit(filename, log): try: subprocess.call(cmd) except OSError as exc: - raise ui.UserError(f"could not run editor command {cmd[0]!r}: {exc}") + raise ui.UserError( + f"could not run editor command {repr(cmd[0])}: {exc}" + ) def dump(arg): diff --git c/beetsplug/replaygain.py i/beetsplug/replaygain.py index 78cce1e6..1c8aaaa9 100644 --- c/beetsplug/replaygain.py +++ i/beetsplug/replaygain.py @@ -534,7 +534,7 @@ class FfmpegBackend(Backend): if output[i].startswith(search): return i raise ReplayGainError( - f"ffmpeg output: missing {search!r} after line {start_line}" + f"ffmpeg output: missing {repr(search)} after line {start_line}" ) def _parse_float(self, line: bytes) -> float: @@ -547,7 +547,7 @@ class FfmpegBackend(Backend): parts = line.split(b":", 1) if len(parts) < 2: raise ReplayGainError( - f"ffmpeg output: expected key value pair, found {line!r}" + f"ffmpeg output: expected key value pair, found {repr(line)}" ) value = parts[1].lstrip() # strip unit @@ -557,7 +557,7 @@ class FfmpegBackend(Backend): return float(value) except ValueError: raise ReplayGainError( - f"ffmpeg output: expected float value, found {value!r}" + f"ffmpeg output: expected float value, found {repr(value)}" ) @@ -886,7 +886,7 @@ class GStreamerBackend(Backend): f = self._src.get_property("location") # A GStreamer error, either an unsupported format or a bug. self._error = ReplayGainError( - f"Error {err!r} - {debug!r} on file {f!r}" + f"Error {repr(err)} - {repr(debug)} on file {repr(f)}" ) def _on_tag(self, bus, message): diff --git c/beetsplug/thumbnails.py i/beetsplug/thumbnails.py index acca413d..0cde56c7 100644 --- c/beetsplug/thumbnails.py +++ i/beetsplug/thumbnails.py @@ -292,4 +292,6 @@ class GioURI(URIGetter): try: return uri.decode(util._fsencoding()) except UnicodeDecodeError: - raise RuntimeError(f"Could not decode filename from GIO: {uri!r}") + raise RuntimeError( + f"Could not decode filename from GIO: {repr(uri)}" + ) diff --git c/test/plugins/test_lyrics.py i/test/plugins/test_lyrics.py index 7cb081fc..484d4889 100644 --- c/test/plugins/test_lyrics.py +++ i/test/plugins/test_lyrics.py @@ -223,9 +223,9 @@ class LyricsAssertions: if not keywords <= words: details = ( - f"{keywords!r} is not a subset of {words!r}." - f" Words only in expected set {keywords - words!r}," - f" Words only in result set {words - keywords!r}." + f"{repr(keywords)} is not a subset of {repr(words)}." + f" Words only in expected set {repr(keywords - words)}," + f" Words only in result set {repr(words - keywords)}." ) self.fail(f"{details} : {msg}") diff --git c/test/plugins/test_player.py i/test/plugins/test_player.py index bf466e1b..e23b6396 100644 --- c/test/plugins/test_player.py +++ i/test/plugins/test_player.py @@ -132,7 +132,7 @@ class MPCResponse: cmd, rest = rest[2:].split("}") return False, (int(code), int(pos), cmd, rest[1:]) else: - raise RuntimeError(f"Unexpected status: {status!r}") + raise RuntimeError(f"Unexpected status: {repr(status)}") def _parse_body(self, body): """Messages are generally in the format "header: content". @@ -145,7 +145,7 @@ class MPCResponse: if not line: continue if ":" not in line: - raise RuntimeError(f"Unexpected line: {line!r}") + raise RuntimeError(f"Unexpected line: {repr(line)}") header, content = line.split(":", 1) content = content.lstrip() if header in repeated_headers: @@ -191,7 +191,7 @@ class MPCClient: responses.append(MPCResponse(response)) response = b"" elif not line: - raise RuntimeError(f"Unexpected response: {line!r}") + raise RuntimeError(f"Unexpected response: {repr(line)}") def serialise_command(self, command, *args): cmd = [command.encode("utf-8")] diff --git c/test/plugins/test_thumbnails.py i/test/plugins/test_thumbnails.py index 07775995..1931061b 100644 --- c/test/plugins/test_thumbnails.py +++ i/test/plugins/test_thumbnails.py @@ -71,7 +71,7 @@ class ThumbnailsTest(BeetsTestCase): return False if path == syspath(LARGE_DIR): return True - raise ValueError(f"unexpected path {path!r}") + raise ValueError(f"unexpected path {repr(path)}") mock_os.path.exists = exists plugin = ThumbnailsPlugin()
The logic is a bit easier to follow now. See: <beetbox#5337 (comment)>
The new version doesn't rely on regular expressions, provides more intuitive names, and will probably be easier to maintain. See: <beetbox#5337 (comment)>
In cases where the values being filled in did not intuitively describe what they represented as URL components, it became difficult to figure out the structure of the URL. See: <beetbox#5337 (comment)>
See: <beetbox#5337 (comment)> diff --git c/beets/autotag/mb.py i/beets/autotag/mb.py index e16643d7..f152b567 100644 --- c/beets/autotag/mb.py +++ i/beets/autotag/mb.py @@ -66,7 +66,9 @@ class MusicBrainzAPIError(util.HumanReadableException): super().__init__(reason, verb, tb) def get_message(self): - return f"{self._reasonstr()} in {self.verb} with query {self.query!r}" + return ( + f"{self._reasonstr()} in {self.verb} with query {repr(self.query)}" + ) log = logging.getLogger("beets") diff --git c/beets/dbcore/db.py i/beets/dbcore/db.py index 55ba6f11..5aa75aa5 100755 --- c/beets/dbcore/db.py +++ i/beets/dbcore/db.py @@ -397,7 +397,7 @@ class Model(ABC): def __repr__(self) -> str: name = type(self).__name__ - fields = ", ".join(f"{k}={v!r}" for k, v in dict(self).items()) + fields = ", ".join(f"{k}={repr(v)}" for k, v in dict(self).items()) return f"{name}({fields})" def clear_dirty(self): @@ -558,12 +558,12 @@ class Model(ABC): def __getattr__(self, key): if key.startswith("_"): - raise AttributeError(f"model has no attribute {key!r}") + raise AttributeError(f"model has no attribute {repr(key)}") else: try: return self[key] except KeyError: - raise AttributeError(f"no such field {key!r}") + raise AttributeError(f"no such field {repr(key)}") def __setattr__(self, key, value): if key.startswith("_"): diff --git c/beets/dbcore/query.py i/beets/dbcore/query.py index 357b5685..6e94ddd5 100644 --- c/beets/dbcore/query.py +++ i/beets/dbcore/query.py @@ -171,7 +171,7 @@ class FieldQuery(Query, Generic[P]): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.field_name!r}, {self.pattern!r}, " + f"{self.__class__.__name__}({repr(self.field_name)}, {repr(self.pattern)}, " f"fast={self.fast})" ) @@ -210,7 +210,9 @@ class NoneQuery(FieldQuery[None]): return obj.get(self.field_name) is None def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.field_name!r}, {self.fast})" + return ( + f"{self.__class__.__name__}({repr(self.field_name)}, {self.fast})" + ) class StringFieldQuery(FieldQuery[P]): @@ -503,7 +505,7 @@ class CollectionQuery(Query): return clause, subvals def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.subqueries!r})" + return f"{self.__class__.__name__}({repr(self.subqueries)})" def __eq__(self, other) -> bool: return super().__eq__(other) and self.subqueries == other.subqueries @@ -548,7 +550,7 @@ class AnyFieldQuery(CollectionQuery): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.pattern!r}, {self.fields!r}, " + f"{self.__class__.__name__}({repr(self.pattern)}, {repr(self.fields)}, " f"{self.query_class.__name__})" ) @@ -619,7 +621,7 @@ class NotQuery(Query): return not self.subquery.match(obj) def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.subquery!r})" + return f"{self.__class__.__name__}({repr(self.subquery)})" def __eq__(self, other) -> bool: return super().__eq__(other) and self.subquery == other.subquery @@ -975,7 +977,7 @@ class MultipleSort(Sort): return items def __repr__(self): - return f"{self.__class__.__name__}({self.sorts!r})" + return f"{self.__class__.__name__}({repr(self.sorts)})" def __hash__(self): return hash(tuple(self.sorts)) @@ -1015,7 +1017,7 @@ class FieldSort(Sort): def __repr__(self) -> str: return ( f"{self.__class__.__name__}" - f"({self.field!r}, ascending={self.ascending!r})" + f"({repr(self.field)}, ascending={repr(self.ascending)})" ) def __hash__(self) -> int: diff --git c/beets/library.py i/beets/library.py index 77d24ecd..a9adc13d 100644 --- c/beets/library.py +++ i/beets/library.py @@ -156,7 +156,7 @@ class PathQuery(dbcore.FieldQuery[bytes]): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.field!r}, {self.pattern!r}, " + f"{self.__class__.__name__}({repr(self.field)}, {repr(self.pattern)}, " f"fast={self.fast}, case_sensitive={self.case_sensitive})" ) @@ -735,7 +735,7 @@ class Item(LibModel): # can even deadlock due to the database lock. name = type(self).__name__ keys = self.keys(with_album=False) - fields = (f"{k}={self[k]!r}" for k in keys) + fields = (f"{k}={repr(self[k])}" for k in keys) return f"{name}({', '.join(fields)})" def keys(self, computed=False, with_album=True): @@ -1578,7 +1578,7 @@ def parse_query_string(s, model_cls): The string is split into components using shell-like syntax. """ - message = f"Query is not unicode: {s!r}" + message = f"Query is not unicode: {repr(s)}" assert isinstance(s, str), message try: parts = shlex.split(s) diff --git c/beets/test/_common.py i/beets/test/_common.py index c12838e2..0bc1baf8 100644 --- c/beets/test/_common.py +++ i/beets/test/_common.py @@ -152,7 +152,7 @@ class Assertions: """A mixin with additional unit test assertions.""" def assertExists(self, path): # noqa - assert os.path.exists(syspath(path)), f"file does not exist: {path!r}" + assert os.path.exists(syspath(path)), f"file does not exist: {repr(path)}" def assertNotExists(self, path): # noqa assert not os.path.exists(syspath(path)), f"file exists: {repr(path)}" @@ -186,7 +186,7 @@ class InputException(Exception): def __str__(self): msg = "Attempt to read with no input provided." if self.output is not None: - msg += f" Output: {self.output!r}" + msg += f" Output: {repr(self.output)}" return msg diff --git c/beets/ui/commands.py i/beets/ui/commands.py index 3042ca77..a717c94c 100755 --- c/beets/ui/commands.py +++ i/beets/ui/commands.py @@ -213,7 +213,7 @@ def get_singleton_disambig_fields(info: hooks.TrackInfo) -> Sequence[str]: out = [] chosen_fields = config["match"]["singleton_disambig_fields"].as_str_seq() calculated_values = { - "index": f"Index {info.index!s}", + "index": f"Index {str(info.index)}", "track_alt": f"Track {info.track_alt}", "album": ( f"[{info.album}]" diff --git c/beets/util/__init__.py i/beets/util/__init__.py index aa94b6d2..a0f13fa1 100644 --- c/beets/util/__init__.py +++ i/beets/util/__init__.py @@ -104,7 +104,7 @@ class HumanReadableException(Exception): elif hasattr(self.reason, "strerror"): # i.e., EnvironmentError return self.reason.strerror else: - return f'"{self.reason!s}"' + return f'"{str(self.reason)}"' def get_message(self): """Create the human-readable description of the error, sans diff --git c/beets/util/functemplate.py i/beets/util/functemplate.py index 35f60b7d..f149d370 100644 --- c/beets/util/functemplate.py +++ i/beets/util/functemplate.py @@ -166,7 +166,7 @@ class Call: self.original = original def __repr__(self): - return f"Call({self.ident!r}, {self.args!r}, {self.original!r})" + return f"Call({repr(self.ident)}, {repr(self.args)}, {repr(self.original)})" def evaluate(self, env): """Evaluate the function call in the environment, returning a diff --git c/beetsplug/bpd/__init__.py i/beetsplug/bpd/__init__.py index d6c75380..3336702c 100644 --- c/beetsplug/bpd/__init__.py +++ i/beetsplug/bpd/__init__.py @@ -1142,7 +1142,7 @@ class Server(BaseServer): pass for tagtype, field in self.tagtype_map.items(): - info_lines.append(f"{tagtype}: {getattr(item, field)!s}") + info_lines.append(f"{tagtype}: {str(getattr(item, field))}") return info_lines @@ -1301,7 +1301,7 @@ class Server(BaseServer): yield ( f"bitrate: {item.bitrate / 1000}", - f"audio: {item.samplerate!s}:{item.bitdepth!s}:{item.channels!s}", + f"audio: {str(item.samplerate)}:{str(item.bitdepth)}:{str(item.channels)}", ) (pos, total) = self.player.time() diff --git c/beetsplug/edit.py i/beetsplug/edit.py index 61f2020a..20430255 100644 --- c/beetsplug/edit.py +++ i/beetsplug/edit.py @@ -47,7 +47,9 @@ def edit(filename, log): try: subprocess.call(cmd) except OSError as exc: - raise ui.UserError(f"could not run editor command {cmd[0]!r}: {exc}") + raise ui.UserError( + f"could not run editor command {repr(cmd[0])}: {exc}" + ) def dump(arg): diff --git c/beetsplug/replaygain.py i/beetsplug/replaygain.py index 78cce1e6..1c8aaaa9 100644 --- c/beetsplug/replaygain.py +++ i/beetsplug/replaygain.py @@ -534,7 +534,7 @@ class FfmpegBackend(Backend): if output[i].startswith(search): return i raise ReplayGainError( - f"ffmpeg output: missing {search!r} after line {start_line}" + f"ffmpeg output: missing {repr(search)} after line {start_line}" ) def _parse_float(self, line: bytes) -> float: @@ -547,7 +547,7 @@ class FfmpegBackend(Backend): parts = line.split(b":", 1) if len(parts) < 2: raise ReplayGainError( - f"ffmpeg output: expected key value pair, found {line!r}" + f"ffmpeg output: expected key value pair, found {repr(line)}" ) value = parts[1].lstrip() # strip unit @@ -557,7 +557,7 @@ class FfmpegBackend(Backend): return float(value) except ValueError: raise ReplayGainError( - f"ffmpeg output: expected float value, found {value!r}" + f"ffmpeg output: expected float value, found {repr(value)}" ) @@ -886,7 +886,7 @@ class GStreamerBackend(Backend): f = self._src.get_property("location") # A GStreamer error, either an unsupported format or a bug. self._error = ReplayGainError( - f"Error {err!r} - {debug!r} on file {f!r}" + f"Error {repr(err)} - {repr(debug)} on file {repr(f)}" ) def _on_tag(self, bus, message): diff --git c/beetsplug/thumbnails.py i/beetsplug/thumbnails.py index acca413d..0cde56c7 100644 --- c/beetsplug/thumbnails.py +++ i/beetsplug/thumbnails.py @@ -292,4 +292,6 @@ class GioURI(URIGetter): try: return uri.decode(util._fsencoding()) except UnicodeDecodeError: - raise RuntimeError(f"Could not decode filename from GIO: {uri!r}") + raise RuntimeError( + f"Could not decode filename from GIO: {repr(uri)}" + ) diff --git c/test/plugins/test_lyrics.py i/test/plugins/test_lyrics.py index 7cb081fc..484d4889 100644 --- c/test/plugins/test_lyrics.py +++ i/test/plugins/test_lyrics.py @@ -223,9 +223,9 @@ class LyricsAssertions: if not keywords <= words: details = ( - f"{keywords!r} is not a subset of {words!r}." - f" Words only in expected set {keywords - words!r}," - f" Words only in result set {words - keywords!r}." + f"{repr(keywords)} is not a subset of {repr(words)}." + f" Words only in expected set {repr(keywords - words)}," + f" Words only in result set {repr(words - keywords)}." ) self.fail(f"{details} : {msg}") diff --git c/test/plugins/test_player.py i/test/plugins/test_player.py index bf466e1b..e23b6396 100644 --- c/test/plugins/test_player.py +++ i/test/plugins/test_player.py @@ -132,7 +132,7 @@ class MPCResponse: cmd, rest = rest[2:].split("}") return False, (int(code), int(pos), cmd, rest[1:]) else: - raise RuntimeError(f"Unexpected status: {status!r}") + raise RuntimeError(f"Unexpected status: {repr(status)}") def _parse_body(self, body): """Messages are generally in the format "header: content". @@ -145,7 +145,7 @@ class MPCResponse: if not line: continue if ":" not in line: - raise RuntimeError(f"Unexpected line: {line!r}") + raise RuntimeError(f"Unexpected line: {repr(line)}") header, content = line.split(":", 1) content = content.lstrip() if header in repeated_headers: @@ -191,7 +191,7 @@ class MPCClient: responses.append(MPCResponse(response)) response = b"" elif not line: - raise RuntimeError(f"Unexpected response: {line!r}") + raise RuntimeError(f"Unexpected response: {repr(line)}") def serialise_command(self, command, *args): cmd = [command.encode("utf-8")] diff --git c/test/plugins/test_thumbnails.py i/test/plugins/test_thumbnails.py index 07775995..1931061b 100644 --- c/test/plugins/test_thumbnails.py +++ i/test/plugins/test_thumbnails.py @@ -71,7 +71,7 @@ class ThumbnailsTest(BeetsTestCase): return False if path == syspath(LARGE_DIR): return True - raise ValueError(f"unexpected path {path!r}") + raise ValueError(f"unexpected path {repr(path)}") mock_os.path.exists = exists plugin = ThumbnailsPlugin()
The logic is a bit easier to follow now. See: <beetbox#5337 (comment)>
The new version doesn't rely on regular expressions, provides more intuitive names, and will probably be easier to maintain. See: <beetbox#5337 (comment)>
In cases where the values being filled in did not intuitively describe what they represented as URL components, it became difficult to figure out the structure of the URL. See: <beetbox#5337 (comment)>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this! Mostly looks good except for two patterns, see the comments. Both apply to the rest of the PR
@@ -502,7 +505,7 @@ def clause_with_joiner( | |||
return clause, subvals | |||
|
|||
def __repr__(self) -> str: | |||
return f"{self.__class__.__name__}({self.subqueries!r})" | |||
return f"{self.__class__.__name__}({repr(self.subqueries)})" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why !r
got replaced by repr
calls? As far as I'm aware !r
is the preference here: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@snejus My reading of the PEP for f-strings led me to believe the opposite? They claim that they support the old calls, !r
etc, to maintain compatibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good shout @Serene-Arc. What I gathered from that section is that they aren't required anymore since they can be replaced by the equivalent expressions, which was not supported previously.
Regarding the preference, from what I've seen in the wild Python community tends to use the native syntax supported by the f-string instead of an equivalent expression, if possible, for example:
- Datetime
dt = datetime.now()
f"Datetime: {dt:%F %T}"
# vs
f'Datetime: {dt.strftime("%F %T")}'
- Indentation
txt = "text"
f"{txt:>10}"
# vs
f'{" " * (10 - len(txt))}{txt}'
In a similar way, I prefer !r
over repr
call since there's no need to use an expression and it's more concise.
I asked perplexity to see what it thinks: https://www.perplexity.ai/search/should-r-or-repr-call-be-prefe-YGZ43OrrTGOYR2eNjl7QeQ
beetsplug/embedart.py
Outdated
@@ -149,7 +149,7 @@ def embed_func(lib, opts, args): | |||
with open(tempimg, "wb") as f: | |||
f.write(response.content) | |||
except Exception as e: | |||
self._log.error("Unable to save image: {}".format(e)) | |||
self._log.error(f"Unable to save image: {e}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer using lazy formatting for logging messages
self._log.error(f"Unable to save image: {e}") | |
self._log.error(f"Unable to save image: {}", e) |
Context:
- https://www.reddit.com/r/learnpython/comments/1eix0cy/why_avoid_using_fstrings_in_loggingerror_messages/
- https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/logging-format-interpolation.html
Edit: Replaced %s
with {}
to align it with the syntax that beets logger uses
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we don't have any benchmarking tests at the moment, I'm not sure how much of a difference this would be. Since this is largely a disk-bound program and we use multiple threads, I think having fstrings through the code as standard might be a better advantage than whatever mild speed gains there are from this.
The pylint page says that using f-strings is a reasonable option.
Plus we're not passing through to the logging module transparently, so the string interpolation is being done anyway from the looks of things. If we change that module, then it might make sense to switch just the logging events, but even then, we'd need benchmarks to see.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern has less to do with speed and more with errors.
For example, I edited FieldSort
implementation to embed an error into its __repr__
method:
class FieldSort(Sort):
...
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}{1 / 0}"
f"({repr(self.field)}, ascending={repr(self.ascending)})"
)
I'm using beets logger. Note that the logging level is 30
(WARNING)
from beets.logging import getLogger
log = getLogger(__name__)
log.getEffectiveLevel()
# 30
s = FieldSort("field")
Using f-string
[ins] In [20]: log.debug(f"Sort {s}")
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ in <module>:1 │
│ │
│ /home/sarunas/repo/beets/beets/dbcore/query.py:1019 in __repr__ │
│ │
│ 1016 │ │
│ 1017 │ def __repr__(self) -> str: │
│ 1018 │ │ return ( │
│ ❱ 1019 │ │ │ f"{self.__class__.__name__}{1 / 0}" │
│ 1020 │ │ │ f"({repr(self.field)}, ascending={repr(self.ascending)})" │
│ 1021 │ │ ) │
│ 1022 │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ZeroDivisionError: division by zero
Lazy logging
log.debug("Sort: {}", s)
With an f-string this issue is raised immediately in the main thread, regardless of whether the user runs beets in verbose mode or not. With lazy logging, that logic does not run.
Lazily, even when this is allowed to be logged out, there's no exception raised in the main logic:
[ins] In [30]: log.warning("Sort: {}", s)
...: print("\n---\nMain logic continues")
--- Logging error ---
Traceback (most recent call last):
File "/home/sarunas/.local/share/pyenv/versions/3.8.19/lib/python3.8/logging/__init__.py", line 1085, in emit
msg = self.format(record)
File "/home/sarunas/.local/share/pyenv/versions/3.8.19/lib/python3.8/logging/__init__.py", line 929, in format
return fmt.format(record)
File "/home/sarunas/.local/share/pyenv/versions/3.8.19/lib/python3.8/logging/__init__.py", line 668, in format
record.message = record.getMessage()
File "/home/sarunas/.local/share/pyenv/versions/3.8.19/lib/python3.8/logging/__init__.py", line 371, in getMessage
msg = str(self.msg)
File "/home/sarunas/repo/beets/beets/logging.py", line 70, in __str__
return self.msg.format(*args, **kwargs)
File "/home/sarunas/repo/beets/beets/dbcore/query.py", line 1019, in __repr__
f"{self.__class__.__name__}{1 / 0}"
ZeroDivisionError: division by zero
Call stack:
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/bin/ptipython", line 8, in <module>
sys.exit(run())
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/lib/python3.8/site-packages/ptpython/entry_points/run_ptipython.py", line 72, in run
embed(
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/lib/python3.8/site-packages/ptpython/ipython.py", line 323, in embed
shell(header=header, stack_depth=2, compile_flags=compile_flags)
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/lib/python3.8/site-packages/IPython/terminal/embed.py", line 251, in __call__
self.mainloop(
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/lib/python3.8/site-packages/IPython/terminal/embed.py", line 343, in mainloop
self.interact()
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/lib/python3.8/site-packages/IPython/terminal/interactiveshell.py", line 881, in interact
self.run_cell(code, store_history=True)
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/lib/python3.8/site-packages/IPython/core/interactiveshell.py", line 3009, in run_cell
result = self._run_cell(
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/lib/python3.8/site-packages/IPython/core/interactiveshell.py", line 3064, in _run_cell
result = runner(coro)
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/lib/python3.8/site-packages/IPython/core/async_helpers.py", line 129, in _pseudo_sync_runner
coro.send(None)
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/lib/python3.8/site-packages/IPython/core/interactiveshell.py", line 3269, in run_cell_async
has_raised = await self.run_ast_nodes(code_ast.body, cell_name,
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/lib/python3.8/site-packages/IPython/core/interactiveshell.py", line 3448, in run_ast_nodes
if await self.run_code(code, result, async_=asy):
File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.8/lib/python3.8/site-packages/IPython/core/interactiveshell.py", line 3508, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-30-ecad3783550f>", line 1, in <module>
log.warning("Sort: {}", s)
File "/home/sarunas/.local/share/pyenv/versions/3.8.19/lib/python3.8/logging/__init__.py", line 1458, in warning
self._log(WARNING, msg, args, **kwargs)
File "/home/sarunas/repo/beets/beets/logging.py", line 88, in _log
return super()._log(
Message: <beets.logging.StrFormatLogger._LogMessage object at 0x765899d57310>
Arguments: ()
---
Main logic continues
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree with this argument. It seems impossible to me that any of the formatting logic in the codebase can fail depending on the current logging level. Any f-string passed to log
should be safe to evaluate at any logging level. If you can find an actual example of this not being true in the current codebase, please show us.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree with this argument. It seems impossible to me that any of the formatting logic in the codebase can fail depending on the current logging level.
Sorry, I must have miscommunicated - normally, when logging logic is clean and does not raise any exceptions, there is no difference between f-string and lazy formatting (except for a slight performance difference).
On the other hand, if logging logic happens to be broken, for example, an exception is raised in __repr__
method,
-
if the logging message is formatted with an f-string
self._log.debug(f"Object: {repr(obj)}")
Since
obj.__repr__
runs when this line is executed, the exception is always raised immediately (regardless of the logging level) in the main logic. If this is not handled, Python exits immediately. That's because Python executes an f-string expression right when it comes across one. -
using lazy logging
self._log.debug("Object: {}", obj)
obj.__repr__
is called lazily if the user's configured logging level allows the message to be logged out. Ifobj.__repr__
fails, the exception is only printed in the background and never raised in the main logic. That's because the broken expression is not in the source code (Python only comes acrossobj
when it reads this line). Instead,obj.__repr__
gets executed by thelogging
module following a form of this (simplified) logic:def debug(self): if USER_LOGGING_LEVEL >= DEBUG: try: # note __repr__ gets called implicitly if an object does not have __str__ method print(self.msg.format(self.args)) except Exception as e: print(e)
-
Note that in the example in my previous comment, I adjusted a
__repr__
method implementation with1 / 0
expression in order to simulate this issue.
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}{1 / 0}"
f"({repr(self.field)}, ascending={repr(self.ascending)})"
)
Ultimately, none of this is based on my subjective personal opinion - this is one of Python's logging best practices. If you're interested, have a read here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well ... there shouldn't be any errors in the logging logic. If there are, we want to find out about them and fix them. Making them less noticeable doesn't really help that. We want users to notice that something is wrong with the program, because there otherwise we can't find out that there is something wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are, we want to find out about them and fix them.
We've got 62% coverage, so we need to test the rest of 38% of code in order to be sure about this.
Making them less noticeable doesn't really help that. We want users to notice that something is wrong with the program, because there otherwise we can't find out that there is something wrong.
I agree that visibility is important. But are we ready to accept that this crashes beets immediately, since an error in an f-string is not handled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While coverage is not complete, I think it is highly unlikely that this can cause crashes. Crashes could occur either due to incorrect expressions within f-strings, or due to actual __repr__
methods we implement. I peeked around at the __repr__
implementations in the codebase, and I don't think any of them are problematic. Since f-strings weren't used in the codebase until this PR, a simple review of it should show that there aren't any incorrect expressions within the f-strings. Even if a crash or two does occur, I think it's worth it. I'm happy to look into increasing coverage for __repr__
in a later PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if a crash or two does occur, I think it's worth it.
I don't think I am on the same page regarding this. Mistakes happen, and issues slip through the reviews (#5356). If I accidentally merge some untested code which raises an exception while formatting log.debug
message, this will make beets unusable until the issue is addressed and the fix is merged.
Some proactive users will report the issue here, however others (especially new) users will stop using beets since it's not functional.
At least for me, this drawback is more important than anything we may gain from increased visibility.
Even if we are happy to accept the above, most of the code base uses lazy logging, so I think we want to be consistent with what we already have.
For example, we've got 28 log.debug
statements that use non-lazy logging:
$ oneline_python **/*.py | grep -E 'log.debug\(.*(format\(|f"|" \+ )'
log.debug("found duplicates: {}".format([o.id for o in found_duplicates]))
self._log.debug(f"adding simple rewrite '{pattern}' → '{value}' " f"for field {fieldname}")
self._log.debug(f"adding advanced rewrite to '{replacement}' " f"for field {fieldname}")
log.debug("Listening for control signals on {}:{}".format(host, ctrl_port))
self._log.debug(f"Searching {self.data_source} for '{query}'")
self._log.debug("{}: error receiving response".format(self.NAME))
self._log.debug("{}: error loading response: {}".format(self.NAME, response.text))
self._log.debug("google: error loading response: {}".format(response.text))
self._log.debug("lastfm: error loading response: {}".format(response.text))
self._log.debug("Error: " + str(e))
self._log.debug("Spotify: error loading response: {}".format(response.text))
self._log.debug(f"Cover art URL {image_url} found for {album}")
self._log.debug(f"Cover art URL not found for {album}")
self._log.debug(f"Cover art URL not found for {album}")
self._log.debug("using {0.LOC_STR} image {1}".format(source, util.displayable_path(out.path)))
self._log.debug(f"Invalid Search Error: {e}")
self._log.debug(f"{self.__class__.__name__}: {message}", *args)
self._log.debug(f"loading iTunes library from {library_path}")
self._log.debug(f"analyzing {item}")
self._log.debug("{}: {} blocks over {} LUFS".format(item, n_blocks, gating_threshold))
self._log.debug("{}: gain {} LU, peak {}".format(item, gain, peak))
self._log.debug(f"{self.data_source} access token has expired. " f"Reauthenticating.")
self._log.debug(f"Too many API requests. Retrying after " f"{seconds} seconds.")
self._log.debug(f"Searching {self.data_source} for '{query}'")
self._log.debug(f"using {ArtResizer.shared.method} to write metadata")
log.debug(f"artresizer: method is {self.local_method.NAME}")
self._log.debug("debug " + name)
self._log.debug("debug " + name)
And 237 that use lazy logging:
$ oneline_python **/*.py | grep 'log.debug(.*{' | grep -vE 'log.debug.*(format\(|f"|" \+ )'
log.debug("embedding {0}", displayable_path(imagepath))
log.debug("Resizing album art to {0} pixels wide and encoding at quality \
log.debug("Clearing art for {0}", item)
log.debug("Searching for discovered album ID: {0}", first)
log.debug("Candidate: {0} - {1} ({2})", info.artist, info.album, info.album_id)
log.debug("Ignored. Missing required tag: {0}", req_tag)
log.debug("Ignored. Penalty: {0}", penalty)
log.debug("Success. Distance: {0}", dist)
log.debug("Tagging {0} - {1}", cur_artist, cur_album)
log.debug("Searching for album ID: {0}", search_id)
log.debug("Album ID match recommendation is {0}", rec)
log.debug("Search terms: {0} - {1}", search_artist, search_album)
log.debug("Additional search terms: {0}", extra_tags)
log.debug("Album might be VA: {0}", va_likely)
log.debug("Evaluating {0} candidates.", len(candidates))
log.debug("Searching for track ID: {0}", trackid)
log.debug("Item search terms: {0} - {1}", search_artist, search_title)
log.debug("Found {0} candidates.", len(candidates))
log.debug("Album {} has too many tracks", release["id"])
log.debug("Retrieving tracks starting at {}", i)
log.debug("Found link to {} release via MusicBrainz", source.capitalize())
log.debug("Searching for MusicBrainz releases with: {!r}", criteria)
log.debug("Requesting MusicBrainz release {}", releaseid)
log.debug("Invalid MBID ({0}).", releaseid)
log.debug("Invalid MBID ({0}).", releaseid)
log.debug("state file could not be read: {0}", exc)
log.debug("removing {0} old duplicated items", len(duplicate_items))
log.debug("deleting duplicate {0}", util.displayable_path(item.path))
log.debug("Set field {1}={2} for {0}", displayable_path(self.paths), field, value)
log.debug("Reimported {} {}. Not preserving flexible attributes {}. " "Path: {}", noun, new_obj.id, overwritten_fields, displayable_path(new_obj.path))
log.debug("Reimported album {}. Preserving attribute ['added']. " "Path: {}", self.album.id, displayable_path(self.album.path))
log.debug("Reimported album {}. Preserving flexible attributes {}. " "Path: {}", self.album.id, list(album_fields.keys()), displayable_path(self.album.path))
log.debug("Reimported item {}. Preserving attribute ['added']. " "Path: {}", item.id, displayable_path(item.path))
log.debug("Reimported item {}. Preserving flexible attributes {}. " "Path: {}", item.id, list(item_fields.keys()), displayable_path(item.path))
log.debug("Replacing item {0}: {1}", dup_item.id, displayable_path(item.path))
log.debug("{0} of {1} items replaced", sum(bool(v) for v in self.replaced_items.values()), len(self.imported_items()))
log.debug("Set field {1}={2} for {0}", displayable_path(self.paths), field, value)
log.debug("Removing extracted directory: {0}", displayable_path(self.toppath))
log.debug("Skipping previously-imported path: {0}", displayable_path(path))
log.debug("Skipping previously-imported path: {0}", displayable_path(dirs))
log.debug("Extracting archive: {0}", displayable_path(self.toppath))
log.debug("Archive extracted to: {0}", self.toppath)
log.debug("yielding album {0}: {1} - {2}", album.id, album.albumartist, album.album)
log.debug("Looking up: {0}", displayable_path(task.paths))
log.debug("default action for duplicates: {0}", duplicate_action)
log.debug("moving {0} to synchronize path", util.displayable_path(self.path))
log.debug("moving album art {0} to {1}", util.displayable_path(old_art), util.displayable_path(new_art))
log.debug("Parsed query: {!r}", query)
log.debug("Parsed sort: {!r}", sort)
self._log.debug("Successfully submitted AcousticBrainz analysis " "for {}.", item)
self._log.debug("fetching URL: {}", url)
self._log.debug("Invalid Response: {}", res.text)
self._log.debug("attribute {} of {} set to {}", attr, item, val)
self._log.debug("skipping attribute {} of {}" " (value {}) due to config", attr, item, val)
self._log.debug("Data {} could not be mapped to scheme {} " "because key {} was not found", subdata, v, k)
self._log.debug("running command: {}", displayable_path(list2cmdline(cmd)))
self._log.debug("checking path: {}", dpath)
self._log.debug("authentication error: {0}", e)
self._log.debug("authentication error: {0}", e)
self._log.debug("Beatport token {0}, secret {1}", token, secret)
self._log.debug("API Error: {0} (query: {1})", e, query)
self._log.debug("API Error: {0} (query: {1})", e, query)
self._log.debug("Searching for release {0}", release_id)
self._log.debug("Searching for track {0}", track_id)
self.server._log.debug("{}[{}]: {}", kind, self.address, message)
self.server._log.debug("CTRL {}[{}]: {}", kind, self.address, message)
self._log.debug("moving album {}", album)
log.debug("fingerprint matching {0} failed: {1}", util.displayable_path(repr(path)), exc)
log.debug("chroma: fingerprinted {0}", util.displayable_path(repr(path)))
log.debug("matched recordings {0} on releases {1}", recording_ids, release_ids)
self._log.debug("acoustid album candidates: {0}", len(albums))
self._log.debug("acoustid item candidates: {0}", len(tracks))
self._log.debug("Command {0} exited with status {1}: {2}", args, exc.returncode, exc.output)
self._log.debug("embedding album art from {}", util.displayable_path(album.artpath))
self._log.debug("image size: {}", size)
self._log.debug("Error fetching album tracks for {}", deezer_id)
self._log.debug("Error fetching album tracks for {}", track_data["album"]["id"])
self._log.debug("Found {} result(s) from {} for '{}'", len(response_data), self.data_source, query)
self._log.debug("No deezer_track_id present for: {}", item)
self._log.debug("Deezer track: {} has {} rank", deezer_track_id, rank)
self._log.debug("Invalid Deezer track_id: {}", e)
self._log.debug("connection error: {0}", e)
self._log.debug("connection error: {0}", e)
self._log.debug("Discogs token {0}, secret {1}", token, secret)
self._log.debug("API Error: {0} (query: {1})", e, query)
self._log.debug("API Error: {0} (query: {1})", e, query)
self._log.debug("searching within album {0}", album_cur.album)
self._log.debug("Searching for release {0}", album_id)
self._log.debug("API Error: {0} (query: {1})", e, result.data["resource_url"])
self._log.debug("Communication error while searching for {0!r}", query, exc_info=True)
self._log.debug("Searching for master release {0}", master_id)
self._log.debug("API Error: {0} (query: {1})", e, result.data["resource_url"])
self._log.debug("{}", traceback.format_exc())
self._log.debug("Invalid position: {0}", position)
self._log.debug("key {0} on item {1} not cached:" "computing checksum", key, displayable_path(item.path))
self._log.debug("computed checksum for {0} using {1}", item.title, key)
self._log.debug("failed to checksum {0}: {1}", displayable_path(item.path), e)
self._log.debug("key {0} on item {1} cached:" "not computing checksum", key, displayable_path(item.path))
self._log.debug("some keys {0} on item {1} are null or empty:" " skipping", keys, displayable_path(obj.path))
self._log.debug("all keys {0} on item {1} are null or empty:" " skipping", keys, displayable_path(obj.path))
self._log.debug("key {0} on item {1} is null " "or empty: setting from item {2}", f, displayable_path(objs[0].path), displayable_path(o.path))
self._log.debug("item {0} missing from album {1}:" " merging from {2} into {3}", missing, objs[0], displayable_path(o.path), displayable_path(missing.destination()))
log.debug("invoking editor command: {!r}", cmd)
self._log.debug("saving changes to {}", ob)
self._log.debug("Removing album art file for {0}", album)
self._log.debug("image size: {}", self.size)
self._log.debug("image too small ({} < {})", self.size[0], plugin.minwidth)
self._log.debug("image is not close enough to being " "square, ({} - {} > {})", long_edge, short_edge, plugin.margin_px)
self._log.debug("image is not close enough to being " "square, ({} - {} > {})", long_edge, short_edge, margin_px)
self._log.debug("image is not square ({} != {})", self.size[0], self.size[1])
self._log.debug("image needs rescaling ({} > {})", self.size[0], plugin.maxwidth)
self._log.debug("image needs resizing ({}B > {}B)", filesize, plugin.max_filesize)
self._log.debug("image needs reformatting: {} -> {}", fmt, plugin.cover_format)
log.debug("{}: {}", message, prepped.url)
self._log.debug("not a supported image: {}", real_ct or "unknown content type")
self._log.debug("downloaded art to: {0}", util.displayable_path(filename))
self._log.debug("error fetching art: {}", exc)
self._log.debug("error cleaning up tmp art: {}", exc)
self._log.debug("scraped art URL: {0}", resp.url)
self._log.debug("google fetchart error: {0}", reason)
self._log.debug("fanart.tv: error loading response: {}", response.text)
self._log.debug("fanart.tv: error on request: {}", data["error message"])
self._log.debug("iTunes search failed: {0}", e)
self._log.debug("Could not decode json response: {0}", e)
self._log.debug("{} not found in json. Fields are {} ", e, list(r.json().keys()))
self._log.debug("iTunes search for {!r} got no results", payload["term"])
self._log.debug("Malformed itunes candidate: {} not found in {}", e, list(c.keys()))
self._log.debug("Malformed itunes candidate: {} not found in {}", e, list(c.keys()))
self._log.debug("wikipedia: error scraping dbpedia response: {}", dbpedia_response.text)
self._log.debug("using well-named art file {0}", util.displayable_path(fn))
self._log.debug("using fallback art file {0}", util.displayable_path(remaining[0]))
self._log.debug("lastfm: no results for {}", album.mb_albumid)
self._log.debug("Storing art_source for {0.albumartist} - {0.album}", album)
self._log.debug("trying source {0} for album {1.albumartist} - {1.album}", SOURCE_NAMES[type(source)], album)
self._log.debug('running command "{0}" for event {1}', " ".join(command_pieces), event)
self._log.debug("Recorded mtime {0} for item '{1}' imported from " "'{2}'", mtime, util.displayable_path(destination), util.displayable_path(source))
self._log.debug("Album '{0}' is reimported, skipping import of " "added dates for the album and its items.", util.displayable_path(album.path))
self._log.debug("Import of album '{0}', selected album.added={1} " "from item file mtimes.", album.album, album.added)
self._log.debug("Item '{0}' is reimported, skipping import of " "added date.", util.displayable_path(item.path))
self._log.debug("Import of item '{0}', selected item.added={1}", util.displayable_path(item.path), item.added)
self._log.debug("Write of item '{0}', selected item.added={1}", util.displayable_path(item.path), item.added)
self._log.debug("adding item field {0}", key)
self._log.debug("adding album field {0}", key)
log.debug("Sending event: {0}", event)
log.debug("Extracting {} ID from '{}'", url_type, id_)
self._log.debug("{0} already added", album_dir)
self._log.debug("Loading canonicalization tree {0}", c14n_filename)
self._log.debug("added last.fm item genre ({0}): {1}", src, item.genre)
self._log.debug("added last.fm album genre ({0}): {1}", src, album.genre)
self._log.debug("added last.fm item genre ({0}): {1}", src, item.genre)
self._log.debug("added last.fm item genre ({0}): {1}", src, item.genre)
self._log.debug("last.fm error: {0}", exc)
self._log.debug("{}", traceback.format_exc())
log.debug("query: {0} - {1} ({2})", artist, title, album)
log.debug("no album match, trying by album/title: {0} - {1}", album, title)
log.debug("match: {0} - {1} ({2}) " "updating: play_count {3} => {4}", song.artist, song.title, song.album, count, new_count)
self._log.debug("loading extension {}", ext)
self._log.debug("applying changes to {}", album_formatted)
self._log.debug("moving album {0}", album_formatted)
self._log.debug("track {0} in album {1}", track_info.track_id, album_info.album_id)
self._log.debug("music_directory: {0}", self.music_directory)
self._log.debug("strip_path: {0}", self.strip_path)
self._log.debug("returning: {0}", result)
self._log.debug("updated: {0} = {1} [{2}]", attribute, item[attribute], displayable_path(item.path))
self._log.debug('unhandled status "{0}"', status)
self._log.debug("no composer for {}; add one at " "https://musicbrainz.org/work/{}", item, work_info["work"]["id"])
self._log.debug("error fetching work: {}", e)
self._log.debug("Work fetched: {} - {}", parent_info["parentwork"], parent_info["parent_composer"])
self._log.debug("Work fetched: {} - no parent composer", parent_info["parentwork"])
self._log.debug("{}: Work present, skipping", item)
log.debug("set permissions to {}, but permissions are now {}", permission, os.stat(syspath(path)).st_mode & 0o777)
self._log.debug("setting file permissions on {}", displayable_path(path))
self._log.debug("setting directory permissions on {}", displayable_path(path))
log.debug("executing command: {} {!r}", command_str, open_args)
self._log.debug("applied track gain {0} LU, peak {1} of FS", item.rg_track_gain, item.rg_track_peak)
self._log.debug("applied album gain {0} LU, peak {1} of FS", item.rg_album_gain, item.rg_album_peak)
self._log.debug("done analyzing {0}", item)
self._log.debug("done analyzing {0}", item)
self._log.debug("applied r128 track gain {0} LU", item.r128_track_gain)
self._log.debug("applied r128 album gain {0} LU", item.r128_album_gain)
self._log.debug("{}: gain {} LU, peak {}", task.album, album_gain, album_peak)
self._log.debug("executing {0}", " ".join(map(displayable_path, cmd)))
self._log.debug("analyzing {0} files", len(items))
self._log.debug("executing {0}", " ".join(map(displayable_path, cmd)))
self._log.debug("bad tool output: {0}", text)
self._log.debug("error in rg.title_gain() call: {}", exc)
self._log.debug("ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}", item.artist, item.title, rg_track_gain, rg_track_peak)
self._log.debug("ReplayGain for track {0}: {1:.2f}, {2:.2f}", item, rg_track_gain, rg_track_peak)
self._log.debug("ReplayGain for album {0}: {1:.2f}, {2:.2f}", task.items[0].album, rg_album_gain, rg_album_peak)
self._log.debug("adding template field {0}", key)
self._log.debug("auto-scrubbing {0}", util.displayable_path(item.path))
self._log.debug("{0} will be updated because of {1}", n, model)
self._log.debug("{} access token: {}", self.data_source, self.access_token)
self._log.debug("Album removed from Spotify: {}", album_id)
self._log.debug("Spotify API error: {}", e)
self._log.debug("Found {} result(s) from {} for '{}'", len(response_data), self.data_source, query)
self._log.debug("Your beets query returned no items, skipping {}.", self.data_source)
self._log.debug("{} track(s) found, count: {}", self.data_source, len(response_data_tracks))
self._log.debug("Most popular track chosen, count: {}", len(response_data_tracks))
self._log.debug("Total {} tracks", len(items))
self._log.debug("Popularity already present for: {}", item)
self._log.debug("No track_id present for: {}", item)
self._log.debug("track_popularity: {} and track_isrc: {}", track_data.get("popularity"), track_data.get("external_ids").get("isrc"))
self._log.debug("Spotify API error: {}", e)
self._log.debug("URL is {0}", url)
self._log.debug("auth type is {0}", config["subsonic"]["auth"])
self._log.debug('"{0}" -> "{1}"', text, r)
self._log.debug("using {0.name} to compute URIs", uri_getter)
self._log.debug("generating thumbnail for {0}", album)
self._log.debug("found a suitable {1}x{1} thumbnail for {0}, " "forcing regeneration", album, size)
self._log.debug("{1}x{1} thumbnail for {0} exists and is " "recent enough", album, size)
self._log.debug("Wrote file {0}", displayable_path(outfilename))
self._log.debug("{0}: {1} -> None", field, value)
log.debug("skipping {0} because mtime is up to date ({1})", displayable_path(item.path), item.mtime)
log.debug("emptied album {0}", album_id)
log.debug("moving album {0}", album_id)
log.debug("moving: {0}", util.displayable_path(obj.path))
log.debug("Invalid color_name: {0}", color_name)
log.debug("plugin paths: {0}", util.displayable_path(paths))
log.debug("overlaying configuration: {0}", util.displayable_path(overlay_path))
log.debug("user configuration: {0}", util.displayable_path(config_path))
log.debug("no user configuration found at {0}", util.displayable_path(config_path))
log.debug("data directory: {0}", util.displayable_path(config.config_dir()))
log.debug("{}", traceback.format_exc())
log.debug("library database: {0}\n" "library directory: {1}", util.displayable_path(lib.path), util.displayable_path(lib.directory))
log.debug("{}", traceback.format_exc())
log.debug("{}", traceback.format_exc())
log.debug("ImageMagick version check failed: {}", exc)
log.debug("artresizer: ImageMagick resizing {0} to {1}", displayable_path(path_in), displayable_path(path_out))
log.debug("`convert` exited with (status {}) when " "getting size with command {}:\n{}", exc.returncode, cmd, exc.output.strip())
log.debug("comparing images with pipeline {} | {}", convert_cmd, compare_cmd)
log.debug("ImageMagick convert failed with status {}: {!r}", convert_proc.returncode, convert_stderr)
log.debug("ImageMagick compare failed: {0}, {1}", displayable_path(im2), displayable_path(im1))
log.debug("IM output is not a number: {0!r}", out_str)
log.debug("ImageMagick compare score: {0}", phash_diff)
log.debug("artresizer: PIL resizing {0} to {1}", displayable_path(path_in), displayable_path(path_out))
log.debug("PIL Pass {0} : Output size: {1}B", i, filesize)
See oneline_python
shell function if you want to check yourself
oneline_python() {
# args: <python-file> ...
# info: _unformat given .py files: remove newlines from each statement
sed -zr '
s/(^|\n) *# [^\n]*//g
s/ # [^\n]*//g
s/,?\n\s*([]}).]+)/\1/g
s/\n\s+(\b(and|or)\b|==)/ \1/g
s/,\s*\n\s+/, /g
s/"\s*\n\s+(%|f['\''"])/" \1/g
s/([[{(])\s*\n\s+/\1/g
s/(["'\''])\n\s*(["'\''+])/\1 \2/g
s/\n\s*( \+)/\1/g
s/(\[[^]\n]+)\n\s*( for)/\1\2/g
' $@ |
tr '\000' '\n'
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's more important to get the rest of the PR merged than to argue back and forth over this right now. Let me fix this change, and any other logging-related f-strings I added, and then we should merge.
Along the way, I've made some small code improvements beyond just the use of f-strings; I don't think these will conflict with others' work. In particular, I've avoided modifying the formatting of paths; there is work to greatly simplify that using 'pathlib', at which point a second commit can clean them up. # Conflicts: # beets/test/_common.py # test/plugins/test_player.py diff --git c/beets/__init__.py i/beets/__init__.py index 16f51f85..e5b19b33 100644 --- c/beets/__init__.py +++ i/beets/__init__.py @@ -35,7 +35,7 @@ class IncludeLazyConfig(confuse.LazyConfig): except confuse.NotFoundError: pass except confuse.ConfigReadError as err: - stderr.write("configuration `import` failed: {}".format(err.reason)) + stderr.write(f"configuration `import` failed: {err.reason}") config = IncludeLazyConfig("beets", __name__) diff --git c/beets/autotag/hooks.py i/beets/autotag/hooks.py index 363bcaab..a711bc89 100644 --- c/beets/autotag/hooks.py +++ i/beets/autotag/hooks.py @@ -268,7 +268,7 @@ class TrackInfo(AttrDict): # Parameters for string distance function. # Words that can be moved to the end of a string using a comma. -SD_END_WORDS = ["the", "a", "an"] +SD_END_REPLACE = re.compile(r"^(.*), (the|a|an)$") # Reduced weights for certain portions of the string. SD_PATTERNS = [ (r"^the ", 0.1), @@ -317,11 +317,11 @@ def string_dist(str1: Optional[str], str2: Optional[str]) -> float: # Don't penalize strings that move certain words to the end. For # example, "the something" should be considered equal to # "something, the". - for word in SD_END_WORDS: - if str1.endswith(", %s" % word): - str1 = "{} {}".format(word, str1[: -len(word) - 2]) - if str2.endswith(", %s" % word): - str2 = "{} {}".format(word, str2[: -len(word) - 2]) + def replacer(m: re.Match[str]) -> str: + return f"{m.group(2)} {m.group(1)}" + + str1 = re.sub(SD_END_REPLACE, replacer, str1) + str2 = re.sub(SD_END_REPLACE, replacer, str2) # Perform a couple of basic normalizing substitutions. for pat, repl in SD_REPLACE: @@ -469,9 +469,7 @@ class Distance: def update(self, dist: "Distance"): """Adds all the distance penalties from `dist`.""" if not isinstance(dist, Distance): - raise ValueError( - "`dist` must be a Distance object, not {}".format(type(dist)) - ) + raise ValueError(f"`dist` must be a Distance object, not {dist}") for key, penalties in dist._penalties.items(): self._penalties.setdefault(key, []).extend(penalties) diff --git c/beets/autotag/mb.py i/beets/autotag/mb.py index 80ac6c8e..e16643d7 100644 --- c/beets/autotag/mb.py +++ i/beets/autotag/mb.py @@ -66,9 +66,7 @@ class MusicBrainzAPIError(util.HumanReadableException): super().__init__(reason, verb, tb) def get_message(self): - return "{} in {} with query {}".format( - self._reasonstr(), self.verb, repr(self.query) - ) + return f"{self._reasonstr()} in {self.verb} with query {self.query!r}" log = logging.getLogger("beets") diff --git c/beets/dbcore/db.py i/beets/dbcore/db.py index 566c1163..55ba6f11 100755 --- c/beets/dbcore/db.py +++ i/beets/dbcore/db.py @@ -396,10 +396,9 @@ class Model(ABC): return obj def __repr__(self) -> str: - return "{}({})".format( - type(self).__name__, - ", ".join(f"{k}={v!r}" for k, v in dict(self).items()), - ) + name = type(self).__name__ + fields = ", ".join(f"{k}={v!r}" for k, v in dict(self).items()) + return f"{name}({fields})" def clear_dirty(self): """Mark all fields as *clean* (i.e., not needing to be stored to @@ -414,10 +413,11 @@ class Model(ABC): has a reference to a database (`_db`) and an id. A ValueError exception is raised otherwise. """ + name = type(self).__name__ if not self._db: - raise ValueError("{} has no database".format(type(self).__name__)) + raise ValueError(f"{name} has no database") if need_id and not self.id: - raise ValueError("{} has no id".format(type(self).__name__)) + raise ValueError(f"{name} has no id") return self._db @@ -585,38 +585,34 @@ class Model(ABC): will be. """ if fields is None: - fields = self._fields + fields = self._fields.keys() + fields = set(fields) - {"id"} db = self._check_db() # Build assignments for query. - assignments = [] - subvars = [] - for key in fields: - if key != "id" and key in self._dirty: - self._dirty.remove(key) - assignments.append(key + "=?") - value = self._type(key).to_sql(self[key]) - subvars.append(value) + dirty_fields = list(fields & self._dirty) + self._dirty -= fields + assignments = ",".join(f"{k}=?" for k in dirty_fields) + subvars = [self._type(k).to_sql(self[k]) for k in dirty_fields] with db.transaction() as tx: # Main table update. if assignments: - query = "UPDATE {} SET {} WHERE id=?".format( - self._table, ",".join(assignments) - ) + query = f"UPDATE {self._table} SET {assignments} WHERE id=?" subvars.append(self.id) tx.mutate(query, subvars) # Modified/added flexible attributes. - for key, value in self._values_flex.items(): - if key in self._dirty: - self._dirty.remove(key) - tx.mutate( - "INSERT INTO {} " - "(entity_id, key, value) " - "VALUES (?, ?, ?);".format(self._flex_table), - (self.id, key, value), - ) + flex_fields = set(self._values_flex.keys()) + dirty_flex_fields = list(flex_fields & self._dirty) + self._dirty -= flex_fields + for key in dirty_flex_fields: + tx.mutate( + f"INSERT INTO {self._flex_table} " + "(entity_id, key, value) " + "VALUES (?, ?, ?);", + (self.id, key, self._values_flex[key]), + ) # Deleted flexible attributes. for key in self._dirty: @@ -1192,9 +1188,8 @@ class Database: columns = [] for name, typ in fields.items(): columns.append(f"{name} {typ.sql}") - setup_sql = "CREATE TABLE {} ({});\n".format( - table, ", ".join(columns) - ) + columns_def = ", ".join(columns) + setup_sql = f"CREATE TABLE {table} ({columns_def});\n" else: # Table exists does not match the field set. @@ -1202,8 +1197,8 @@ class Database: for name, typ in fields.items(): if name in current_fields: continue - setup_sql += "ALTER TABLE {} ADD COLUMN {} {};\n".format( - table, name, typ.sql + setup_sql += ( + f"ALTER TABLE {table} ADD COLUMN {name} {typ.sql};\n" ) with self.transaction() as tx: @@ -1215,18 +1210,16 @@ class Database: """ with self.transaction() as tx: tx.script( - """ - CREATE TABLE IF NOT EXISTS {0} ( + f""" + CREATE TABLE IF NOT EXISTS {flex_table} ( id INTEGER PRIMARY KEY, entity_id INTEGER, key TEXT, value TEXT, UNIQUE(entity_id, key) ON CONFLICT REPLACE); - CREATE INDEX IF NOT EXISTS {0}_by_entity - ON {0} (entity_id); - """.format( - flex_table - ) + CREATE INDEX IF NOT EXISTS {flex_table}_by_entity + ON {flex_table} (entity_id); + """ ) # Querying. diff --git c/beets/dbcore/query.py i/beets/dbcore/query.py index f8cf7fe4..357b5685 100644 --- c/beets/dbcore/query.py +++ i/beets/dbcore/query.py @@ -151,6 +151,7 @@ class FieldQuery(Query, Generic[P]): self.fast = fast def col_clause(self) -> Tuple[str, Sequence[SQLiteType]]: + # TODO: Avoid having to insert raw text into SQL clauses. return self.field, () def clause(self) -> Tuple[Optional[str], Sequence[SQLiteType]]: @@ -791,9 +792,7 @@ class DateInterval: def __init__(self, start: Optional[datetime], end: Optional[datetime]): if start is not None and end is not None and not start < end: - raise ValueError( - "start date {} is not before end date {}".format(start, end) - ) + raise ValueError(f"start date {start} is not before end date {end}") self.start = start self.end = end @@ -841,8 +840,6 @@ class DateQuery(FieldQuery[str]): date = datetime.fromtimestamp(timestamp) return self.interval.contains(date) - _clause_tmpl = "{0} {1} ?" - def col_clause(self) -> Tuple[str, Sequence[SQLiteType]]: clause_parts = [] subvals = [] @@ -850,11 +847,11 @@ class DateQuery(FieldQuery[str]): # Convert the `datetime` objects to an integer number of seconds since # the (local) Unix epoch using `datetime.timestamp()`. if self.interval.start: - clause_parts.append(self._clause_tmpl.format(self.field, ">=")) + clause_parts.append(f"{self.field} >= ?") subvals.append(int(self.interval.start.timestamp())) if self.interval.end: - clause_parts.append(self._clause_tmpl.format(self.field, "<")) + clause_parts.append(f"{self.field} < ?") subvals.append(int(self.interval.end.timestamp())) if clause_parts: diff --git c/beets/importer.py i/beets/importer.py index 3a290a03..9786891b 100644 --- c/beets/importer.py +++ i/beets/importer.py @@ -1583,9 +1583,7 @@ def resolve_duplicates(session, task): if task.choice_flag in (action.ASIS, action.APPLY, action.RETAG): found_duplicates = task.find_duplicates(session.lib) if found_duplicates: - log.debug( - "found duplicates: {}".format([o.id for o in found_duplicates]) - ) + log.debug(f"found duplicates: {[o.id for o in found_duplicates]}") # Get the default action to follow from config. duplicate_action = config["import"]["duplicate_action"].as_choice( diff --git c/beets/library.py i/beets/library.py index 84f6a7bf..77d24ecd 100644 --- c/beets/library.py +++ i/beets/library.py @@ -733,13 +733,10 @@ class Item(LibModel): # This must not use `with_album=True`, because that might access # the database. When debugging, that is not guaranteed to succeed, and # can even deadlock due to the database lock. - return "{}({})".format( - type(self).__name__, - ", ".join( - "{}={!r}".format(k, self[k]) - for k in self.keys(with_album=False) - ), - ) + name = type(self).__name__ + keys = self.keys(with_album=False) + fields = (f"{k}={self[k]!r}" for k in keys) + return f"{name}({', '.join(fields)})" def keys(self, computed=False, with_album=True): """Get a list of available field names. diff --git c/beets/plugins.py i/beets/plugins.py index 35995c34..ed5e63b8 100644 --- c/beets/plugins.py +++ i/beets/plugins.py @@ -344,9 +344,9 @@ def types(model_cls): for field in plugin_types: if field in types and plugin_types[field] != types[field]: raise PluginConflictException( - "Plugin {} defines flexible field {} " - "which has already been defined with " - "another type.".format(plugin.name, field) + f"Plugin {plugin.name} defines flexible field " + f"{field} which has already been defined with " + "another type." ) types.update(plugin_types) return types @@ -519,9 +519,8 @@ def feat_tokens(for_artist=True): feat_words = ["ft", "featuring", "feat", "feat.", "ft."] if for_artist: feat_words += ["with", "vs", "and", "con", "&"] - return r"(?<=\s)(?:{})(?=\s)".format( - "|".join(re.escape(x) for x in feat_words) - ) + matcher = "|".join(re.escape(x) for x in feat_words) + return rf"(?<=\s)(?:{matcher})(?=\s)" def sanitize_choices(choices, choices_all): diff --git c/beets/test/_common.py i/beets/test/_common.py index 50dbde43..c12838e2 100644 --- c/beets/test/_common.py +++ i/beets/test/_common.py @@ -155,19 +155,19 @@ class Assertions: assert os.path.exists(syspath(path)), f"file does not exist: {path!r}" def assertNotExists(self, path): # noqa - assert not os.path.exists(syspath(path)), f"file exists: {path!r}" + assert not os.path.exists(syspath(path)), f"file exists: {repr(path)}" def assertIsFile(self, path): # noqa self.assertExists(path) assert os.path.isfile( syspath(path) - ), "path exists, but is not a regular file: {!r}".format(path) + ), f"path exists, but is not a regular file: {repr(path)}" def assertIsDir(self, path): # noqa self.assertExists(path) assert os.path.isdir( syspath(path) - ), "path exists, but is not a directory: {!r}".format(path) + ), f"path exists, but is not a directory: {repr(path)}" def assert_equal_path(self, a, b): """Check that two paths are equal.""" diff --git c/beets/ui/__init__.py i/beets/ui/__init__.py index 8580bd1e..c38ad404 100644 --- c/beets/ui/__init__.py +++ i/beets/ui/__init__.py @@ -775,7 +775,7 @@ def get_replacements(): replacements.append((re.compile(pattern), repl)) except re.error: raise UserError( - "malformed regular expression in replace: {}".format(pattern) + f"malformed regular expression in replace: {pattern}" ) return replacements @@ -1253,22 +1253,15 @@ def show_path_changes(path_changes): # Print every change over two lines for source, dest in zip(sources, destinations): color_source, color_dest = colordiff(source, dest) - print_("{0} \n -> {1}".format(color_source, color_dest)) + print_(f"{color_source} \n -> {color_dest}") else: # Print every change on a single line, and add a header - title_pad = max_width - len("Source ") + len(" -> ") - - print_("Source {0} Destination".format(" " * title_pad)) + source = "Source " + print_(f"{source:<{max_width}} Destination") for source, dest in zip(sources, destinations): - pad = max_width - len(source) color_source, color_dest = colordiff(source, dest) - print_( - "{0} {1} -> {2}".format( - color_source, - " " * pad, - color_dest, - ) - ) + width = max_width - len(source) + len(color_source) + print_(f"{color_source:<{width}} -> {color_dest}") # Helper functions for option parsing. @@ -1294,9 +1287,7 @@ def _store_dict(option, opt_str, value, parser): raise ValueError except ValueError: raise UserError( - "supplied argument `{}' is not of the form `key=value'".format( - value - ) + f"supplied argument `{value}' is not of the form `key=value'" ) option_values[key] = value diff --git c/beets/ui/commands.py i/beets/ui/commands.py index 24cae1dd..3042ca77 100755 --- c/beets/ui/commands.py +++ i/beets/ui/commands.py @@ -213,10 +213,10 @@ def get_singleton_disambig_fields(info: hooks.TrackInfo) -> Sequence[str]: out = [] chosen_fields = config["match"]["singleton_disambig_fields"].as_str_seq() calculated_values = { - "index": "Index {}".format(str(info.index)), - "track_alt": "Track {}".format(info.track_alt), + "index": f"Index {info.index!s}", + "track_alt": f"Track {info.track_alt}", "album": ( - "[{}]".format(info.album) + f"[{info.album}]" if ( config["import"]["singleton_album_disambig"].get() and info.get("album") @@ -242,7 +242,7 @@ def get_album_disambig_fields(info: hooks.AlbumInfo) -> Sequence[str]: chosen_fields = config["match"]["album_disambig_fields"].as_str_seq() calculated_values = { "media": ( - "{}x{}".format(info.mediums, info.media) + f"{info.mediums}x{info.media}" if (info.mediums and info.mediums > 1) else info.media ), @@ -490,7 +490,6 @@ class ChangeRepresentation: """Format colored track indices.""" cur_track = self.format_index(item) new_track = self.format_index(track_info) - templ = "(#{})" changed = False # Choose color based on change. if cur_track != new_track: @@ -502,8 +501,8 @@ class ChangeRepresentation: else: highlight_color = "text_faint" - cur_track = templ.format(cur_track) - new_track = templ.format(new_track) + cur_track = f"(#{cur_track})" + new_track = f"(#{new_track})" lhs_track = ui.colorize(highlight_color, cur_track) rhs_track = ui.colorize(highlight_color, new_track) return lhs_track, rhs_track, changed @@ -711,9 +710,9 @@ class AlbumChange(ChangeRepresentation): if self.match.extra_items: print_(f"Unmatched tracks ({len(self.match.extra_items)}):") for item in self.match.extra_items: - line = " ! {} (#{})".format(item.title, self.format_index(item)) + line = f" ! {item.title} (#{self.format_index(item)})" if item.length: - line += " ({})".format(ui.human_seconds_short(item.length)) + line += f" ({ui.human_seconds_short(item.length)})" print_(ui.colorize("text_warning", line)) @@ -769,7 +768,7 @@ def summarize_items(items, singleton): """ summary_parts = [] if not singleton: - summary_parts.append("{} items".format(len(items))) + summary_parts.append(f"{len(items)} items") format_counts = {} for item in items: @@ -885,7 +884,7 @@ def choose_candidate( if singleton: print_("No matching recordings found.") else: - print_("No matching release found for {} tracks.".format(itemcount)) + print_(f"No matching release found for {itemcount} tracks.") print_( "For help, see: " "https://beets.readthedocs.org/en/latest/faq.html#nomatch" @@ -920,7 +919,7 @@ def choose_candidate( print_(ui.indent(2) + "Candidates:") for i, match in enumerate(candidates): # Index, metadata, and distance. - index0 = "{0}.".format(i + 1) + index0 = f"{i + 1}." index = dist_colorize(index0, match.distance) dist = "({:.1f}%)".format((1 - match.distance) * 100) distance = dist_colorize(dist, match.distance) @@ -1043,9 +1042,9 @@ class TerminalImportSession(importer.ImportSession): path_str0 = displayable_path(task.paths, "\n") path_str = ui.colorize("import_path", path_str0) - items_str0 = "({} items)".format(len(task.items)) + items_str0 = f"({len(task.items)} items)" items_str = ui.colorize("import_path_items", items_str0) - print_(" ".join([path_str, items_str])) + print_(f"{path_str} {items_str}") # Let plugins display info or prompt the user before we go through the # process of selecting candidate. diff --git c/beets/util/__init__.py i/beets/util/__init__.py index 4f0aa283..aa94b6d2 100644 --- c/beets/util/__init__.py +++ i/beets/util/__init__.py @@ -104,7 +104,7 @@ class HumanReadableException(Exception): elif hasattr(self.reason, "strerror"): # i.e., EnvironmentError return self.reason.strerror else: - return '"{}"'.format(str(self.reason)) + return f'"{self.reason!s}"' def get_message(self): """Create the human-readable description of the error, sans diff --git c/beets/util/artresizer.py i/beets/util/artresizer.py index 09cc29e0..550a7c1d 100644 --- c/beets/util/artresizer.py +++ i/beets/util/artresizer.py @@ -44,7 +44,7 @@ def resize_url(url, maxwidth, quality=0): if quality > 0: params["q"] = quality - return "{}?{}".format(PROXY_URL, urlencode(params)) + return f"{PROXY_URL}?{urlencode(params)}" class LocalBackendNotAvailableError(Exception): diff --git c/beets/util/functemplate.py i/beets/util/functemplate.py index 7d7e8f01..35f60b7d 100644 --- c/beets/util/functemplate.py +++ i/beets/util/functemplate.py @@ -166,9 +166,7 @@ class Call: self.original = original def __repr__(self): - return "Call({}, {}, {})".format( - repr(self.ident), repr(self.args), repr(self.original) - ) + return f"Call({self.ident!r}, {self.args!r}, {self.original!r})" def evaluate(self, env): """Evaluate the function call in the environment, returning a diff --git c/beetsplug/absubmit.py i/beetsplug/absubmit.py index fc40b85e..b50a8e7c 100644 --- c/beetsplug/absubmit.py +++ i/beetsplug/absubmit.py @@ -44,9 +44,7 @@ def call(args): try: return util.command_output(args).stdout except subprocess.CalledProcessError as e: - raise ABSubmitError( - "{} exited with status {}".format(args[0], e.returncode) - ) + raise ABSubmitError(f"{args[0]} exited with status {e.returncode}") class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): @@ -65,9 +63,7 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): # Explicit path to extractor if not os.path.isfile(self.extractor): raise ui.UserError( - "Extractor command does not exist: {0}.".format( - self.extractor - ) + f"Extractor command does not exist: {self.extractor}." ) else: # Implicit path to extractor, search for it in path diff --git c/beetsplug/aura.py i/beetsplug/aura.py index 09d85920..77a006de 100644 --- c/beetsplug/aura.py +++ i/beetsplug/aura.py @@ -246,7 +246,7 @@ class AURADocument: else: # Increment page token by 1 next_url = request.url.replace( - f"page={page}", "page={}".format(page + 1) + f"page={page}", f"page={page + 1}" ) # Get only the items in the page range data = [ @@ -431,9 +431,7 @@ class TrackDocument(AURADocument): return self.error( "404 Not Found", "No track with the requested id.", - "There is no track with an id of {} in the library.".format( - track_id - ), + f"There is no track with an id of {track_id} in the library.", ) return self.single_resource_document( self.get_resource_object(self.lib, track) @@ -517,9 +515,7 @@ class AlbumDocument(AURADocument): return self.error( "404 Not Found", "No album with the requested id.", - "There is no album with an id of {} in the library.".format( - album_id - ), + f"There is no album with an id of {album_id} in the library.", ) return self.single_resource_document( self.get_resource_object(self.lib, album) @@ -604,9 +600,7 @@ class ArtistDocument(AURADocument): return self.error( "404 Not Found", "No artist with the requested id.", - "There is no artist with an id of {} in the library.".format( - artist_id - ), + f"There is no artist with an id of {artist_id} in the library.", ) return self.single_resource_document(artist_resource) @@ -731,9 +725,7 @@ class ImageDocument(AURADocument): return self.error( "404 Not Found", "No image with the requested id.", - "There is no image with an id of {} in the library.".format( - image_id - ), + f"There is no image with an id of {image_id} in the library.", ) return self.single_resource_document(image_resource) @@ -779,9 +771,7 @@ def audio_file(track_id): return AURADocument.error( "404 Not Found", "No track with the requested id.", - "There is no track with an id of {} in the library.".format( - track_id - ), + f"There is no track with an id of {track_id} in the library.", ) path = os.fsdecode(track.path) @@ -789,9 +779,7 @@ def audio_file(track_id): return AURADocument.error( "404 Not Found", "No audio file for the requested track.", - ( - "There is no audio file for track {} at the expected location" - ).format(track_id), + f"There is no audio file for track {track_id} at the expected location", ) file_mimetype = guess_type(path)[0] @@ -799,10 +787,8 @@ def audio_file(track_id): return AURADocument.error( "500 Internal Server Error", "Requested audio file has an unknown mimetype.", - ( - "The audio file for track {} has an unknown mimetype. " - "Its file extension is {}." - ).format(track_id, path.split(".")[-1]), + f"The audio file for track {track_id} has an unknown mimetype. " + f"Its file extension is {path.split('.')[-1]}.", ) # Check that the Accept header contains the file's mimetype @@ -814,10 +800,8 @@ def audio_file(track_id): return AURADocument.error( "406 Not Acceptable", "Unsupported MIME type or bitrate parameter in Accept header.", - ( - "The audio file for track {} is only available as {} and " - "bitrate parameters are not supported." - ).format(track_id, file_mimetype), + f"The audio file for track {track_id} is only available as " + f"{file_mimetype} and bitrate parameters are not supported.", ) return send_file( @@ -900,9 +884,7 @@ def image_file(image_id): return AURADocument.error( "404 Not Found", "No image with the requested id.", - "There is no image with an id of {} in the library".format( - image_id - ), + f"There is no image with an id of {image_id} in the library", ) return send_file(img_path) diff --git c/beetsplug/beatport.py i/beetsplug/beatport.py index 6108b039..bcb010dc 100644 --- c/beetsplug/beatport.py +++ i/beetsplug/beatport.py @@ -201,14 +201,10 @@ class BeatportClient: try: response = self.api.get(self._make_url(endpoint), params=kwargs) except Exception as e: - raise BeatportAPIError( - "Error connecting to Beatport API: {}".format(e) - ) + raise BeatportAPIError(f"Error connecting to Beatport API: {e}") if not response: raise BeatportAPIError( - "Error {0.status_code} for '{0.request.path_url}".format( - response - ) + f"Error {response.status_code} for '{response.request.path_url}" ) return response.json()["results"] @@ -219,11 +215,7 @@ class BeatportRelease(BeatportObject): artist_str = ", ".join(x[1] for x in self.artists) else: artist_str = "Various Artists" - return "<BeatportRelease: {} - {} ({})>".format( - artist_str, - self.name, - self.catalog_number, - ) + return f"<BeatportRelease: {artist_str} - {self.name} ({self.catalog_number})>" def __repr__(self): return str(self).encode("utf-8") @@ -237,8 +229,8 @@ class BeatportRelease(BeatportObject): if "category" in data: self.category = data["category"] if "slug" in data: - self.url = "https://beatport.com/release/{}/{}".format( - data["slug"], data["id"] + self.url = ( + f"https://beatport.com/release/{data['slug']}/{data['id']}" ) self.genre = data.get("genre") @@ -246,9 +238,7 @@ class BeatportRelease(BeatportObject): class BeatportTrack(BeatportObject): def __str__(self): artist_str = ", ".join(x[1] for x in self.artists) - return "<BeatportTrack: {} - {} ({})>".format( - artist_str, self.name, self.mix_name - ) + return f"<BeatportTrack: {artist_str} - {self.name} ({self.mix_name})>" def __repr__(self): return str(self).encode("utf-8") @@ -267,9 +257,7 @@ class BeatportTrack(BeatportObject): except ValueError: pass if "slug" in data: - self.url = "https://beatport.com/track/{}/{}".format( - data["slug"], data["id"] - ) + self.url = f"https://beatport.com/track/{data['slug']}/{data['id']}" self.track_number = data.get("trackNumber") self.bpm = data.get("bpm") self.initial_key = str((data.get("key") or {}).get("shortName")) diff --git c/beetsplug/bpd/__init__.py i/beetsplug/bpd/__init__.py index a4cb4d29..d6c75380 100644 --- c/beetsplug/bpd/__init__.py +++ i/beetsplug/bpd/__init__.py @@ -895,9 +895,7 @@ class MPDConnection(Connection): return except BPDIdle as e: self.idle_subscriptions = e.subsystems - self.debug( - "awaiting: {}".format(" ".join(e.subsystems)), kind="z" - ) + self.debug(f"awaiting: {' '.join(e.subsystems)}", kind="z") yield bluelet.call(self.server.dispatch_events()) @@ -929,7 +927,7 @@ class ControlConnection(Connection): func = command.delegate("ctrl_", self) yield bluelet.call(func(*command.args)) except (AttributeError, TypeError) as e: - yield self.send("ERROR: {}".format(e.args[0])) + yield self.send(f"ERROR: {e.args[0]}") except Exception: yield self.send( ["ERROR: server error", traceback.format_exc().rstrip()] @@ -1007,7 +1005,7 @@ class Command: # If the command accepts a variable number of arguments skip the check. if wrong_num and not argspec.varargs: raise TypeError( - 'wrong number of arguments for "{}"'.format(self.name), + f'wrong number of arguments for "{self.name}"', self.name, ) @@ -1114,10 +1112,8 @@ class Server(BaseServer): self.lib = library self.player = gstplayer.GstPlayer(self.play_finished) self.cmd_update(None) - log.info("Server ready and listening on {}:{}".format(host, port)) - log.debug( - "Listening for control signals on {}:{}".format(host, ctrl_port) - ) + log.info(f"Server ready and listening on {host}:{port}") + log.debug(f"Listening for control signals on {host}:{ctrl_port}") def run(self): self.player.run() @@ -1146,9 +1142,7 @@ class Server(BaseServer): pass for tagtype, field in self.tagtype_map.items(): - info_lines.append( - "{}: {}".format(tagtype, str(getattr(item, field))) - ) + info_lines.append(f"{tagtype}: {getattr(item, field)!s}") return info_lines @@ -1306,22 +1300,15 @@ class Server(BaseServer): item = self.playlist[self.current_index] yield ( - "bitrate: " + str(item.bitrate / 1000), - "audio: {}:{}:{}".format( - str(item.samplerate), - str(item.bitdepth), - str(item.channels), - ), + f"bitrate: {item.bitrate / 1000}", + f"audio: {item.samplerate!s}:{item.bitdepth!s}:{item.channels!s}", ) (pos, total) = self.player.time() yield ( - "time: {}:{}".format( - str(int(pos)), - str(int(total)), - ), - "elapsed: " + f"{pos:.3f}", - "duration: " + f"{total:.3f}", + f"time: {int(pos)}:{int(total)}", + f"elapsed: {pos:.3f}", + f"duration: {total:.3f}", ) # Also missing 'updating_db'. diff --git c/beetsplug/convert.py i/beetsplug/convert.py index f150b7c3..03b9034a 100644 --- c/beetsplug/convert.py +++ i/beetsplug/convert.py @@ -63,9 +63,7 @@ def get_format(fmt=None): command = format_info["command"] extension = format_info.get("extension", fmt) except KeyError: - raise ui.UserError( - 'convert: format {} needs the "command" field'.format(fmt) - ) + raise ui.UserError(f'convert: format {fmt} needs the "command" field') except ConfigTypeError: command = config["convert"]["formats"][fmt].get(str) extension = fmt diff --git c/beetsplug/deezer.py i/beetsplug/deezer.py index a861ea0e..a5d1df09 100644 --- c/beetsplug/deezer.py +++ i/beetsplug/deezer.py @@ -113,7 +113,7 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin): else: raise ui.UserError( "Invalid `release_date` returned " - "by {} API: '{}'".format(self.data_source, release_date) + f"by {self.data_source} API: '{release_date}'" ) tracks_obj = self.fetch_data(self.album_url + deezer_id + "/tracks") if tracks_obj is None: diff --git c/beetsplug/discogs.py i/beetsplug/discogs.py index 344d67a2..98e3a2e4 100644 --- c/beetsplug/discogs.py +++ i/beetsplug/discogs.py @@ -610,7 +610,7 @@ class DiscogsPlugin(BeetsPlugin): idx, medium_idx, sub_idx = self.get_track_index( subtracks[0]["position"] ) - position = "{}{}".format(idx or "", medium_idx or "") + position = f"{idx or ''}{medium_idx or ''}" if tracklist and not tracklist[-1]["position"]: # Assume the previous index track contains the track title. @@ -632,8 +632,8 @@ class DiscogsPlugin(BeetsPlugin): # option is set if self.config["index_tracks"]: for subtrack in subtracks: - subtrack["title"] = "{}: {}".format( - index_track["title"], subtrack["title"] + subtrack["title"] = ( + f"{index_track['title']}: {subtrack['title']}" ) tracklist.extend(subtracks) else: diff --git c/beetsplug/edit.py i/beetsplug/edit.py index 323dd9e4..61f2020a 100644 --- c/beetsplug/edit.py +++ i/beetsplug/edit.py @@ -47,9 +47,7 @@ def edit(filename, log): try: subprocess.call(cmd) except OSError as exc: - raise ui.UserError( - "could not run editor command {!r}: {}".format(cmd[0], exc) - ) + raise ui.UserError(f"could not run editor command {cmd[0]!r}: {exc}") def dump(arg): @@ -72,9 +70,7 @@ def load(s): for d in yaml.safe_load_all(s): if not isinstance(d, dict): raise ParseError( - "each entry must be a dictionary; found {}".format( - type(d).__name__ - ) + f"each entry must be a dictionary; found {type(d).__name__}" ) # Convert all keys to strings. They started out as strings, diff --git c/beetsplug/embedart.py i/beetsplug/embedart.py index 740863bf..b8b894ad 100644 --- c/beetsplug/embedart.py +++ i/beetsplug/embedart.py @@ -137,7 +137,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): response = requests.get(opts.url, timeout=5) response.raise_for_status() except requests.exceptions.RequestException as e: - self._log.error("{}".format(e)) + self._log.error(str(e)) return extension = guess_extension(response.headers["Content-Type"]) if extension is None: @@ -149,7 +149,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): with open(tempimg, "wb") as f: f.write(response.content) except Exception as e: - self._log.error("Unable to save image: {}".format(e)) + self._log.error(f"Unable to save image: {e}") return items = lib.items(decargs(args)) # Confirm with user. diff --git c/beetsplug/embyupdate.py i/beetsplug/embyupdate.py index 22c88947..8215012e 100644 --- c/beetsplug/embyupdate.py +++ i/beetsplug/embyupdate.py @@ -39,9 +39,7 @@ def api_url(host, port, endpoint): hostname_list.insert(0, "http://") hostname = "".join(hostname_list) - joined = urljoin( - "{hostname}:{port}".format(hostname=hostname, port=port), endpoint - ) + joined = urljoin(f"{hostname}:{port}", endpoint) scheme, netloc, path, query_string, fragment = urlsplit(joined) query_params = parse_qs(query_string) @@ -82,12 +80,12 @@ def create_headers(user_id, token=None): headers = {} authorization = ( - 'MediaBrowser UserId="{user_id}", ' + f'MediaBrowser UserId="{user_id}", ' 'Client="other", ' 'Device="beets", ' 'DeviceId="beets", ' 'Version="0.0.0"' - ).format(user_id=user_id) + ) headers["x-emby-authorization"] = authorization diff --git c/beetsplug/fetchart.py i/beetsplug/fetchart.py index 72aa3aa2..3071edaf 100644 --- c/beetsplug/fetchart.py +++ i/beetsplug/fetchart.py @@ -456,18 +456,14 @@ class CoverArtArchive(RemoteArtSource): try: response = self.request(url) except requests.RequestException: - self._log.debug( - "{}: error receiving response".format(self.NAME) - ) + self._log.debug(f"{self.NAME}: error receiving response") return try: data = response.json() except ValueError: self._log.debug( - "{}: error loading response: {}".format( - self.NAME, response.text - ) + f"{self.NAME}: error loading response: {response.text}" ) return @@ -601,9 +597,7 @@ class GoogleImages(RemoteArtSource): try: data = response.json() except ValueError: - self._log.debug( - "google: error loading response: {}".format(response.text) - ) + self._log.debug(f"google: error loading response: {response.text}") return if "error" in data: @@ -1071,9 +1065,7 @@ class LastFM(RemoteArtSource): url=images[size], size=self.SIZES[size] ) except ValueError: - self._log.debug( - "lastfm: error loading response: {}".format(response.text) - ) + self._log.debug(f"lastfm: error loading response: {response.text}") return @@ -1112,9 +1104,7 @@ class Spotify(RemoteArtSource): ] yield self._candidate(url=image_url, match=Candidate.MATCH_EXACT) except ValueError: - self._log.debug( - "Spotify: error loading response: {}".format(response.text) - ) + self._log.debug(f"Spotify: error loading response: {response.text}") return diff --git c/beetsplug/fish.py i/beetsplug/fish.py index 71ac8574..d29eac1a 100644 --- c/beetsplug/fish.py +++ i/beetsplug/fish.py @@ -127,18 +127,10 @@ class FishPlugin(BeetsPlugin): totstring += get_cmds_list([name[0] for name in cmd_names_help]) totstring += "" if nobasicfields else get_standard_fields(fields) totstring += get_extravalues(lib, extravalues) if extravalues else "" - totstring += ( - "\n" - + "# ====== {} =====".format("setup basic beet completion") - + "\n" * 2 - ) + totstring += "\n" "# ====== setup basic beet completion =====" "\n\n" totstring += get_basic_beet_options() totstring += ( - "\n" - + "# ====== {} =====".format( - "setup field completion for subcommands" - ) - + "\n" + "\n" "# ====== setup field completion for subcommands =====" "\n" ) totstring += get_subcommands(cmd_names_help, nobasicfields, extravalues) # Set up completion for all the command options @@ -227,11 +219,7 @@ def get_subcommands(cmd_name_and_help, nobasicfields, extravalues): for cmdname, cmdhelp in cmd_name_and_help: cmdname = _escape(cmdname) - word += ( - "\n" - + "# ------ {} -------".format("fieldsetups for " + cmdname) - + "\n" - ) + word += "\n" f"# ------ fieldsetups for {cmdname} -------" "\n" word += BL_NEED2.format( ("-a " + cmdname), ("-f " + "-d " + wrap(clean_whitespace(cmdhelp))) ) @@ -269,11 +257,7 @@ def get_all_commands(beetcmds): name = _escape(name) word += "\n" - word += ( - ("\n" * 2) - + "# ====== {} =====".format("completions for " + name) - + "\n" - ) + word += "\n\n" f"# ====== completions for {name} =====" "\n" for option in cmd.parser._get_all_options()[1:]: cmd_l = ( diff --git c/beetsplug/fromfilename.py i/beetsplug/fromfilename.py index 103e8290..4c643106 100644 --- c/beetsplug/fromfilename.py +++ i/beetsplug/fromfilename.py @@ -112,7 +112,7 @@ def apply_matches(d, log): for item in d: if not item.artist: item.artist = artist - log.info("Artist replaced with: {}".format(item.artist)) + log.info(f"Artist replaced with: {item.artist}") # No artist field: remaining field is the title. else: @@ -122,11 +122,11 @@ def apply_matches(d, log): for item in d: if bad_title(item.title): item.title = str(d[item][title_field]) - log.info("Title replaced with: {}".format(item.title)) + log.info(f"Title replaced with: {item.title}") if "track" in d[item] and item.track == 0: item.track = int(d[item]["track"]) - log.info("Track replaced with: {}".format(item.track)) + log.info(f"Track replaced with: {item.track}") # Plugin structure and hook into import process. diff --git c/beetsplug/info.py i/beetsplug/info.py index 1c3b6f54..d122fc3f 100644 --- c/beetsplug/info.py +++ i/beetsplug/info.py @@ -119,7 +119,6 @@ def print_data(data, item=None, fmt=None): return maxwidth = max(len(key) for key in formatted) - lineformat = f"{{0:>{maxwidth}}}: {{1}}" if path: ui.print_(displayable_path(path)) @@ -128,7 +127,7 @@ def print_data(data, item=None, fmt=None): value = formatted[field] if isinstance(value, list): value = "; ".join(value) - ui.print_(lineformat.format(field, value)) + ui.print_(f"{field:>{maxwidth}}: {value}") def print_data_keys(data, item=None): @@ -141,12 +140,11 @@ def print_data_keys(data, item=None): if len(formatted) == 0: return - line_format = "{0}{{0}}".format(" " * 4) if path: ui.print_(displayable_path(path)) for field in sorted(formatted): - ui.print_(line_format.format(field)) + ui.print_(f" {field}") class InfoPlugin(BeetsPlugin): diff --git c/beetsplug/inline.py i/beetsplug/inline.py index 4ca676e5..74416244 100644 --- c/beetsplug/inline.py +++ i/beetsplug/inline.py @@ -38,7 +38,8 @@ def _compile_func(body): """Given Python code for a function body, return a compiled callable that invokes that code. """ - body = "def {}():\n {}".format(FUNC_NAME, body.replace("\n", "\n ")) + body = body.replace("\n", "\n ") + body = f"def {FUNC_NAME}():\n {body}" code = compile(body, "inline", "exec") env = {} eval(code, env) diff --git c/beetsplug/lyrics.py i/beetsplug/lyrics.py index db29c9c6..19fe7f45 100644 --- c/beetsplug/lyrics.py +++ i/beetsplug/lyrics.py @@ -182,7 +182,7 @@ def search_pairs(item): # examples include (live), (remix), and (acoustic). r"(.+?)\s+[(].*[)]$", # Remove any featuring artists from the title - r"(.*?) {}".format(plugins.feat_tokens(for_artist=False)), + rf"(.*?) {plugins.feat_tokens(for_artist=False)}", # Remove part of title after colon ':' for songs with subtitles r"(.+?)\s*:.*", ] @@ -997,12 +997,10 @@ class LyricsPlugin(plugins.BeetsPlugin): tmpalbum = self.album = item.album.strip() if self.album == "": tmpalbum = "Unknown album" - self.rest += "{}\n{}\n\n".format(tmpalbum, "-" * len(tmpalbum)) + self.rest += f"{tmpalbum}\n{'-'*len(tmpalbum)}\n\n" title_str = ":index:`%s`" % item.title.strip() block = "| " + item.lyrics.replace("\n", "\n| ") - self.rest += "{}\n{}\n\n{}\n\n".format( - title_str, "~" * len(title_str), block - ) + self.rest += f"{title_str}\n{'~'*len(title_str)}\n\n{block}\n\n" def writerest(self, directory): """Write self.rest to a ReST file""" @@ -1132,5 +1130,5 @@ class LyricsPlugin(plugins.BeetsPlugin): translations = dict(zip(text_lines, lines_translated.split("|"))) result = "" for line in text.split("\n"): - result += "{} / {}\n".format(line, translations[line]) + result += f"{line} / {translations[line]}\n" return result diff --git c/beetsplug/mbcollection.py i/beetsplug/mbcollection.py index 1c010bf5..d0512cd6 100644 --- c/beetsplug/mbcollection.py +++ i/beetsplug/mbcollection.py @@ -79,9 +79,7 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): collection = self.config["collection"].as_str() if collection: if collection not in collection_ids: - raise ui.UserError( - "invalid collection ID: {}".format(collection) - ) + raise ui.UserError(f"invalid collection ID: {collection}") return collection # No specified collection. Just return the first collection ID diff --git c/beetsplug/metasync/__init__.py i/beetsplug/metasync/__init__.py index d17071b5..89e20812 100644 --- c/beetsplug/metasync/__init__.py +++ i/beetsplug/metasync/__init__.py @@ -120,14 +120,13 @@ class MetaSyncPlugin(BeetsPlugin): try: cls = META_SOURCES[player] except KeyError: - self._log.error("Unknown metadata source '{}'".format(player)) + self._log.error(f"Unknown metadata source '{player}'") try: meta_source_instances[player] = cls(self.config, self._log) except (ImportError, ConfigValueError) as e: self._log.error( - "Failed to instantiate metadata source " - "'{}': {}".format(player, e) + "Failed to instantiate metadata source " f"'{player}': {e}" ) # Avoid needlessly iterating over items diff --git c/beetsplug/missing.py i/beetsplug/missing.py index 2e37fde7..4f958ef9 100644 --- c/beetsplug/missing.py +++ i/beetsplug/missing.py @@ -222,7 +222,7 @@ class MissingPlugin(BeetsPlugin): missing_titles = {rg["title"] for rg in missing} for release_title in missing_titles: - print_("{} - {}".format(artist[0], release_title)) + print_(f"{artist[0]} - {release_title}") if total: print(total_missing) diff --git c/beetsplug/play.py i/beetsplug/play.py index 3476e582..9169bcd2 100644 --- c/beetsplug/play.py +++ i/beetsplug/play.py @@ -44,7 +44,7 @@ def play( """ # Print number of tracks or albums to be played, log command to be run. item_type += "s" if len(selection) > 1 else "" - ui.print_("Playing {} {}.".format(len(selection), item_type)) + ui.print_(f"Playing {len(selection)} {item_type}.") log.debug("executing command: {} {!r}", command_str, open_args) try: @@ -180,9 +180,7 @@ class PlayPlugin(BeetsPlugin): ui.print_( ui.colorize( "text_warning", - "You are about to queue {} {}.".format( - len(selection), item_type - ), + f"You are about to queue {len(selection)} {item_type}.", ) ) diff --git c/beetsplug/playlist.py i/beetsplug/playlist.py index 83f95796..9a83aa4c 100644 --- c/beetsplug/playlist.py +++ i/beetsplug/playlist.py @@ -192,9 +192,7 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin): if changes or deletions: self._log.info( - "Updated playlist {} ({} changes, {} deletions)".format( - filename, changes, deletions - ) + f"Updated playlist {filename} ({changes} changes, {deletions} deletions)" ) beets.util.copy(new_playlist, filename, replace=True) beets.util.remove(new_playlist) diff --git c/beetsplug/plexupdate.py i/beetsplug/plexupdate.py index 9b4419c7..c0ea0f4e 100644 --- c/beetsplug/plexupdate.py +++ i/beetsplug/plexupdate.py @@ -22,9 +22,7 @@ def get_music_section( ): """Getting the section key for the music library in Plex.""" api_endpoint = append_token("library/sections", token) - url = urljoin( - "{}://{}:{}".format(get_protocol(secure), host, port), api_endpoint - ) + url = urljoin(f"{get_protocol(secure)}://{host}:{port}", api_endpoint) # Sends request. r = requests.get( @@ -54,9 +52,7 @@ def update_plex(host, port, token, library_name, secure, ignore_cert_errors): ) api_endpoint = f"library/sections/{section_key}/refresh" api_endpoint = append_token(api_endpoint, token) - url = urljoin( - "{}://{}:{}".format(get_protocol(secure), host, port), api_endpoint - ) + url = urljoin(f"{get_protocol(secure)}://{host}:{port}", api_endpoint) # Sends request and returns requests object. r = requests.get( diff --git c/beetsplug/replaygain.py i/beetsplug/replaygain.py index a2753f96..78cce1e6 100644 --- c/beetsplug/replaygain.py +++ i/beetsplug/replaygain.py @@ -77,9 +77,7 @@ def call(args: List[Any], log: Logger, **kwargs: Any): return command_output(args, **kwargs) except subprocess.CalledProcessError as e: log.debug(e.output.decode("utf8", "ignore")) - raise ReplayGainError( - "{} exited with status {}".format(args[0], e.returncode) - ) + raise ReplayGainError(f"{args[0]} exited with status {e.returncode}") except UnicodeEncodeError: # Due to a bug in Python 2's subprocess on Windows, Unicode # filenames can fail to encode on that platform. See: @@ -182,9 +180,7 @@ class RgTask: # `track_gains` without throwing FatalReplayGainError # => raise non-fatal exception & continue raise ReplayGainError( - "ReplayGain backend `{}` failed for track {}".format( - self.backend_name, item - ) + f"ReplayGain backend `{self.backend_name}` failed for track {item}" ) self._store_track_gain(item, self.track_gains[0]) @@ -203,10 +199,8 @@ class RgTask: # `album_gain` without throwing FatalReplayGainError # => raise non-fatal exception & continue raise ReplayGainError( - "ReplayGain backend `{}` failed " - "for some tracks in album {}".format( - self.backend_name, self.album - ) + f"ReplayGain backend `{self.backend_name}` failed " + f"for some tracks in album {self.album}" ) for item, track_gain in zip(self.items, self.track_gains): self._store_track_gain(item, track_gain) @@ -517,12 +511,10 @@ class FfmpegBackend(Backend): if self._parse_float(b"M: " + line[1]) >= gating_threshold: n_blocks += 1 self._log.debug( - "{}: {} blocks over {} LUFS".format( - item, n_blocks, gating_threshold - ) + f"{item}: {n_blocks} blocks over {gating_threshold} LUFS" ) - self._log.debug("{}: gain {} LU, peak {}".format(item, gain, peak)) + self._log.debug(f"{item}: gain {gain} LU, peak {peak}") return Gain(gain, peak), n_blocks @@ -542,9 +534,7 @@ class FfmpegBackend(Backend): if output[i].startswith(search): return i raise ReplayGainError( - "ffmpeg output: missing {} after line {}".format( - repr(search), start_line - ) + f"ffmpeg output: missing {search!r} after line {start_line}" ) def _parse_float(self, line: bytes) -> float: @@ -591,7 +581,7 @@ class CommandBackend(Backend): # Explicit executable path. if not os.path.isfile(self.command): raise FatalReplayGainError( - "replaygain command does not exist: {}".format(self.command) + f"replaygain command does not exist: {self.command}" ) else: # Check whether the program is in $PATH. @@ -1241,10 +1231,8 @@ class ReplayGainPlugin(BeetsPlugin): if self.backend_name not in BACKENDS: raise ui.UserError( - "Selected ReplayGain backend {} is not supported. " - "Please select one of: {}".format( - self.backend_name, ", ".join(BACKENDS.keys()) - ) + f"Selected ReplayGain backend {self.backend_name} is not supported. " + f"Please select one of: {', '.join(BACKENDS.keys())}" ) # FIXME: Consider renaming the configuration option to 'peak_method' @@ -1252,10 +1240,8 @@ class ReplayGainPlugin(BeetsPlugin): peak_method = self.config["peak"].as_str() if peak_method not in PeakMethod.__members__: raise ui.UserError( - "Selected ReplayGain peak method {} is not supported. " - "Please select one of: {}".format( - peak_method, ", ".join(PeakMethod.__members__) - ) + f"Selected ReplayGain peak method {peak_method} is not supported. " + f"Please select one of: {', '.join(PeakMethod.__members__)}" ) # This only applies to plain old rg tags, r128 doesn't store peak # values. @@ -1543,18 +1529,14 @@ class ReplayGainPlugin(BeetsPlugin): if opts.album: albums = lib.albums(ui.decargs(args)) self._log.info( - "Analyzing {} albums ~ {} backend...".format( - len(albums), self.backend_name - ) + f"Analyzing {len(albums)} albums ~ {self.backend_name} backend..." ) for album in albums: self.handle_album(album, write, force) else: items = lib.items(ui.decargs(args)) self._log.info( - "Analyzing {} tracks ~ {} backend...".format( - len(items), self.backend_name - ) + f"Analyzing {len(items)} tracks ~ {self.backend_name} backend..." ) for item in items: self.handle_track(item, write, force) diff --git c/beetsplug/smartplaylist.py i/beetsplug/smartplaylist.py index 9df2cca6..35419f98 100644 --- c/beetsplug/smartplaylist.py +++ i/beetsplug/smartplaylist.py @@ -313,10 +313,11 @@ class SmartPlaylistPlugin(BeetsPlugin): ) mkdirall(m3u_path) pl_format = self.config["output"].get() - if pl_format != "m3u" and pl_format != "extm3u": - msg = "Unsupported output format '{}' provided! " - msg += "Supported: m3u, extm3u" - raise Exception(msg.format(pl_format)) + if pl_format not in ["m3u", "extm3u"]: + raise Exception( + f"Unsupported output format '{pl_format}' provided! " + "Supported: m3u, extm3u" + ) extm3u = pl_format == "extm3u" with open(syspath(m3u_path), "wb") as f: keys = [] @@ -332,9 +333,7 @@ class SmartPlaylistPlugin(BeetsPlugin): f" {a[0]}={json.dumps(str(a[1]))}" for a in attr ] attrs = "".join(al) - comment = "#EXTINF:{}{},{} - {}\n".format( - int(item.length), attrs, item.artist, item.title - ) + comment = f"#EXTINF:{int(item.length)}{attrs},{item.artist} - {item.title}\n" f.write(comment.encode("utf-8") + entry.uri + b"\n") # Send an event when playlists were updated. send_event("smartplaylist_update") diff --git c/beetsplug/spotify.py i/beetsplug/spotify.py index 55a77a8a..ec30363a 100644 --- c/beetsplug/spotify.py +++ i/beetsplug/spotify.py @@ -146,7 +146,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): response.raise_for_status() except requests.exceptions.HTTPError as e: raise ui.UserError( - "Spotify authorization failed: {}\n{}".format(e, response.text) + f"Spotify authorization failed: {e}\n{response.text}" ) self.access_token = response.json()["access_token"] @@ -271,9 +271,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): else: raise ui.UserError( "Invalid `release_date_precision` returned " - "by {} API: '{}'".format( - self.data_source, release_date_precision - ) + f"by {self.data_source} API: '{release_date_precision}'" ) tracks_data = album_data["tracks"] @@ -457,17 +455,15 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): "-m", "--mode", action="store", - help='"open" to open {} with playlist, ' - '"list" to print (default)'.format(self.data_source), + help=f'"open" to open {self.data_source} with ' + 'playlist, "list" to print (default)', ) spotify_cmd.parser.add_option( "-f", "--show-failures", action="store_true", dest="show_failures", - help="list tracks that did not match a {} ID".format( - self.data_source - ), + help=f"list tracks that did not match a {self.data_source} ID", ) spotify_cmd.func = queries @@ -628,9 +624,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): spotify_ids = [track_data["id"] for track_data in results] if self.config["mode"].get() == "open": self._log.info( - "Attempting to open {} with playlist".format( - self.data_source - ) + f"Attempting to open {self.data_source} with playlist" ) spotify_url = "spotify:trackset:Playlist:" + ",".join( spotify_ids diff --git c/beetsplug/thumbnails.py i/beetsplug/thumbnails.py index 19c19f06..acca413d 100644 --- c/beetsplug/thumbnails.py +++ i/beetsplug/thumbnails.py @@ -204,7 +204,7 @@ class ThumbnailsPlugin(BeetsPlugin): artfile = os.path.split(album.artpath)[1] with open(syspath(outfilename), "w") as f: f.write("[Desktop Entry]\n") - f.write("Icon=./{}".format(artfile.decode("utf-8"))) + f.write(f"Icon=./{artfile.decode('utf-8')}") f.close() self._log.debug("Wrote file {0}", displayable_path(outfilename)) diff --git c/beetsplug/types.py i/beetsplug/types.py index 9ba3aac6..451d9e98 100644 --- c/beetsplug/types.py +++ i/beetsplug/types.py @@ -45,6 +45,6 @@ class TypesPlugin(BeetsPlugin): mytypes[key] = library.DateType() else: raise ConfigValueError( - "unknown type '{}' for the '{}' field".format(value, key) + f"unknown type '{value}' for the '{key}' field" ) return mytypes diff --git c/test/plugins/test_art.py i/test/plugins/test_art.py index 20bbcdce..a5244c20 100644 --- c/test/plugins/test_art.py +++ i/test/plugins/test_art.py @@ -64,11 +64,11 @@ class FetchImageTestCase(FetchImageHelper, UseThePlugin): class CAAHelper: """Helper mixin for mocking requests to the Cover Art Archive.""" - MBID_RELASE = "rid" + MBID_RELEASE = "rid" MBID_GROUP = "rgid" - RELEASE_URL = "coverartarchive.org/release/{}".format(MBID_RELASE) - GROUP_URL = "coverartarchive.org/release-group/{}".format(MBID_GROUP) + RELEASE_URL = f"coverartarchive.org/release/{MBID_RELEASE}" + GROUP_URL = f"coverartarchive.org/release-group/{MBID_GROUP}" RELEASE_URL = "https://" + RELEASE_URL GROUP_URL = "https://" + GROUP_URL @@ -281,10 +281,8 @@ class FSArtTest(UseThePlugin): class CombinedTest(FetchImageTestCase, CAAHelper): ASIN = "xxxx" MBID = "releaseid" - AMAZON_URL = "https://images.amazon.com/images/P/{}.01.LZZZZZZZ.jpg".format( - ASIN - ) - AAO_URL = "https://www.albumart.org/index_detail.php?asin={}".format(ASIN) + AMAZON_URL = f"https://images.amazon.com/images/P/{ASIN}.01.LZZZZZZZ.jpg" + AAO_URL = f"https://www.albumart.org/index_detail.php?asin={ASIN}" def setUp(self): super().setUp() @@ -342,7 +340,7 @@ class CombinedTest(FetchImageTestCase, CAAHelper): content_type="image/jpeg", ) album = _common.Bag( - mb_albumid=self.MBID_RELASE, + mb_albumid=self.MBID_RELEASE, mb_releasegroupid=self.MBID_GROUP, asin=self.ASIN, ) @@ -562,7 +560,7 @@ class CoverArtArchiveTest(UseThePlugin, CAAHelper): def test_caa_finds_image(self): album = _common.Bag( - mb_albumid=self.MBID_RELASE, mb_releasegroupid=self.MBID_GROUP + mb_albumid=self.MBID_RELEASE, mb_releasegroupid=self.MBID_GROUP ) self.mock_caa_response(self.RELEASE_URL, self.RESPONSE_RELEASE) self.mock_caa_response(self.GROUP_URL, self.RESPONSE_GROUP) @@ -578,7 +576,7 @@ class CoverArtArchiveTest(UseThePlugin, CAAHelper): self.settings = Settings(maxwidth=maxwidth) album = _common.Bag( - mb_albumid=self.MBID_RELASE, mb_releasegroupid=self.MBID_GROUP + mb_albumid=self.MBID_RELEASE, mb_releasegroupid=self.MBID_GROUP ) self.mock_caa_response(self.RELEASE_URL, self.RESPONSE_RELEA…
See: <beetbox#5337 (comment)> diff --git c/beets/autotag/mb.py i/beets/autotag/mb.py index e16643d7..f152b567 100644 --- c/beets/autotag/mb.py +++ i/beets/autotag/mb.py @@ -66,7 +66,9 @@ class MusicBrainzAPIError(util.HumanReadableException): super().__init__(reason, verb, tb) def get_message(self): - return f"{self._reasonstr()} in {self.verb} with query {self.query!r}" + return ( + f"{self._reasonstr()} in {self.verb} with query {repr(self.query)}" + ) log = logging.getLogger("beets") diff --git c/beets/dbcore/db.py i/beets/dbcore/db.py index 55ba6f11..5aa75aa5 100755 --- c/beets/dbcore/db.py +++ i/beets/dbcore/db.py @@ -397,7 +397,7 @@ class Model(ABC): def __repr__(self) -> str: name = type(self).__name__ - fields = ", ".join(f"{k}={v!r}" for k, v in dict(self).items()) + fields = ", ".join(f"{k}={repr(v)}" for k, v in dict(self).items()) return f"{name}({fields})" def clear_dirty(self): @@ -558,12 +558,12 @@ class Model(ABC): def __getattr__(self, key): if key.startswith("_"): - raise AttributeError(f"model has no attribute {key!r}") + raise AttributeError(f"model has no attribute {repr(key)}") else: try: return self[key] except KeyError: - raise AttributeError(f"no such field {key!r}") + raise AttributeError(f"no such field {repr(key)}") def __setattr__(self, key, value): if key.startswith("_"): diff --git c/beets/dbcore/query.py i/beets/dbcore/query.py index 357b5685..6e94ddd5 100644 --- c/beets/dbcore/query.py +++ i/beets/dbcore/query.py @@ -171,7 +171,7 @@ class FieldQuery(Query, Generic[P]): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.field_name!r}, {self.pattern!r}, " + f"{self.__class__.__name__}({repr(self.field_name)}, {repr(self.pattern)}, " f"fast={self.fast})" ) @@ -210,7 +210,9 @@ class NoneQuery(FieldQuery[None]): return obj.get(self.field_name) is None def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.field_name!r}, {self.fast})" + return ( + f"{self.__class__.__name__}({repr(self.field_name)}, {self.fast})" + ) class StringFieldQuery(FieldQuery[P]): @@ -503,7 +505,7 @@ class CollectionQuery(Query): return clause, subvals def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.subqueries!r})" + return f"{self.__class__.__name__}({repr(self.subqueries)})" def __eq__(self, other) -> bool: return super().__eq__(other) and self.subqueries == other.subqueries @@ -548,7 +550,7 @@ class AnyFieldQuery(CollectionQuery): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.pattern!r}, {self.fields!r}, " + f"{self.__class__.__name__}({repr(self.pattern)}, {repr(self.fields)}, " f"{self.query_class.__name__})" ) @@ -619,7 +621,7 @@ class NotQuery(Query): return not self.subquery.match(obj) def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.subquery!r})" + return f"{self.__class__.__name__}({repr(self.subquery)})" def __eq__(self, other) -> bool: return super().__eq__(other) and self.subquery == other.subquery @@ -975,7 +977,7 @@ class MultipleSort(Sort): return items def __repr__(self): - return f"{self.__class__.__name__}({self.sorts!r})" + return f"{self.__class__.__name__}({repr(self.sorts)})" def __hash__(self): return hash(tuple(self.sorts)) @@ -1015,7 +1017,7 @@ class FieldSort(Sort): def __repr__(self) -> str: return ( f"{self.__class__.__name__}" - f"({self.field!r}, ascending={self.ascending!r})" + f"({repr(self.field)}, ascending={repr(self.ascending)})" ) def __hash__(self) -> int: diff --git c/beets/library.py i/beets/library.py index 77d24ecd..a9adc13d 100644 --- c/beets/library.py +++ i/beets/library.py @@ -156,7 +156,7 @@ class PathQuery(dbcore.FieldQuery[bytes]): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.field!r}, {self.pattern!r}, " + f"{self.__class__.__name__}({repr(self.field)}, {repr(self.pattern)}, " f"fast={self.fast}, case_sensitive={self.case_sensitive})" ) @@ -735,7 +735,7 @@ class Item(LibModel): # can even deadlock due to the database lock. name = type(self).__name__ keys = self.keys(with_album=False) - fields = (f"{k}={self[k]!r}" for k in keys) + fields = (f"{k}={repr(self[k])}" for k in keys) return f"{name}({', '.join(fields)})" def keys(self, computed=False, with_album=True): @@ -1578,7 +1578,7 @@ def parse_query_string(s, model_cls): The string is split into components using shell-like syntax. """ - message = f"Query is not unicode: {s!r}" + message = f"Query is not unicode: {repr(s)}" assert isinstance(s, str), message try: parts = shlex.split(s) diff --git c/beets/test/_common.py i/beets/test/_common.py index c12838e2..0bc1baf8 100644 --- c/beets/test/_common.py +++ i/beets/test/_common.py @@ -152,7 +152,7 @@ class Assertions: """A mixin with additional unit test assertions.""" def assertExists(self, path): # noqa - assert os.path.exists(syspath(path)), f"file does not exist: {path!r}" + assert os.path.exists(syspath(path)), f"file does not exist: {repr(path)}" def assertNotExists(self, path): # noqa assert not os.path.exists(syspath(path)), f"file exists: {repr(path)}" @@ -186,7 +186,7 @@ class InputException(Exception): def __str__(self): msg = "Attempt to read with no input provided." if self.output is not None: - msg += f" Output: {self.output!r}" + msg += f" Output: {repr(self.output)}" return msg diff --git c/beets/ui/commands.py i/beets/ui/commands.py index 3042ca77..a717c94c 100755 --- c/beets/ui/commands.py +++ i/beets/ui/commands.py @@ -213,7 +213,7 @@ def get_singleton_disambig_fields(info: hooks.TrackInfo) -> Sequence[str]: out = [] chosen_fields = config["match"]["singleton_disambig_fields"].as_str_seq() calculated_values = { - "index": f"Index {info.index!s}", + "index": f"Index {str(info.index)}", "track_alt": f"Track {info.track_alt}", "album": ( f"[{info.album}]" diff --git c/beets/util/__init__.py i/beets/util/__init__.py index aa94b6d2..a0f13fa1 100644 --- c/beets/util/__init__.py +++ i/beets/util/__init__.py @@ -104,7 +104,7 @@ class HumanReadableException(Exception): elif hasattr(self.reason, "strerror"): # i.e., EnvironmentError return self.reason.strerror else: - return f'"{self.reason!s}"' + return f'"{str(self.reason)}"' def get_message(self): """Create the human-readable description of the error, sans diff --git c/beets/util/functemplate.py i/beets/util/functemplate.py index 35f60b7d..f149d370 100644 --- c/beets/util/functemplate.py +++ i/beets/util/functemplate.py @@ -166,7 +166,7 @@ class Call: self.original = original def __repr__(self): - return f"Call({self.ident!r}, {self.args!r}, {self.original!r})" + return f"Call({repr(self.ident)}, {repr(self.args)}, {repr(self.original)})" def evaluate(self, env): """Evaluate the function call in the environment, returning a diff --git c/beetsplug/bpd/__init__.py i/beetsplug/bpd/__init__.py index d6c75380..3336702c 100644 --- c/beetsplug/bpd/__init__.py +++ i/beetsplug/bpd/__init__.py @@ -1142,7 +1142,7 @@ class Server(BaseServer): pass for tagtype, field in self.tagtype_map.items(): - info_lines.append(f"{tagtype}: {getattr(item, field)!s}") + info_lines.append(f"{tagtype}: {str(getattr(item, field))}") return info_lines @@ -1301,7 +1301,7 @@ class Server(BaseServer): yield ( f"bitrate: {item.bitrate / 1000}", - f"audio: {item.samplerate!s}:{item.bitdepth!s}:{item.channels!s}", + f"audio: {str(item.samplerate)}:{str(item.bitdepth)}:{str(item.channels)}", ) (pos, total) = self.player.time() diff --git c/beetsplug/edit.py i/beetsplug/edit.py index 61f2020a..20430255 100644 --- c/beetsplug/edit.py +++ i/beetsplug/edit.py @@ -47,7 +47,9 @@ def edit(filename, log): try: subprocess.call(cmd) except OSError as exc: - raise ui.UserError(f"could not run editor command {cmd[0]!r}: {exc}") + raise ui.UserError( + f"could not run editor command {repr(cmd[0])}: {exc}" + ) def dump(arg): diff --git c/beetsplug/replaygain.py i/beetsplug/replaygain.py index 78cce1e6..1c8aaaa9 100644 --- c/beetsplug/replaygain.py +++ i/beetsplug/replaygain.py @@ -534,7 +534,7 @@ class FfmpegBackend(Backend): if output[i].startswith(search): return i raise ReplayGainError( - f"ffmpeg output: missing {search!r} after line {start_line}" + f"ffmpeg output: missing {repr(search)} after line {start_line}" ) def _parse_float(self, line: bytes) -> float: @@ -547,7 +547,7 @@ class FfmpegBackend(Backend): parts = line.split(b":", 1) if len(parts) < 2: raise ReplayGainError( - f"ffmpeg output: expected key value pair, found {line!r}" + f"ffmpeg output: expected key value pair, found {repr(line)}" ) value = parts[1].lstrip() # strip unit @@ -557,7 +557,7 @@ class FfmpegBackend(Backend): return float(value) except ValueError: raise ReplayGainError( - f"ffmpeg output: expected float value, found {value!r}" + f"ffmpeg output: expected float value, found {repr(value)}" ) @@ -886,7 +886,7 @@ class GStreamerBackend(Backend): f = self._src.get_property("location") # A GStreamer error, either an unsupported format or a bug. self._error = ReplayGainError( - f"Error {err!r} - {debug!r} on file {f!r}" + f"Error {repr(err)} - {repr(debug)} on file {repr(f)}" ) def _on_tag(self, bus, message): diff --git c/beetsplug/thumbnails.py i/beetsplug/thumbnails.py index acca413d..0cde56c7 100644 --- c/beetsplug/thumbnails.py +++ i/beetsplug/thumbnails.py @@ -292,4 +292,6 @@ class GioURI(URIGetter): try: return uri.decode(util._fsencoding()) except UnicodeDecodeError: - raise RuntimeError(f"Could not decode filename from GIO: {uri!r}") + raise RuntimeError( + f"Could not decode filename from GIO: {repr(uri)}" + ) diff --git c/test/plugins/test_lyrics.py i/test/plugins/test_lyrics.py index 7cb081fc..484d4889 100644 --- c/test/plugins/test_lyrics.py +++ i/test/plugins/test_lyrics.py @@ -223,9 +223,9 @@ class LyricsAssertions: if not keywords <= words: details = ( - f"{keywords!r} is not a subset of {words!r}." - f" Words only in expected set {keywords - words!r}," - f" Words only in result set {words - keywords!r}." + f"{repr(keywords)} is not a subset of {repr(words)}." + f" Words only in expected set {repr(keywords - words)}," + f" Words only in result set {repr(words - keywords)}." ) self.fail(f"{details} : {msg}") diff --git c/test/plugins/test_player.py i/test/plugins/test_player.py index bf466e1b..e23b6396 100644 --- c/test/plugins/test_player.py +++ i/test/plugins/test_player.py @@ -132,7 +132,7 @@ class MPCResponse: cmd, rest = rest[2:].split("}") return False, (int(code), int(pos), cmd, rest[1:]) else: - raise RuntimeError(f"Unexpected status: {status!r}") + raise RuntimeError(f"Unexpected status: {repr(status)}") def _parse_body(self, body): """Messages are generally in the format "header: content". @@ -145,7 +145,7 @@ class MPCResponse: if not line: continue if ":" not in line: - raise RuntimeError(f"Unexpected line: {line!r}") + raise RuntimeError(f"Unexpected line: {repr(line)}") header, content = line.split(":", 1) content = content.lstrip() if header in repeated_headers: @@ -191,7 +191,7 @@ class MPCClient: responses.append(MPCResponse(response)) response = b"" elif not line: - raise RuntimeError(f"Unexpected response: {line!r}") + raise RuntimeError(f"Unexpected response: {repr(line)}") def serialise_command(self, command, *args): cmd = [command.encode("utf-8")] diff --git c/test/plugins/test_thumbnails.py i/test/plugins/test_thumbnails.py index 07775995..1931061b 100644 --- c/test/plugins/test_thumbnails.py +++ i/test/plugins/test_thumbnails.py @@ -71,7 +71,7 @@ class ThumbnailsTest(BeetsTestCase): return False if path == syspath(LARGE_DIR): return True - raise ValueError(f"unexpected path {path!r}") + raise ValueError(f"unexpected path {repr(path)}") mock_os.path.exists = exists plugin = ThumbnailsPlugin() diff --git c/beets/autotag/mb.py i/beets/autotag/mb.py index 537123a77..1402c9420 100644 --- c/beets/autotag/mb.py +++ i/beets/autotag/mb.py @@ -66,7 +66,9 @@ class MusicBrainzAPIError(util.HumanReadableError): super().__init__(reason, verb, tb) def get_message(self): - return f"{self._reasonstr()} in {self.verb} with query {self.query!r}" + return ( + f"{self._reasonstr()} in {self.verb} with query {repr(self.query)}" + ) log = logging.getLogger("beets") diff --git c/beets/dbcore/db.py i/beets/dbcore/db.py index 55ba6f110..5aa75aa59 100755 --- c/beets/dbcore/db.py +++ i/beets/dbcore/db.py @@ -397,7 +397,7 @@ class Model(ABC): def __repr__(self) -> str: name = type(self).__name__ - fields = ", ".join(f"{k}={v!r}" for k, v in dict(self).items()) + fields = ", ".join(f"{k}={repr(v)}" for k, v in dict(self).items()) return f"{name}({fields})" def clear_dirty(self): @@ -558,12 +558,12 @@ class Model(ABC): def __getattr__(self, key): if key.startswith("_"): - raise AttributeError(f"model has no attribute {key!r}") + raise AttributeError(f"model has no attribute {repr(key)}") else: try: return self[key] except KeyError: - raise AttributeError(f"no such field {key!r}") + raise AttributeError(f"no such field {repr(key)}") def __setattr__(self, key, value): if key.startswith("_"): diff --git c/beets/dbcore/query.py i/beets/dbcore/query.py index 357b56857..6e94ddd51 100644 --- c/beets/dbcore/query.py +++ i/beets/dbcore/query.py @@ -171,7 +171,7 @@ class FieldQuery(Query, Generic[P]): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.field_name!r}, {self.pattern!r}, " + f"{self.__class__.__name__}({repr(self.field_name)}, {repr(self.pattern)}, " f"fast={self.fast})" ) @@ -210,7 +210,9 @@ class NoneQuery(FieldQuery[None]): return obj.get(self.field_name) is None def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.field_name!r}, {self.fast})" + return ( + f"{self.__class__.__name__}({repr(self.field_name)}, {self.fast})" + ) class StringFieldQuery(FieldQuery[P]): @@ -503,7 +505,7 @@ class CollectionQuery(Query): return clause, subvals def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.subqueries!r})" + return f"{self.__class__.__name__}({repr(self.subqueries)})" def __eq__(self, other) -> bool: return super().__eq__(other) and self.subqueries == other.subqueries @@ -548,7 +550,7 @@ class AnyFieldQuery(CollectionQuery): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.pattern!r}, {self.fields!r}, " + f"{self.__class__.__name__}({repr(self.pattern)}, {repr(self.fields)}, " f"{self.query_class.__name__})" ) @@ -619,7 +621,7 @@ class NotQuery(Query): return not self.subquery.match(obj) def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.subquery!r})" + return f"{self.__class__.__name__}({repr(self.subquery)})" def __eq__(self, other) -> bool: return super().__eq__(other) and self.subquery == other.subquery @@ -975,7 +977,7 @@ class MultipleSort(Sort): return items def __repr__(self): - return f"{self.__class__.__name__}({self.sorts!r})" + return f"{self.__class__.__name__}({repr(self.sorts)})" def __hash__(self): return hash(tuple(self.sorts)) @@ -1015,7 +1017,7 @@ class FieldSort(Sort): def __repr__(self) -> str: return ( f"{self.__class__.__name__}" - f"({self.field!r}, ascending={self.ascending!r})" + f"({repr(self.field)}, ascending={repr(self.ascending)})" ) def __hash__(self) -> int: diff --git c/beets/library.py i/beets/library.py index 9a9dedf38..89420cfe1 100644 --- c/beets/library.py +++ i/beets/library.py @@ -157,7 +157,7 @@ class PathQuery(dbcore.FieldQuery[bytes]): def __repr__(self) -> str: return ( - f"{self.__class__.__name__}({self.field!r}, {self.pattern!r}, " + f"{self.__class__.__name__}({repr(self.field)}, {repr(self.pattern)}, " f"fast={self.fast}, case_sensitive={self.case_sensitive})" ) @@ -736,7 +736,7 @@ class Item(LibModel): # can even deadlock due to the database lock. name = type(self).__name__ keys = self.keys(with_album=False) - fields = (f"{k}={self[k]!r}" for k in keys) + fields = (f"{k}={repr(self[k])}" for k in keys) return f"{name}({', '.join(fields)})" def keys(self, computed=False, with_album=True): @@ -1579,7 +1579,7 @@ def parse_query_string(s, model_cls): The string is split into components using shell-like syntax. """ - message = f"Query is not unicode: {s!r}" + message = f"Query is not unicode: {repr(s)}" assert isinstance(s, str), message try: parts = shlex.split(s) diff --git c/beets/test/_common.py i/beets/test/_common.py index abb2e6ae9..2fda9760d 100644 --- c/beets/test/_common.py +++ i/beets/test/_common.py @@ -178,7 +178,7 @@ class InputError(Exception): def __str__(self): msg = "Attempt to read with no input provided." if self.output is not None: - msg += f" Output: {self.output!r}" + msg += f" Output: {repr(self.output)}" return msg diff --git c/beets/ui/commands.py i/beets/ui/commands.py index 4988027b9..78267a329 100755 --- c/beets/ui/commands.py +++ i/beets/ui/commands.py @@ -212,7 +212,7 @@ def get_singleton_disambig_fields(info: hooks.TrackInfo) -> Sequence[str]: out = [] chosen_fields = config["match"]["singleton_disambig_fields"].as_str_seq() calculated_values = { - "index": f"Index {info.index!s}", + "index": f"Index {str(info.index)}", "track_alt": f"Track {info.track_alt}", "album": ( f"[{info.album}]" diff --git c/beets/util/__init__.py i/beets/util/__init__.py index 437bb57d1..251f1eaee 100644 --- c/beets/util/__init__.py +++ i/beets/util/__init__.py @@ -106,7 +106,7 @@ class HumanReadableError(Exception): elif hasattr(self.reason, "strerror"): # i.e., EnvironmentError return self.reason.strerror else: - return f'"{self.reason!s}"' + return f'"{str(self.reason)}"' def get_message(self): """Create the human-readable description of the error, sans diff --git c/beets/util/functemplate.py i/beets/util/functemplate.py index 768371b07..cae646ab0 100644 --- c/beets/util/functemplate.py +++ i/beets/util/functemplate.py @@ -165,7 +165,7 @@ class Call: self.original = original def __repr__(self): - return f"Call({self.ident!r}, {self.args!r}, {self.original!r})" + return f"Call({repr(self.ident)}, {repr(self.args)}, {repr(self.original)})" def evaluate(self, env): """Evaluate the function call in the environment, returning a diff --git c/beetsplug/bpd/__init__.py i/beetsplug/bpd/__init__.py index f9fdab7b7..10e1b0828 100644 --- c/beetsplug/bpd/__init__.py +++ i/beetsplug/bpd/__init__.py @@ -1141,7 +1141,7 @@ class Server(BaseServer): pass for tagtype, field in self.tagtype_map.items(): - info_lines.append(f"{tagtype}: {getattr(item, field)!s}") + info_lines.append(f"{tagtype}: {str(getattr(item, field))}") return info_lines @@ -1300,7 +1300,7 @@ class Server(BaseServer): yield ( f"bitrate: {item.bitrate / 1000}", - f"audio: {item.samplerate!s}:{item.bitdepth!s}:{item.channels!s}", + f"audio: {str(item.samplerate)}:{str(item.bitdepth)}:{str(item.channels)}", ) (pos, total) = self.player.time() diff --git c/beetsplug/edit.py i/beetsplug/edit.py index d53b5942f..28821e97c 100644 --- c/beetsplug/edit.py +++ i/beetsplug/edit.py @@ -46,7 +46,9 @@ def edit(filename, log): try: subprocess.call(cmd) except OSError as exc: - raise ui.UserError(f"could not run editor command {cmd[0]!r}: {exc}") + raise ui.UserError( + f"could not run editor command {repr(cmd[0])}: {exc}" + ) def dump(arg): diff --git c/beetsplug/replaygain.py i/beetsplug/replaygain.py index dac1018bb..54441341e 100644 --- c/beetsplug/replaygain.py +++ i/beetsplug/replaygain.py @@ -532,7 +532,7 @@ class FfmpegBackend(Backend): if output[i].startswith(search): return i raise ReplayGainError( - f"ffmpeg output: missing {search!r} after line {start_line}" + f"ffmpeg output: missing {repr(search)} after line {start_line}" ) def _parse_float(self, line: bytes) -> float: @@ -545,7 +545,7 @@ class FfmpegBackend(Backend): parts = line.split(b":", 1) if len(parts) < 2: raise ReplayGainError( - f"ffmpeg output: expected key value pair, found {line!r}" + f"ffmpeg output: expected key value pair, found {repr(line)}" ) value = parts[1].lstrip() # strip unit @@ -555,7 +555,7 @@ class FfmpegBackend(Backend): return float(value) except ValueError: raise ReplayGainError( - f"ffmpeg output: expected float value, found {value!r}" + f"ffmpeg output: expected float value, found {repr(value)}" ) @@ -884,7 +884,7 @@ class GStreamerBackend(Backend): f = self._src.get_property("location") # A GStreamer error, either an unsupported format or a bug. self._error = ReplayGainError( - f"Error {err!r} - {debug!r} on file {f!r}" + f"Error {repr(err)} - {repr(debug)} on file {repr(f)}" ) def _on_tag(self, bus, message): diff --git c/beetsplug/thumbnails.py i/beetsplug/thumbnails.py index bd377d7f9..873f32445 100644 --- c/beetsplug/thumbnails.py +++ i/beetsplug/thumbnails.py @@ -290,4 +290,6 @@ class GioURI(URIGetter): try: return uri.decode(util._fsencoding()) except UnicodeDecodeError: - raise RuntimeError(f"Could not decode filename from GIO: {uri!r}") + raise RuntimeError( + f"Could not decode filename from GIO: {repr(uri)}" + ) diff --git c/test/plugins/test_lyrics.py i/test/plugins/test_lyrics.py index 937e0a3cb..104b847c2 100644 --- c/test/plugins/test_lyrics.py +++ i/test/plugins/test_lyrics.py @@ -223,9 +223,9 @@ class LyricsAssertions: if not keywords <= words: details = ( - f"{keywords!r} is not a subset of {words!r}." - f" Words only in expected set {keywords - words!r}," - f" Words only in result set {words - keywords!r}." + f"{repr(keywords)} is not a subset of {repr(words)}." + f" Words only in expected set {repr(keywords - words)}," + f" Words only in result set {repr(words - keywords)}." ) self.fail(f"{details} : {msg}") diff --git c/test/plugins/test_player.py i/test/plugins/test_player.py index b17a78c17..4e59cda06 100644 --- c/test/plugins/test_player.py +++ i/test/plugins/test_player.py @@ -132,7 +132,7 @@ class MPCResponse: cmd, rest = rest[2:].split("}") return False, (int(code), int(pos), cmd, rest[1:]) else: - raise RuntimeError(f"Unexpected status: {status!r}") + raise RuntimeError(f"Unexpected status: {repr(status)}") def _parse_body(self, body): """Messages are generally in the format "header: content". @@ -145,7 +145,7 @@ class MPCResponse: if not line: continue if ":" not in line: - raise RuntimeError(f"Unexpected line: {line!r}") + raise RuntimeError(f"Unexpected line: {repr(line)}") header, content = line.split(":", 1) content = content.lstrip() if header in repeated_headers: @@ -191,7 +191,7 @@ class MPCClient: responses.append(MPCResponse(response)) response = b"" elif not line: - raise RuntimeError(f"Unexpected response: {line!r}") + raise RuntimeError(f"Unexpected response: {repr(line)}") def serialise_command(self, command, *args): cmd = [command.encode("utf-8")] diff --git c/test/plugins/test_thumbnails.py i/test/plugins/test_thumbnails.py index 3eb36cd25..c9e1c7743 100644 --- c/test/plugins/test_thumbnails.py +++ i/test/plugins/test_thumbnails.py @@ -71,7 +71,7 @@ class ThumbnailsTest(BeetsTestCase): return False if path == syspath(LARGE_DIR): return True - raise ValueError(f"unexpected path {path!r}") + raise ValueError(f"unexpected path {repr(path)}") mock_os.path.exists = exists plugin = ThumbnailsPlugin()
The logic is a bit easier to follow now. See: <beetbox#5337 (comment)>
The new version doesn't rely on regular expressions, provides more intuitive names, and will probably be easier to maintain. See: <beetbox#5337 (comment)>
In cases where the values being filled in did not intuitively describe what they represented as URL components, it became difficult to figure out the structure of the URL. See: <beetbox#5337 (comment)>
Fixes #5293.
Along the way, I've made some small code improvements beyond just the use of f-strings; I don't think these will conflict with others' work. In particular, I've avoided modifying the formatting of paths; there is work to greatly simplify that using 'pathlib', at which point a second commit can clean them up.