Skip to content
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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Commits on Sep 22, 2024

  1. Replace most 'str.format()' uses with f-strings

    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…
    Arav K. committed Sep 22, 2024
    Configuration menu
    Copy the full SHA
    3ab272d View commit details
    Browse the repository at this point in the history
  2. Fix minor linting issues

    Arav K. committed Sep 22, 2024
    Configuration menu
    Copy the full SHA
    5496060 View commit details
    Browse the repository at this point in the history
  3. Use 'repr' and 'str' in place of '!r' and '!s'

    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()
    Arav K. committed Sep 22, 2024
    Configuration menu
    Copy the full SHA
    c3682e7 View commit details
    Browse the repository at this point in the history
  4. [beets.ui] Simplify some formatting logic

    Arav K. committed Sep 22, 2024
    Configuration menu
    Copy the full SHA
    9575fc9 View commit details
    Browse the repository at this point in the history
  5. [beets.dbcore.db] Rewrite 'Model.store()'

    The logic is a bit easier to follow now.
    
    See: <beetbox#5337 (comment)>
    Arav K. committed Sep 22, 2024
    Configuration menu
    Copy the full SHA
    3a61d49 View commit details
    Browse the repository at this point in the history
  6. Fix lint warnings

    Arav K. committed Sep 22, 2024
    Configuration menu
    Copy the full SHA
    6290b0b View commit details
    Browse the repository at this point in the history
  7. Configuration menu
    Copy the full SHA
    79a3495 View commit details
    Browse the repository at this point in the history
  8. [beets/autotag/hooks] Rework article switching in string distance

    The new version doesn't rely on regular expressions, provides more
    intuitive names, and will probably be easier to maintain.
    
    See: <beetbox#5337 (comment)>
    Arav K. committed Sep 22, 2024
    Configuration menu
    Copy the full SHA
    4acbf63 View commit details
    Browse the repository at this point in the history
  9. Revert to 'str.format' for most URLs

    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)>
    Arav K. committed Sep 22, 2024
    Configuration menu
    Copy the full SHA
    7dc6a97 View commit details
    Browse the repository at this point in the history
  10. Use lazy logging instead of f-strings

    arya dradjica committed Sep 22, 2024
    Configuration menu
    Copy the full SHA
    fcf9a78 View commit details
    Browse the repository at this point in the history