diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index c6ec4cb5f1..b20434e23c 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -57,3 +57,13 @@ c490ac5810b70f3cf5fd8649669838e8fdb19f4d 769dcdc88a1263638ae25944ba6b2be3e8933666 # Reformat all docs using docstrfmt ab5acaabb3cd24c482adb7fa4800c89fd6a2f08d +# Replace format calls with f-strings +4a361bd501e85de12c91c2474c423559ca672852 +# Replace percent formatting +9352a79e4108bd67f7e40b1e944c01e0a7353272 +# Replace string concatenation (' + ') +1c16b2b3087e9c3635d68d41c9541c4319d0bdbe +# Do not use backslashes to deal with long strings +2fccf64efe82851861e195b521b14680b480a42a +# Do not use explicit indices for logging args when not needed +d93ddf8dd43e4f9ed072a03829e287c78d2570a2 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 92375b465c..031e8fbc5a 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -238,25 +238,22 @@ There are a few coding conventions we use in beets: .. code-block:: python with g.lib.transaction() as tx: - rows = tx.query( - "SELECT DISTINCT '{0}' FROM '{1}' ORDER BY '{2}'".format( - field, model._table, sort_field - ) - ) + rows = tx.query("SELECT DISTINCT {field} FROM {model._table} ORDER BY {sort_field}") To fetch Item objects from the database, use lib.items(…) and supply a query as an argument. Resist the urge to write raw SQL for your query. If you must - use lower-level queries into the database, do this: + use lower-level queries into the database, do this, for example: .. code-block:: python with lib.transaction() as tx: - rows = tx.query("SELECT …") + rows = tx.query("SELECT path FROM items WHERE album_id = ?", (album_id,)) Transaction objects help control concurrent access to the database and assist in debugging conflicting accesses. -- ``str.format()`` should be used instead of the ``%`` operator +- f-strings should be used instead of the ``%`` operator and ``str.format()`` + calls. - Never ``print`` informational messages; use the `logging `__ module instead. In particular, we have our own logging shim, so you’ll see ``from beets import @@ -264,7 +261,7 @@ There are a few coding conventions we use in beets: - The loggers use `str.format `__-style logging - instead of ``%``-style, so you can type ``log.debug("{0}", obj)`` to do your + instead of ``%``-style, so you can type ``log.debug("{}", obj)`` to do your formatting. - Exception handlers must use ``except A as B:`` instead of ``except A, B:``. diff --git a/beets/__init__.py b/beets/__init__.py index 8be3052029..c5b93230f8 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -35,7 +35,7 @@ def read(self, user=True, defaults=True): 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 a/beets/art.py b/beets/art.py index 2ff58c309e..656c303ce5 100644 --- a/beets/art.py +++ b/beets/art.py @@ -38,11 +38,7 @@ def get_art(log, item): try: mf = mediafile.MediaFile(syspath(item.path)) except mediafile.UnreadableFileError as exc: - log.warning( - "Could not extract art from {0}: {1}", - displayable_path(item.path), - exc, - ) + log.warning("Could not extract art from {.filepath}: {}", item, exc) return return mf.art @@ -83,16 +79,16 @@ def embed_item( # Get the `Image` object from the file. try: - log.debug("embedding {0}", displayable_path(imagepath)) + log.debug("embedding {}", displayable_path(imagepath)) image = mediafile_image(imagepath, maxwidth) except OSError as exc: - log.warning("could not read image file: {0}", exc) + log.warning("could not read image file: {}", exc) return # Make sure the image kind is safe (some formats only support PNG # and JPEG). if image.mime_type not in ("image/jpeg", "image/png"): - log.info("not embedding image of unsupported type: {}", image.mime_type) + log.info("not embedding image of unsupported type: {.mime_type}", image) return item.try_write(path=itempath, tags={"images": [image]}, id3v23=id3v23) @@ -110,11 +106,11 @@ def embed_album( """Embed album art into all of the album's items.""" imagepath = album.artpath if not imagepath: - log.info("No album art present for {0}", album) + log.info("No album art present for {}", album) return if not os.path.isfile(syspath(imagepath)): log.info( - "Album art not found at {0} for {1}", + "Album art not found at {} for {}", displayable_path(imagepath), album, ) @@ -122,7 +118,7 @@ def embed_album( if maxwidth: imagepath = resize_image(log, imagepath, maxwidth, quality) - log.info("Embedding album art into {0}", album) + log.info("Embedding album art into {}", album) for item in album.items(): embed_item( @@ -143,8 +139,7 @@ def resize_image(log, imagepath, maxwidth, quality): specified quality level. """ log.debug( - "Resizing album art to {0} pixels wide and encoding at quality \ - level {1}", + "Resizing album art to {} pixels wide and encoding at quality level {}", maxwidth, quality, ) @@ -184,18 +179,18 @@ def extract(log, outpath, item): art = get_art(log, item) outpath = bytestring_path(outpath) if not art: - log.info("No album art present in {0}, skipping.", item) + log.info("No album art present in {}, skipping.", item) return # Add an extension to the filename. ext = mediafile.image_extension(art) if not ext: - log.warning("Unknown image type in {0}.", displayable_path(item.path)) + log.warning("Unknown image type in {.filepath}.", item) return - outpath += bytestring_path("." + ext) + outpath += bytestring_path(f".{ext}") log.info( - "Extracting album art from: {0} to: {1}", + "Extracting album art from: {} to: {}", item, displayable_path(outpath), ) @@ -213,7 +208,7 @@ def extract_first(log, outpath, items): def clear(log, lib, query): items = lib.items(query) - log.info("Clearing album art from {0} items", len(items)) + log.info("Clearing album art from {} items", len(items)) for item in items: - log.debug("Clearing art for {0}", item) + log.debug("Clearing art for {}", item) item.try_write(tags={"images": None}) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 4d107b3a1c..319f7f5226 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -261,7 +261,7 @@ def apply_metadata(album_info: AlbumInfo, mapping: Mapping[Item, TrackInfo]): continue for suffix in "year", "month", "day": - key = prefix + suffix + key = f"{prefix}{suffix}" value = getattr(album_info, key) or 0 # If we don't even have a year, apply nothing. diff --git a/beets/autotag/distance.py b/beets/autotag/distance.py index 39d16858f4..727439ea3a 100644 --- a/beets/autotag/distance.py +++ b/beets/autotag/distance.py @@ -78,10 +78,10 @@ def string_dist(str1: str | None, str2: str | None) -> float: # 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]) + if str1.endswith(f", {word}"): + str1 = f"{word} {str1[: -len(word) - 2]}" + if str2.endswith(f", {word}"): + str2 = f"{word} {str2[: -len(word) - 2]}" # Perform a couple of basic normalizing substitutions. for pat, repl in SD_REPLACE: @@ -230,7 +230,7 @@ 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)) + f"`dist` must be a Distance object, not {type(dist)}" ) for key, penalties in dist._penalties.items(): self._penalties.setdefault(key, []).extend(penalties) @@ -444,7 +444,7 @@ def distance( # Preferred media options. media_patterns: Sequence[str] = preferred_config["media"].as_str_seq() options = [ - re.compile(r"(\d+x)?(%s)" % pat, re.I) for pat in media_patterns + re.compile(rf"(\d+x)?({pat})", re.I) for pat in media_patterns ] if options: dist.add_priority("media", album_info.media, options) diff --git a/beets/autotag/match.py b/beets/autotag/match.py index e74d217551..8fec844a64 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -118,7 +118,7 @@ def match_by_id(items: Iterable[Item]) -> AlbumInfo | None: log.debug("No album ID consensus.") return None # If all album IDs are equal, look up the album. - log.debug("Searching for discovered album ID: {0}", first) + log.debug("Searching for discovered album ID: {}", first) return metadata_plugins.album_for_id(first) @@ -197,9 +197,7 @@ def _add_candidate( checking the track count, ordering the items, checking for duplicates, and calculating the distance. """ - log.debug( - "Candidate: {0} - {1} ({2})", info.artist, info.album, info.album_id - ) + log.debug("Candidate: {0.artist} - {0.album} ({0.album_id})", info) # Discard albums with zero tracks. if not info.tracks: @@ -215,7 +213,7 @@ def _add_candidate( required_tags: Sequence[str] = config["match"]["required"].as_str_seq() for req_tag in required_tags: if getattr(info, req_tag) is None: - log.debug("Ignored. Missing required tag: {0}", req_tag) + log.debug("Ignored. Missing required tag: {}", req_tag) return # Find mapping between the items and the track info. @@ -229,10 +227,10 @@ def _add_candidate( ignored_tags: Sequence[str] = config["match"]["ignored"].as_str_seq() for penalty in ignored_tags: if penalty in penalties: - log.debug("Ignored. Penalty: {0}", penalty) + log.debug("Ignored. Penalty: {}", penalty) return - log.debug("Success. Distance: {0}", dist) + log.debug("Success. Distance: {}", dist) results[info.album_id] = hooks.AlbumMatch( dist, info, mapping, extra_items, extra_tracks ) @@ -265,7 +263,7 @@ def tag_album( likelies, consensus = get_most_common_tags(items) cur_artist: str = likelies["artist"] cur_album: str = likelies["album"] - log.debug("Tagging {0} - {1}", cur_artist, cur_album) + log.debug("Tagging {} - {}", cur_artist, cur_album) # The output result, keys are the MB album ID. candidates: dict[Any, AlbumMatch] = {} @@ -273,7 +271,7 @@ def tag_album( # Search by explicit ID. if search_ids: for search_id in search_ids: - log.debug("Searching for album ID: {0}", search_id) + log.debug("Searching for album ID: {}", search_id) if info := metadata_plugins.album_for_id(search_id): _add_candidate(items, candidates, info) @@ -283,7 +281,7 @@ def tag_album( if info := match_by_id(items): _add_candidate(items, candidates, info) rec = _recommendation(list(candidates.values())) - log.debug("Album ID match recommendation is {0}", rec) + log.debug("Album ID match recommendation is {}", rec) if candidates and not config["import"]["timid"]: # If we have a very good MBID match, return immediately. # Otherwise, this match will compete against metadata-based @@ -300,7 +298,7 @@ def tag_album( if not (search_artist and search_album): # No explicit search terms -- use current metadata. search_artist, search_album = cur_artist, cur_album - log.debug("Search terms: {0} - {1}", search_artist, search_album) + log.debug("Search terms: {} - {}", search_artist, search_album) # Is this album likely to be a "various artist" release? va_likely = ( @@ -308,7 +306,7 @@ def tag_album( or (search_artist.lower() in VA_ARTISTS) or any(item.comp for item in items) ) - log.debug("Album might be VA: {0}", va_likely) + log.debug("Album might be VA: {}", va_likely) # Get the results from the data sources. for matched_candidate in metadata_plugins.candidates( @@ -316,7 +314,7 @@ def tag_album( ): _add_candidate(items, candidates, matched_candidate) - log.debug("Evaluating {0} candidates.", len(candidates)) + log.debug("Evaluating {} candidates.", len(candidates)) # Sort and get the recommendation. candidates_sorted = _sort_candidates(candidates.values()) rec = _recommendation(candidates_sorted) @@ -345,7 +343,7 @@ def tag_item( trackids = search_ids or [t for t in [item.mb_trackid] if t] if trackids: for trackid in trackids: - log.debug("Searching for track ID: {0}", trackid) + log.debug("Searching for track ID: {}", trackid) if info := metadata_plugins.track_for_id(trackid): dist = track_distance(item, info, incl_artist=True) candidates[info.track_id] = hooks.TrackMatch(dist, info) @@ -369,7 +367,7 @@ def tag_item( # Search terms. search_artist = search_artist or item.artist search_title = search_title or item.title - log.debug("Item search terms: {0} - {1}", search_artist, search_title) + log.debug("Item search terms: {} - {}", search_artist, search_title) # Get and evaluate candidate metadata. for track_info in metadata_plugins.item_candidates( @@ -379,7 +377,7 @@ def tag_item( candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info) # Sort by distance and return with recommendation. - log.debug("Found {0} candidates.", len(candidates)) + log.debug("Found {} candidates.", len(candidates)) candidates_sorted = _sort_candidates(candidates.values()) rec = _recommendation(candidates_sorted) return Proposal(candidates_sorted, rec) diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 81c1be4b90..8cd89111ee 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -390,9 +390,9 @@ def _awaken( return obj def __repr__(self) -> str: - return "{}({})".format( - type(self).__name__, - ", ".join(f"{k}={v!r}" for k, v in dict(self).items()), + return ( + f"{type(self).__name__}" + f"({', '.join(f'{k}={v!r}' for k, v in dict(self).items())})" ) def clear_dirty(self): @@ -409,9 +409,9 @@ def _check_db(self, need_id: bool = True) -> D: exception is raised otherwise. """ if not self._db: - raise ValueError("{} has no database".format(type(self).__name__)) + raise ValueError(f"{type(self).__name__} has no database") if need_id and not self.id: - raise ValueError("{} has no id".format(type(self).__name__)) + raise ValueError(f"{type(self).__name__} has no id") return self._db @@ -588,16 +588,14 @@ def store(self, fields: Iterable[str] | None = None): for key in fields: if key != "id" and key in self._dirty: self._dirty.remove(key) - assignments.append(key + "=?") + assignments.append(f"{key}=?") value = self._type(key).to_sql(self[key]) subvars.append(value) 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 {','.join(assignments)} WHERE id=?" subvars.append(self.id) tx.mutate(query, subvars) @@ -607,9 +605,9 @@ def store(self, fields: Iterable[str] | None = None): self._dirty.remove(key) value = self._type(key).to_sql(value) tx.mutate( - "INSERT INTO {} " + f"INSERT INTO {self._flex_table} " "(entity_id, key, value) " - "VALUES (?, ?, ?);".format(self._flex_table), + "VALUES (?, ?, ?);", (self.id, key, value), ) @@ -1160,7 +1158,7 @@ def _make_table(self, table: str, fields: Mapping[str, types.Type]): """ # Get current schema. with self.transaction() as tx: - rows = tx.query("PRAGMA table_info(%s)" % table) + rows = tx.query(f"PRAGMA table_info({table})") current_fields = {row[1] for row in rows} field_names = set(fields.keys()) @@ -1173,9 +1171,7 @@ def _make_table(self, table: str, fields: Mapping[str, types.Type]): columns = [] for name, typ in fields.items(): columns.append(f"{name} {typ.sql}") - setup_sql = "CREATE TABLE {} ({});\n".format( - table, ", ".join(columns) - ) + setup_sql = f"CREATE TABLE {table} ({', '.join(columns)});\n" else: # Table exists does not match the field set. @@ -1183,8 +1179,8 @@ def _make_table(self, table: str, fields: Mapping[str, types.Type]): 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: @@ -1195,18 +1191,16 @@ def _make_attribute_table(self, flex_table: str): for the given entity (if they don't exist). """ with self.transaction() as tx: - tx.script( - """ - CREATE TABLE IF NOT EXISTS {0} ( + tx.script(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 a/beets/dbcore/query.py b/beets/dbcore/query.py index 49d7f64283..dfeb427078 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -190,7 +190,7 @@ class MatchQuery(FieldQuery[AnySQLiteType]): """A query that looks for exact matches in an Model field.""" def col_clause(self) -> tuple[str, Sequence[SQLiteType]]: - return self.field + " = ?", [self.pattern] + return f"{self.field} = ?", [self.pattern] @classmethod def value_match(cls, pattern: AnySQLiteType, value: Any) -> bool: @@ -204,7 +204,7 @@ def __init__(self, field, fast: bool = True): super().__init__(field, None, fast) def col_clause(self) -> tuple[str, Sequence[SQLiteType]]: - return self.field + " IS NULL", () + return f"{self.field} IS NULL", () def match(self, obj: Model) -> bool: return obj.get(self.field_name) is None @@ -246,7 +246,7 @@ def col_clause(self) -> tuple[str, Sequence[SQLiteType]]: .replace("%", "\\%") .replace("_", "\\_") ) - clause = self.field + " like ? escape '\\'" + clause = f"{self.field} like ? escape '\\'" subvals = [search] return clause, subvals @@ -264,8 +264,8 @@ def col_clause(self) -> tuple[str, Sequence[SQLiteType]]: .replace("%", "\\%") .replace("_", "\\_") ) - search = "%" + pattern + "%" - clause = self.field + " like ? escape '\\'" + search = f"%{pattern}%" + clause = f"{self.field} like ? escape '\\'" subvals = [search] return clause, subvals @@ -471,11 +471,11 @@ def match(self, obj: Model) -> bool: def col_clause(self) -> tuple[str, Sequence[SQLiteType]]: if self.point is not None: - return self.field + "=?", (self.point,) + return f"{self.field}=?", (self.point,) else: if self.rangemin is not None and self.rangemax is not None: return ( - "{0} >= ? AND {0} <= ?".format(self.field), + f"{self.field} >= ? AND {self.field} <= ?", (self.rangemin, self.rangemax), ) elif self.rangemin is not None: @@ -549,9 +549,9 @@ def clause_with_joiner( if not subq_clause: # Fall back to slow query. return None, () - clause_parts.append("(" + subq_clause + ")") + clause_parts.append(f"({subq_clause})") subvals += subq_subvals - clause = (" " + joiner + " ").join(clause_parts) + clause = f" {joiner} ".join(clause_parts) return clause, subvals def __repr__(self) -> str: @@ -690,9 +690,7 @@ class Period: ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"), # second ) relative_units = {"y": 365, "m": 30, "w": 7, "d": 1} - relative_re = ( - "(?P[+|-]?)(?P[0-9]+)" + "(?P[y|m|w|d])" - ) + relative_re = "(?P[+|-]?)(?P[0-9]+)(?P[y|m|w|d])" def __init__(self, date: datetime, precision: str): """Create a period with the given date (a `datetime` object) and @@ -800,9 +798,7 @@ class DateInterval: def __init__(self, start: datetime | None, end: datetime | None): 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 @@ -850,8 +846,6 @@ def match(self, obj: Model) -> bool: date = datetime.fromtimestamp(timestamp) return self.interval.contains(date) - _clause_tmpl = "{0} {1} ?" - def col_clause(self) -> tuple[str, Sequence[SQLiteType]]: clause_parts = [] subvals = [] @@ -859,11 +853,11 @@ def col_clause(self) -> tuple[str, Sequence[SQLiteType]]: # 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: @@ -1074,9 +1068,9 @@ def order_clause(self) -> str: if self.case_insensitive: field = ( "(CASE " - "WHEN TYPEOF({0})='text' THEN LOWER({0}) " - "WHEN TYPEOF({0})='blob' THEN LOWER({0}) " - "ELSE {0} END)".format(self.field) + f"WHEN TYPEOF({self.field})='text' THEN LOWER({self.field}) " + f"WHEN TYPEOF({self.field})='blob' THEN LOWER({self.field}) " + f"ELSE {self.field} END)" ) else: field = self.field diff --git a/beets/dbcore/types.py b/beets/dbcore/types.py index 1b8434a0ba..3b4badd33c 100644 --- a/beets/dbcore/types.py +++ b/beets/dbcore/types.py @@ -194,7 +194,7 @@ def __init__(self, digits: int): self.digits = digits def format(self, value: int | N) -> str: - return "{0:0{1}d}".format(value or 0, self.digits) + return f"{value or 0:0{self.digits}d}" class PaddedInt(BasePaddedInt[int]): @@ -219,7 +219,7 @@ def __init__(self, unit: int, suffix: str = ""): self.suffix = suffix def format(self, value: int) -> str: - return "{}{}".format((value or 0) // self.unit, self.suffix) + return f"{(value or 0) // self.unit}{self.suffix}" class Id(NullInteger): @@ -249,7 +249,7 @@ def __init__(self, digits: int = 1): self.digits = digits def format(self, value: float | N) -> str: - return "{0:.{1}f}".format(value or 0, self.digits) + return f"{value or 0:.{self.digits}f}" class Float(BaseFloat[float]): diff --git a/beets/importer/session.py b/beets/importer/session.py index e45644fa33..46277837e8 100644 --- a/beets/importer/session.py +++ b/beets/importer/session.py @@ -150,7 +150,7 @@ def tag_log(self, status, paths: Sequence[PathBytes]): """Log a message about a given album to the importer log. The status should reflect the reason the album couldn't be tagged. """ - self.logger.info("{0} {1}", status, displayable_path(paths)) + self.logger.info("{} {}", status, displayable_path(paths)) def log_choice(self, task: ImportTask, duplicate=False): """Logs the task's current choice if it should be logged. If @@ -187,7 +187,7 @@ def choose_item(self, task: ImportTask): def run(self): """Run the import task.""" - self.logger.info("import started {0}", time.asctime()) + self.logger.info("import started {}", time.asctime()) self.set_config(config["import"]) # Set up the pipeline. @@ -297,7 +297,7 @@ def ask_resume(self, toppath: PathBytes): # Either accept immediately or prompt for input to decide. if self.want_resume is True or self.should_resume(toppath): log.warning( - "Resuming interrupted import of {0}", + "Resuming interrupted import of {}", util.displayable_path(toppath), ) self._is_resuming[toppath] = True diff --git a/beets/importer/stages.py b/beets/importer/stages.py index 24ff815f3a..d99b742a2b 100644 --- a/beets/importer/stages.py +++ b/beets/importer/stages.py @@ -58,11 +58,11 @@ def read_tasks(session: ImportSession): skipped += task_factory.skipped if not task_factory.imported: - log.warning("No files imported from {0}", displayable_path(toppath)) + log.warning("No files imported from {}", displayable_path(toppath)) # Show skipped directories (due to incremental/resume). if skipped: - log.info("Skipped {0} paths.", skipped) + log.info("Skipped {} paths.", skipped) def query_tasks(session: ImportSession): @@ -82,10 +82,7 @@ def query_tasks(session: ImportSession): # Search for albums. for album in session.lib.albums(session.query): log.debug( - "yielding album {0}: {1} - {2}", - album.id, - album.albumartist, - album.album, + "yielding album {0.id}: {0.albumartist} - {0.album}", album ) items = list(album.items()) _freshen_items(items) @@ -140,7 +137,7 @@ def lookup_candidates(session: ImportSession, task: ImportTask): return plugins.send("import_task_start", session=session, task=task) - log.debug("Looking up: {0}", displayable_path(task.paths)) + log.debug("Looking up: {}", displayable_path(task.paths)) # Restrict the initial lookup to IDs specified by the user via the -m # option. Currently all the IDs are passed onto the tasks directly. @@ -259,11 +256,11 @@ def plugin_stage( def log_files(session: ImportSession, task: ImportTask): """A coroutine (pipeline stage) to log each file to be imported.""" if isinstance(task, SingletonImportTask): - log.info("Singleton: {0}", displayable_path(task.item["path"])) + log.info("Singleton: {}", displayable_path(task.item["path"])) elif task.items: - log.info("Album: {0}", displayable_path(task.paths[0])) + log.info("Album: {}", displayable_path(task.paths[0])) for item in task.items: - log.info(" {0}", displayable_path(item["path"])) + log.info(" {}", displayable_path(item["path"])) # --------------------------------- Consumer --------------------------------- # @@ -341,9 +338,7 @@ def _resolve_duplicates(session: ImportSession, task: ImportTask): 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("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( @@ -355,7 +350,7 @@ def _resolve_duplicates(session: ImportSession, task: ImportTask): "ask": "a", } ) - log.debug("default action for duplicates: {0}", duplicate_action) + log.debug("default action for duplicates: {}", duplicate_action) if duplicate_action == "s": # Skip new. diff --git a/beets/importer/state.py b/beets/importer/state.py index fccb7c282e..fde26c6063 100644 --- a/beets/importer/state.py +++ b/beets/importer/state.py @@ -87,7 +87,7 @@ def _open( # unpickling, including ImportError. We use a catch-all # exception to avoid enumerating them all (the docs don't even have a # full list!). - log.debug("state file could not be read: {0}", exc) + log.debug("state file could not be read: {}", exc) def _save(self): try: @@ -100,7 +100,7 @@ def _save(self): f, ) except OSError as exc: - log.error("state file could not be written: {0}", exc) + log.error("state file could not be written: {}", exc) # -------------------------------- Tagprogress ------------------------------- # diff --git a/beets/importer/tasks.py b/beets/importer/tasks.py index abe2ca8a96..b4d566032c 100644 --- a/beets/importer/tasks.py +++ b/beets/importer/tasks.py @@ -267,13 +267,11 @@ def duplicate_items(self, lib: library.Library): def remove_duplicates(self, lib: library.Library): duplicate_items = self.duplicate_items(lib) - log.debug("removing {0} old duplicated items", len(duplicate_items)) + log.debug("removing {} old duplicated items", len(duplicate_items)) for item in duplicate_items: item.remove() if lib.directory in util.ancestry(item.path): - log.debug( - "deleting duplicate {0}", util.displayable_path(item.path) - ) + log.debug("deleting duplicate {.filepath}", item) util.remove(item.path) util.prune_dirs(os.path.dirname(item.path), lib.directory) @@ -285,10 +283,10 @@ def set_fields(self, lib: library.Library): for field, view in config["import"]["set_fields"].items(): value = str(view.get()) log.debug( - "Set field {1}={2} for {0}", - util.displayable_path(self.paths), + "Set field {}={} for {}", field, value, + util.displayable_path(self.paths), ) self.album.set_parse(field, format(self.album, value)) for item in items: @@ -554,12 +552,11 @@ def _reduce_and_log(new_obj, existing_fields, overwrite_keys): ] if overwritten_fields: log.debug( - "Reimported {} {}. Not preserving flexible attributes {}. " - "Path: {}", + "Reimported {0} {1.id}. Not preserving flexible attributes {2}. " + "Path: {1.filepath}", noun, - new_obj.id, + new_obj, overwritten_fields, - util.displayable_path(new_obj.path), ) for key in overwritten_fields: del existing_fields[key] @@ -578,17 +575,15 @@ def _reduce_and_log(new_obj, existing_fields, overwrite_keys): self.album.artpath = replaced_album.artpath self.album.store() log.debug( - "Reimported album {}. Preserving attribute ['added']. " - "Path: {}", - self.album.id, - util.displayable_path(self.album.path), + "Reimported album {0.album.id}. Preserving attribute ['added']. " + "Path: {0.album.filepath}", + self, ) log.debug( - "Reimported album {}. Preserving flexible attributes {}. " - "Path: {}", - self.album.id, + "Reimported album {0.album.id}. Preserving flexible" + " attributes {1}. Path: {0.album.filepath}", + self, list(album_fields.keys()), - util.displayable_path(self.album.path), ) for item in self.imported_items(): @@ -597,21 +592,19 @@ def _reduce_and_log(new_obj, existing_fields, overwrite_keys): if dup_item.added and dup_item.added != item.added: item.added = dup_item.added log.debug( - "Reimported item {}. Preserving attribute ['added']. " - "Path: {}", - item.id, - util.displayable_path(item.path), + "Reimported item {0.id}. Preserving attribute ['added']. " + "Path: {0.filepath}", + item, ) item_fields = _reduce_and_log( item, dup_item._values_flex, REIMPORT_FRESH_FIELDS_ITEM ) item.update(item_fields) log.debug( - "Reimported item {}. Preserving flexible attributes {}. " - "Path: {}", - item.id, + "Reimported item {0.id}. Preserving flexible attributes {1}. " + "Path: {0.filepath}", + item, list(item_fields.keys()), - util.displayable_path(item.path), ) item.store() @@ -621,14 +614,10 @@ def remove_replaced(self, lib): """ for item in self.imported_items(): for dup_item in self.replaced_items[item]: - log.debug( - "Replacing item {0}: {1}", - dup_item.id, - util.displayable_path(item.path), - ) + log.debug("Replacing item {.id}: {.filepath}", dup_item, item) dup_item.remove() log.debug( - "{0} of {1} items replaced", + "{} of {} items replaced", sum(bool(v) for v in self.replaced_items.values()), len(self.imported_items()), ) @@ -747,10 +736,10 @@ def set_fields(self, lib): for field, view in config["import"]["set_fields"].items(): value = str(view.get()) log.debug( - "Set field {1}={2} for {0}", - util.displayable_path(self.paths), + "Set field {}={} for {}", field, value, + util.displayable_path(self.paths), ) self.item.set_parse(field, format(self.item, value)) self.item.store() @@ -870,7 +859,7 @@ def cleanup(self, copy=False, delete=False, move=False): """Removes the temporary directory the archive was extracted to.""" if self.extracted and self.toppath: log.debug( - "Removing extracted directory: {0}", + "Removing extracted directory: {}", util.displayable_path(self.toppath), ) shutil.rmtree(util.syspath(self.toppath)) @@ -1002,7 +991,7 @@ def singleton(self, path: util.PathBytes): """Return a `SingletonImportTask` for the music file.""" if self.session.already_imported(self.toppath, [path]): log.debug( - "Skipping previously-imported path: {0}", + "Skipping previously-imported path: {}", util.displayable_path(path), ) self.skipped += 1 @@ -1026,7 +1015,7 @@ def album(self, paths: Iterable[util.PathBytes], dirs=None): if self.session.already_imported(self.toppath, dirs): log.debug( - "Skipping previously-imported path: {0}", + "Skipping previously-imported path: {}", util.displayable_path(dirs), ) self.skipped += 1 @@ -1063,19 +1052,17 @@ def unarchive(self): ) return - log.debug( - "Extracting archive: {0}", util.displayable_path(self.toppath) - ) + log.debug("Extracting archive: {}", util.displayable_path(self.toppath)) archive_task = ArchiveImportTask(self.toppath) try: archive_task.extract() except Exception as exc: - log.error("extraction failed: {0}", exc) + log.error("extraction failed: {}", exc) return # Now read albums from the extracted directory. self.toppath = archive_task.toppath - log.debug("Archive extracted to: {0}", self.toppath) + log.debug("Archive extracted to: {.toppath}", self) return archive_task def read_item(self, path: util.PathBytes): @@ -1091,10 +1078,10 @@ def read_item(self, path: util.PathBytes): # Silently ignore non-music files. pass elif isinstance(exc.reason, mediafile.UnreadableFileError): - log.warning("unreadable file: {0}", util.displayable_path(path)) + log.warning("unreadable file: {}", util.displayable_path(path)) else: log.error( - "error reading {0}: {1}", util.displayable_path(path), exc + "error reading {}: {}", util.displayable_path(path), exc ) diff --git a/beets/library/exceptions.py b/beets/library/exceptions.py index 7f117a2fef..0dc874c2a1 100644 --- a/beets/library/exceptions.py +++ b/beets/library/exceptions.py @@ -28,11 +28,11 @@ class ReadError(FileOperationError): """An error while reading a file (i.e. in `Item.read`).""" def __str__(self): - return "error reading " + str(super()) + return f"error reading {super()}" class WriteError(FileOperationError): """An error while writing a file (i.e. in `Item.write`).""" def __str__(self): - return "error writing " + str(super()) + return f"error writing {super()}" diff --git a/beets/library/models.py b/beets/library/models.py index 7501513a1c..cbee2a411e 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -425,7 +425,7 @@ def move_art(self, operation=MoveOperation.MOVE): new_art = util.unique_path(new_art) log.debug( - "moving album art {0} to {1}", + "moving album art {} to {}", util.displayable_path(old_art), util.displayable_path(new_art), ) @@ -482,7 +482,7 @@ def item_dir(self): """ item = self.items().get() if not item: - raise ValueError("empty album for album id %d" % self.id) + raise ValueError(f"empty album for album id {self.id}") return os.path.dirname(item.path) def _albumtotal(self): @@ -844,12 +844,9 @@ def __repr__(self): # 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) - ), + return ( + f"{type(self).__name__}" + f"({', '.join(f'{k}={self[k]!r}' for k in self.keys(with_album=False))})" ) def keys(self, computed=False, with_album=True): @@ -995,7 +992,7 @@ def try_write(self, *args, **kwargs): self.write(*args, **kwargs) return True except FileOperationError as exc: - log.error("{0}", exc) + log.error("{}", exc) return False def try_sync(self, write, move, with_album=True): @@ -1015,10 +1012,7 @@ def try_sync(self, write, move, with_album=True): if move: # Check whether this file is inside the library directory. if self._db and self._db.directory in util.ancestry(self.path): - log.debug( - "moving {0} to synchronize path", - util.displayable_path(self.path), - ) + log.debug("moving {.filepath} to synchronize path", self) self.move(with_album=with_album) self.store() @@ -1090,7 +1084,7 @@ def try_filesize(self): try: return os.path.getsize(syspath(self.path)) except (OSError, Exception) as exc: - log.warning("could not get filesize: {0}", exc) + log.warning("could not get filesize: {}", exc) return 0 # Model methods. diff --git a/beets/plugins.py b/beets/plugins.py index 2daede6551..d9df4323c4 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -130,9 +130,9 @@ def __init__(self, plugin): def filter(self, record): if hasattr(record.msg, "msg") and isinstance(record.msg.msg, str): # A _LogMessage from our hacked-up Logging replacement. - record.msg.msg = self.prefix + record.msg.msg + record.msg.msg = f"{self.prefix}{record.msg.msg}" elif isinstance(record.msg, str): - record.msg = self.prefix + record.msg + record.msg = f"{self.prefix}{record.msg}" return True @@ -424,9 +424,9 @@ def types(model_cls: type[AnyModel]) -> dict[str, Type]: for field in plugin_types: if field in types and plugin_types[field] != types[field]: raise PluginConflictError( - "Plugin {} defines flexible field {} " + f"Plugin {plugin.name} defines flexible field {field} " "which has already been defined with " - "another type.".format(plugin.name, field) + "another type." ) types.update(plugin_types) return types @@ -543,7 +543,7 @@ def send(event: EventType, **arguments: Any) -> list[Any]: Return a list of non-None values returned from the handlers. """ - log.debug("Sending event: {0}", event) + log.debug("Sending event: {}", event) return [ r for handler in BeetsPlugin.listeners[event] @@ -560,8 +560,8 @@ def feat_tokens(for_artist: bool = True) -> str: 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) + return ( + rf"(?<=[\s(\[])(?:{'|'.join(re.escape(x) for x in feat_words)})(?=\s)" ) diff --git a/beets/test/_common.py b/beets/test/_common.py index d70f9ec803..ffb2bfd65d 100644 --- a/beets/test/_common.py +++ b/beets/test/_common.py @@ -153,7 +153,7 @@ def __init__(self, out=None): self.out = out def add(self, s): - self.buf.append(s + "\n") + self.buf.append(f"{s}\n") def close(self): pass diff --git a/beets/test/helper.py b/beets/test/helper.py index f1633c1102..85adc0825c 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -267,7 +267,7 @@ def create_item(self, **values): The item is attached to the database from `self.lib`. """ values_ = { - "title": "t\u00eftle {0}", + "title": "t\u00eftle {}", "artist": "the \u00e4rtist", "album": "the \u00e4lbum", "track": 1, @@ -278,7 +278,7 @@ def create_item(self, **values): values_["db"] = self.lib item = Item(**values_) if "path" not in values: - item["path"] = "audio." + item["format"].lower() + item["path"] = f"audio.{item['format'].lower()}" # mtime needs to be set last since other assignments reset it. item.mtime = 12345 return item @@ -310,7 +310,7 @@ def add_item_fixture(self, **values): item = self.create_item(**values) extension = item["format"].lower() item["path"] = os.path.join( - _common.RSRC, util.bytestring_path("min." + extension) + _common.RSRC, util.bytestring_path(f"min.{extension}") ) item.add(self.lib) item.move(operation=MoveOperation.COPY) @@ -325,7 +325,7 @@ def add_item_fixtures(self, ext="mp3", count=1): """Add a number of items with files to the database.""" # TODO base this on `add_item()` items = [] - path = os.path.join(_common.RSRC, util.bytestring_path("full." + ext)) + path = os.path.join(_common.RSRC, util.bytestring_path(f"full.{ext}")) for i in range(count): item = Item.from_path(path) item.album = f"\u00e4lbum {i}" # Check unicode paths @@ -372,7 +372,7 @@ def create_mediafile_fixture(self, ext="mp3", images=[]): specified extension a cover art image is added to the media file. """ - src = os.path.join(_common.RSRC, util.bytestring_path("full." + ext)) + src = os.path.join(_common.RSRC, util.bytestring_path(f"full.{ext}")) handle, path = mkstemp(dir=self.temp_dir) path = bytestring_path(path) os.close(handle) @@ -570,7 +570,7 @@ def prepare_track_for_import( medium = MediaFile(track_path) medium.update( { - "album": "Tag Album" + (f" {album_id}" if album_id else ""), + "album": f"Tag Album{f' {album_id}' if album_id else ''}", "albumartist": None, "mb_albumid": None, "comp": None, @@ -831,23 +831,21 @@ def item_candidates(self, item, artist, title): def _make_track_match(self, artist, album, number): return TrackInfo( - title="Applied Track %d" % number, - track_id="match %d" % number, + title=f"Applied Track {number}", + track_id=f"match {number}", artist=artist, length=1, index=0, ) def _make_album_match(self, artist, album, tracks, distance=0, missing=0): - if distance: - id = " " + "M" * distance - else: - id = "" + id = f" {'M' * distance}" if distance else "" + if artist is None: artist = "Various Artists" else: - artist = artist.replace("Tag", "Applied") + id - album = album.replace("Tag", "Applied") + id + artist = f"{artist.replace('Tag', 'Applied')}{id}" + album = f"{album.replace('Tag', 'Applied')}{id}" track_infos = [] for i in range(tracks - missing): @@ -858,8 +856,8 @@ def _make_album_match(self, artist, album, tracks, distance=0, missing=0): album=album, tracks=track_infos, va=False, - album_id="albumid" + id, - artist_id="artistid" + id, + album_id=f"albumid{id}", + artist_id=f"artistid{id}", albumtype="soundtrack", data_source="match_source", bandcamp_album_id="bc_url", @@ -885,7 +883,7 @@ def run(self, *args, **kwargs): super().run(*args, **kwargs) IMAGEHEADER: dict[str, bytes] = { - "image/jpeg": b"\xff\xd8\xff" + b"\x00" * 3 + b"JFIF", + "image/jpeg": b"\xff\xd8\xff\x00\x00\x00JFIF", "image/png": b"\211PNG\r\n\032\n", "image/gif": b"GIF89a", # dummy type that is definitely not a valid image content type diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 01030a9771..9f0ae82e17 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -125,7 +125,7 @@ def print_(*strings: str, end: str = "\n") -> None: The `end` keyword argument behaves similarly to the built-in `print` (it defaults to a newline). """ - txt = " ".join(strings or ("",)) + end + txt = f"{' '.join(strings or ('',))}{end}" # Encode the string and write it to stdout. # On Python 3, sys.stdout expects text strings and uses the @@ -269,7 +269,7 @@ def input_options( ) ): # The first option is the default; mark it. - show_letter = "[%s]" % found_letter.upper() + show_letter = f"[{found_letter.upper()}]" is_default = True else: show_letter = found_letter.upper() @@ -308,9 +308,9 @@ def input_options( if isinstance(default, int): default_name = str(default) default_name = colorize("action_default", default_name) - tmpl = "# selection (default %s)" - prompt_parts.append(tmpl % default_name) - prompt_part_lengths.append(len(tmpl % str(default))) + tmpl = "# selection (default {})" + prompt_parts.append(tmpl.format(default_name)) + prompt_part_lengths.append(len(tmpl) - 2 + len(str(default))) else: prompt_parts.append("# selection") prompt_part_lengths.append(len(prompt_parts[-1])) @@ -338,7 +338,7 @@ def input_options( if line_length != 0: # Not the beginning of the line; need a space. - part = " " + part + part = f" {part}" length += 1 prompt += part @@ -349,8 +349,8 @@ def input_options( if not fallback_prompt: fallback_prompt = "Enter one of " if numrange: - fallback_prompt += "%i-%i, " % numrange - fallback_prompt += ", ".join(display_letters) + ":" + fallback_prompt += "{}-{}, ".format(*numrange) + fallback_prompt += f"{', '.join(display_letters)}:" resp = input_(prompt) while True: @@ -406,7 +406,7 @@ def input_select_objects(prompt, objs, rep, prompt_all=None): objects individually. """ choice = input_options( - ("y", "n", "s"), False, "%s? (Yes/no/select)" % (prompt_all or prompt) + ("y", "n", "s"), False, f"{prompt_all or prompt}? (Yes/no/select)" ) print() # Blank line. @@ -420,7 +420,7 @@ def input_select_objects(prompt, objs, rep, prompt_all=None): answer = input_options( ("y", "n", "q"), True, - "%s? (yes/no/quit)" % prompt, + f"{prompt}? (yes/no/quit)", "Enter Y or N:", ) if answer == "y": @@ -494,7 +494,7 @@ def input_select_objects(prompt, objs, rep, prompt_all=None): "bg_cyan": 46, "bg_white": 47, } -RESET_COLOR = COLOR_ESCAPE + "39;49;00m" +RESET_COLOR = f"{COLOR_ESCAPE}39;49;00m" # These abstract COLOR_NAMES are lazily mapped on to the actual color in COLORS # as they are defined in the configuration files, see function: colorize @@ -534,8 +534,8 @@ def _colorize(color, text): # over all "ANSI codes" in `color`. escape = "" for code in color: - escape = escape + COLOR_ESCAPE + "%im" % ANSI_CODES[code] - return escape + text + RESET_COLOR + escape = f"{escape}{COLOR_ESCAPE}{ANSI_CODES[code]}m" + return f"{escape}{text}{RESET_COLOR}" def colorize(color_name, text): @@ -572,7 +572,7 @@ def colorize(color_name, text): # instead of the abstract color name ('text_error') color = COLORS.get(color_name) if not color: - log.debug("Invalid color_name: {0}", color_name) + log.debug("Invalid color_name: {}", color_name) color = color_name return _colorize(color, text) else: @@ -621,8 +621,8 @@ def color_split(colored_text, index): split_index = index - (length - color_len(part)) found_split = True if found_color_code: - pre_split += part[:split_index] + RESET_COLOR - post_split += found_color_code + part[split_index:] + pre_split += f"{part[:split_index]}{RESET_COLOR}" + post_split += f"{found_color_code}{part[split_index:]}" else: pre_split += part[:split_index] post_split += part[split_index:] @@ -726,7 +726,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 @@ -806,17 +806,17 @@ def split_into_lines(string, width_tuple): # Colorize each word with pre/post escapes # Reconstruct colored words words += [ - m.group("esc") + raw_word + RESET_COLOR + f"{m['esc']}{raw_word}{RESET_COLOR}" for raw_word in raw_words ] elif raw_words: # Pretext stops mid-word if m.group("esc") != RESET_COLOR: # Add the rest of the current word, with a reset after it - words[-1] += m.group("esc") + raw_words[0] + RESET_COLOR + words[-1] += f"{m['esc']}{raw_words[0]}{RESET_COLOR}" # Add the subsequent colored words: words += [ - m.group("esc") + raw_word + RESET_COLOR + f"{m['esc']}{raw_word}{RESET_COLOR}" for raw_word in raw_words[1:] ] else: @@ -907,18 +907,12 @@ def print_column_layout( With subsequent lines (i.e. {lhs1}, {rhs1} onwards) being the rest of contents, wrapped if the width would be otherwise exceeded. """ - if right["prefix"] + right["contents"] + right["suffix"] == "": + if f"{right['prefix']}{right['contents']}{right['suffix']}" == "": # No right hand information, so we don't need a separator. separator = "" first_line_no_wrap = ( - indent_str - + left["prefix"] - + left["contents"] - + left["suffix"] - + separator - + right["prefix"] - + right["contents"] - + right["suffix"] + f"{indent_str}{left['prefix']}{left['contents']}{left['suffix']}" + f"{separator}{right['prefix']}{right['contents']}{right['suffix']}" ) if color_len(first_line_no_wrap) < max_width: # Everything fits, print out line. @@ -1044,18 +1038,12 @@ def print_newline_layout( If {lhs0} would go over the maximum width, the subsequent lines are indented a second time for ease of reading. """ - if right["prefix"] + right["contents"] + right["suffix"] == "": + if f"{right['prefix']}{right['contents']}{right['suffix']}" == "": # No right hand information, so we don't need a separator. separator = "" first_line_no_wrap = ( - indent_str - + left["prefix"] - + left["contents"] - + left["suffix"] - + separator - + right["prefix"] - + right["contents"] - + right["suffix"] + f"{indent_str}{left['prefix']}{left['contents']}{left['suffix']}" + f"{separator}{right['prefix']}{right['contents']}{right['suffix']}" ) if color_len(first_line_no_wrap) < max_width: # Everything fits, print out line. @@ -1069,7 +1057,7 @@ def print_newline_layout( empty_space - len(indent_str), empty_space - len(indent_str), ) - left_str = left["prefix"] + left["contents"] + left["suffix"] + left_str = f"{left['prefix']}{left['contents']}{left['suffix']}" left_split = split_into_lines(left_str, left_width_tuple) # Repeat calculations for rhs, including separator on first line right_width_tuple = ( @@ -1077,19 +1065,19 @@ def print_newline_layout( empty_space - len(indent_str), empty_space - len(indent_str), ) - right_str = right["prefix"] + right["contents"] + right["suffix"] + right_str = f"{right['prefix']}{right['contents']}{right['suffix']}" right_split = split_into_lines(right_str, right_width_tuple) for i, line in enumerate(left_split): if i == 0: - print_(indent_str + line) + print_(f"{indent_str}{line}") elif line != "": # Ignore empty lines - print_(indent_str * 2 + line) + print_(f"{indent_str * 2}{line}") for i, line in enumerate(right_split): if i == 0: - print_(indent_str + separator + line) + print_(f"{indent_str}{separator}{line}") elif line != "": - print_(indent_str * 2 + line) + print_(f"{indent_str * 2}{line}") FLOAT_EPSILON = 0.01 @@ -1163,7 +1151,7 @@ def show_model_changes(new, old=None, fields=None, always=False): continue changes.append( - " {}: {}".format(field, colorize("text_highlight", new_fmt[field])) + f" {field}: {colorize('text_highlight', new_fmt[field])}" ) # Print changes. @@ -1204,22 +1192,16 @@ 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)) + print_(f"Source {' ' * title_pad} 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, - ) - ) + print_(f"{color_source} {' ' * pad} -> {color_dest}") # Helper functions for option parsing. @@ -1245,9 +1227,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 @@ -1426,8 +1406,8 @@ def root_parser(self): @root_parser.setter def root_parser(self, root_parser): self._root_parser = root_parser - self.parser.prog = "{} {}".format( - as_string(root_parser.get_prog_name()), self.name + self.parser.prog = ( + f"{as_string(root_parser.get_prog_name())} {self.name}" ) @@ -1483,7 +1463,7 @@ def format_help(self, formatter=None): for subcommand in subcommands: name = subcommand.name if subcommand.aliases: - name += " (%s)" % ", ".join(subcommand.aliases) + name += f" ({', '.join(subcommand.aliases)})" disp_names.append(name) # Set the help position based on the max width. @@ -1496,32 +1476,24 @@ def format_help(self, formatter=None): # Lifted directly from optparse.py. name_width = help_position - formatter.current_indent - 2 if len(name) > name_width: - name = "%*s%s\n" % (formatter.current_indent, "", name) + name = f"{' ' * formatter.current_indent}{name}\n" indent_first = help_position else: - name = "%*s%-*s " % ( - formatter.current_indent, - "", - name_width, - name, - ) + name = f"{' ' * formatter.current_indent}{name:<{name_width}}\n" indent_first = 0 result.append(name) help_width = formatter.width - help_position help_lines = textwrap.wrap(subcommand.help, help_width) help_line = help_lines[0] if help_lines else "" - result.append("%*s%s\n" % (indent_first, "", help_line)) + result.append(f"{' ' * indent_first}{help_line}\n") result.extend( - [ - "%*s%s\n" % (help_position, "", line) - for line in help_lines[1:] - ] + [f"{' ' * help_position}{line}\n" for line in help_lines[1:]] ) formatter.dedent() # Concatenate the original help message with the subcommand # list. - return out + "".join(result) + return f"{out}{''.join(result)}" def _subcommand_for_name(self, name): """Return the subcommand in self.subcommands matching the @@ -1615,19 +1587,19 @@ def _configure(options): if overlay_path: log.debug( - "overlaying configuration: {0}", util.displayable_path(overlay_path) + "overlaying configuration: {}", util.displayable_path(overlay_path) ) config_path = config.user_config_path() if os.path.isfile(config_path): - log.debug("user configuration: {0}", util.displayable_path(config_path)) + log.debug("user configuration: {}", util.displayable_path(config_path)) else: log.debug( - "no user configuration found at {0}", + "no user configuration found at {}", util.displayable_path(config_path), ) - log.debug("data directory: {0}", util.displayable_path(config.config_dir())) + log.debug("data directory: {}", util.displayable_path(config.config_dir())) return config @@ -1637,10 +1609,8 @@ def _ensure_db_directory_exists(path): newpath = os.path.dirname(path) if not os.path.isdir(newpath): if input_yn( - "The database directory {} does not \ - exist. Create it (Y/n)?".format( - util.displayable_path(newpath) - ) + f"The database directory {util.displayable_path(newpath)} does not" + " exist. Create it (Y/n)?" ): os.makedirs(newpath) @@ -1660,12 +1630,11 @@ def _open_library(config: confuse.LazyConfig) -> library.Library: except (sqlite3.OperationalError, sqlite3.DatabaseError) as db_error: log.debug("{}", traceback.format_exc()) raise UserError( - "database file {} cannot not be opened: {}".format( - util.displayable_path(dbpath), db_error - ) + f"database file {util.displayable_path(dbpath)} cannot not be" + f" opened: {db_error}" ) log.debug( - "library database: {0}\nlibrary directory: {1}", + "library database: {}\nlibrary directory: {}", util.displayable_path(lib.path), util.displayable_path(lib.directory), ) @@ -1782,7 +1751,7 @@ def main(args=None): _raw_main(args) except UserError as exc: message = exc.args[0] if exc.args else None - log.error("error: {0}", message) + log.error("error: {}", message) sys.exit(1) except util.HumanReadableError as exc: exc.log(log) @@ -1794,10 +1763,10 @@ def main(args=None): log.error("{}", exc) sys.exit(1) except confuse.ConfigError as exc: - log.error("configuration error: {0}", exc) + log.error("configuration error: {}", exc) sys.exit(1) except db_query.InvalidQueryError as exc: - log.error("invalid query: {0}", exc) + log.error("invalid query: {}", exc) sys.exit(1) except OSError as exc: if exc.errno == errno.EPIPE: @@ -1810,7 +1779,7 @@ def main(args=None): log.debug("{}", traceback.format_exc()) except db.DBAccessError as exc: log.error( - "database access error: {0}\n" + "database access error: {}\n" "the library file might have a permissions problem", exc, ) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 12a8d68750..911a5cfd3d 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -18,6 +18,7 @@ import os import re +import textwrap from collections import Counter from collections.abc import Sequence from itertools import chain @@ -112,15 +113,11 @@ def _parse_logfiles(logfiles): yield from _paths_from_logfile(syspath(normpath(logfile))) except ValueError as err: raise ui.UserError( - "malformed logfile {}: {}".format( - util.displayable_path(logfile), str(err) - ) + f"malformed logfile {util.displayable_path(logfile)}: {err}" ) from err except OSError as err: raise ui.UserError( - "unreadable logfile {}: {}".format( - util.displayable_path(logfile), str(err) - ) + f"unreadable logfile {util.displayable_path(logfile)}: {err}" ) from err @@ -132,13 +129,13 @@ def _print_keys(query): returned row, with indentation of 2 spaces. """ for row in query: - print_(" " * 2 + row["key"]) + print_(f" {row['key']}") def fields_func(lib, opts, args): def _print_rows(names): names.sort() - print_(" " + "\n ".join(names)) + print_(textwrap.indent("\n".join(names), " ")) print_("Item fields:") _print_rows(library.Item.all_keys()) @@ -148,13 +145,13 @@ def _print_rows(names): with lib.transaction() as tx: # The SQL uses the DISTINCT to get unique values from the query - unique_fields = "SELECT DISTINCT key FROM (%s)" + unique_fields = "SELECT DISTINCT key FROM ({})" print_("Item flexible attributes:") - _print_keys(tx.query(unique_fields % library.Item._flex_table)) + _print_keys(tx.query(unique_fields.format(library.Item._flex_table))) print_("Album flexible attributes:") - _print_keys(tx.query(unique_fields % library.Album._flex_table)) + _print_keys(tx.query(unique_fields.format(library.Album._flex_table))) fields_cmd = ui.Subcommand( @@ -213,10 +210,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}", + "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 +239,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 ), @@ -277,7 +274,7 @@ def dist_string(dist): """Formats a distance (a float) as a colorized similarity percentage string. """ - string = "{:.1f}%".format(((1 - dist) * 100)) + string = f"{(1 - dist) * 100:.1f}%" return dist_colorize(string, dist) @@ -295,7 +292,7 @@ def penalty_string(distance, limit=None): if limit and len(penalties) > limit: penalties = penalties[:limit] + ["..."] # Prefix penalty string with U+2260: Not Equal To - penalty_string = "\u2260 {}".format(", ".join(penalties)) + penalty_string = f"\u2260 {', '.join(penalties)}" return ui.colorize("changed", penalty_string) @@ -360,18 +357,18 @@ def show_match_header(self): # 'Match' line and similarity. print_( - self.indent_header + f"Match ({dist_string(self.match.distance)}):" + f"{self.indent_header}Match ({dist_string(self.match.distance)}):" ) if isinstance(self.match.info, autotag.hooks.AlbumInfo): # Matching an album - print that artist_album_str = ( - f"{self.match.info.artist}" + f" - {self.match.info.album}" + f"{self.match.info.artist} - {self.match.info.album}" ) else: # Matching a single track artist_album_str = ( - f"{self.match.info.artist}" + f" - {self.match.info.title}" + f"{self.match.info.artist} - {self.match.info.title}" ) print_( self.indent_header @@ -381,22 +378,23 @@ def show_match_header(self): # Penalties. penalties = penalty_string(self.match.distance) if penalties: - print_(self.indent_header + penalties) + print_(f"{self.indent_header}{penalties}") # Disambiguation. disambig = disambig_string(self.match.info) if disambig: - print_(self.indent_header + disambig) + print_(f"{self.indent_header}{disambig}") # Data URL. if self.match.info.data_url: url = ui.colorize("text_faint", f"{self.match.info.data_url}") - print_(self.indent_header + url) + print_(f"{self.indent_header}{url}") def show_match_details(self): """Print out the details of the match, including changes in album name and artist name. """ + changed_prefix = ui.colorize("changed", "\u2260") # Artist. artist_l, artist_r = self.cur_artist or "", self.match.info.artist if artist_r == VARIOUS_ARTISTS: @@ -406,7 +404,7 @@ def show_match_details(self): artist_l, artist_r = ui.colordiff(artist_l, artist_r) # Prefix with U+2260: Not Equal To left = { - "prefix": ui.colorize("changed", "\u2260") + " Artist: ", + "prefix": f"{changed_prefix} Artist: ", "contents": artist_l, "suffix": "", } @@ -414,7 +412,7 @@ def show_match_details(self): self.print_layout(self.indent_detail, left, right) else: - print_(self.indent_detail + "*", "Artist:", artist_r) + print_(f"{self.indent_detail}*", "Artist:", artist_r) if self.cur_album: # Album @@ -426,14 +424,14 @@ def show_match_details(self): album_l, album_r = ui.colordiff(album_l, album_r) # Prefix with U+2260: Not Equal To left = { - "prefix": ui.colorize("changed", "\u2260") + " Album: ", + "prefix": f"{changed_prefix} Album: ", "contents": album_l, "suffix": "", } right = {"prefix": "", "contents": album_r, "suffix": ""} self.print_layout(self.indent_detail, left, right) else: - print_(self.indent_detail + "*", "Album:", album_r) + print_(f"{self.indent_detail}*", "Album:", album_r) elif self.cur_title: # Title - for singletons title_l, title_r = self.cur_title or "", self.match.info.title @@ -441,14 +439,14 @@ def show_match_details(self): title_l, title_r = ui.colordiff(title_l, title_r) # Prefix with U+2260: Not Equal To left = { - "prefix": ui.colorize("changed", "\u2260") + " Title: ", + "prefix": f"{changed_prefix} Title: ", "contents": title_l, "suffix": "", } right = {"prefix": "", "contents": title_r, "suffix": ""} self.print_layout(self.indent_detail, left, right) else: - print_(self.indent_detail + "*", "Title:", title_r) + print_(f"{self.indent_detail}*", "Title:", title_r) def make_medium_info_line(self, track_info): """Construct a line with the current medium's info.""" @@ -490,7 +488,6 @@ def make_track_numbers(self, item, track_info): """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,10 +499,8 @@ def make_track_numbers(self, item, track_info): else: highlight_color = "text_faint" - cur_track = templ.format(cur_track) - new_track = templ.format(new_track) - lhs_track = ui.colorize(highlight_color, cur_track) - rhs_track = ui.colorize(highlight_color, new_track) + lhs_track = ui.colorize(highlight_color, f"(#{cur_track})") + rhs_track = ui.colorize(highlight_color, f"(#{new_track})") return lhs_track, rhs_track, changed @staticmethod @@ -575,9 +570,9 @@ def make_line(self, item, track_info): prefix = ui.colorize("changed", "\u2260 ") if changed else "* " lhs = { - "prefix": prefix + lhs_track + " ", + "prefix": f"{prefix}{lhs_track} ", "contents": lhs_title, - "suffix": " " + lhs_length, + "suffix": f" {lhs_length}", } rhs = {"prefix": "", "contents": "", "suffix": ""} if not changed: @@ -586,9 +581,9 @@ def make_line(self, item, track_info): else: # Construct a dictionary for the "changed to" side rhs = { - "prefix": rhs_track + " ", + "prefix": f"{rhs_track} ", "contents": rhs_title, - "suffix": " " + rhs_length, + "suffix": f" {rhs_length}", } return (lhs, rhs) @@ -681,7 +676,7 @@ def show_match_tracks(self): # Print tracks from previous medium self.print_tracklist(lines) lines = [] - print_(self.indent_detail + header) + print_(f"{self.indent_detail}{header}") # Save new medium details for future comparison. medium, disctitle = track_info.medium, track_info.disctitle @@ -697,11 +692,9 @@ def show_match_tracks(self): # Missing and unmatched tracks. if self.match.extra_tracks: print_( - "Missing tracks ({0}/{1} - {2:.1%}):".format( - len(self.match.extra_tracks), - len(self.match.info.tracks), - len(self.match.extra_tracks) / len(self.match.info.tracks), - ) + "Missing tracks" + f" ({len(self.match.extra_tracks)}/{len(self.match.info.tracks)} -" + f" {len(self.match.extra_tracks) / len(self.match.info.tracks):.1%}):" ) for track_info in self.match.extra_tracks: line = f" ! {track_info.title} (#{self.format_index(track_info)})" @@ -711,9 +704,9 @@ def show_match_tracks(self): 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(human_seconds_short(item.length)) + line += f" ({human_seconds_short(item.length)})" print_(ui.colorize("text_warning", line)) @@ -769,7 +762,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: @@ -789,10 +782,11 @@ def summarize_items(items, singleton): average_bitrate = sum([item.bitrate for item in items]) / len(items) total_duration = sum([item.length for item in items]) total_filesize = sum([item.filesize for item in items]) - summary_parts.append("{}kbps".format(int(average_bitrate / 1000))) + summary_parts.append(f"{int(average_bitrate / 1000)}kbps") if items[0].format == "FLAC": - sample_bits = "{}kHz/{} bit".format( - round(int(items[0].samplerate) / 1000, 1), items[0].bitdepth + sample_bits = ( + f"{round(int(items[0].samplerate) / 1000, 1)}kHz" + f"/{items[0].bitdepth} bit" ) summary_parts.append(sample_bits) summary_parts.append(human_seconds_short(total_duration)) @@ -885,7 +879,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" @@ -910,40 +904,38 @@ def choose_candidate( # Display list of candidates. print_("") print_( - 'Finding tags for {} "{} - {}".'.format( - "track" if singleton else "album", - item.artist if singleton else cur_artist, - item.title if singleton else cur_album, - ) + f"Finding tags for {'track' if singleton else 'album'}" + f'"{item.artist if singleton else cur_artist} -' + f' {item.title if singleton else cur_album}".' ) - print_(ui.indent(2) + "Candidates:") + print_(" 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) + dist = f"({(1 - match.distance) * 100:.1f}%)" distance = dist_colorize(dist, match.distance) - metadata = "{0} - {1}".format( - match.info.artist, - match.info.title if singleton else match.info.album, + metadata = ( + f"{match.info.artist} -" + f" {match.info.title if singleton else match.info.album}" ) if i == 0: metadata = dist_colorize(metadata, match.distance) else: metadata = ui.colorize("text_highlight_minor", metadata) line1 = [index, distance, metadata] - print_(ui.indent(2) + " ".join(line1)) + print_(f" {' '.join(line1)}") # Penalties. penalties = penalty_string(match.distance, 3) if penalties: - print_(ui.indent(13) + penalties) + print_(f"{' ' * 13}{penalties}") # Disambiguation disambig = disambig_string(match.info) if disambig: - print_(ui.indent(13) + disambig) + print_(f"{' ' * 13}{disambig}") # Ask the user for a choice. sel = ui.input_options(choice_opts, numrange=(1, len(candidates))) @@ -1015,7 +1007,7 @@ def manual_id(session, task): Input an ID, either for an album ("release") or a track ("recording"). """ - prompt = "Enter {} ID:".format("release" if task.is_album else "recording") + prompt = f"Enter {'release' if task.is_album else 'recording'} ID:" search_id = input_(prompt).strip() if task.is_album: @@ -1043,7 +1035,7 @@ def choose_match(self, task): 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])) @@ -1156,7 +1148,7 @@ def resolve_duplicate(self, task, found_duplicates): that's already in the library. """ log.warning( - "This {0} is already in the library!", + "This {} is already in the library!", ("album" if task.is_album else "item"), ) @@ -1217,8 +1209,8 @@ def resolve_duplicate(self, task, found_duplicates): def should_resume(self, path): return ui.input_yn( - "Import of the directory:\n{}\n" - "was interrupted. Resume (Y/n)?".format(displayable_path(path)) + f"Import of the directory:\n{displayable_path(path)}\n" + "was interrupted. Resume (Y/n)?" ) def _get_choices(self, task): @@ -1288,11 +1280,10 @@ def _get_choices(self, task): dup_choices = [c for c in all_choices if c.short == short] for c in dup_choices[1:]: log.warning( - "Prompt choice '{0}' removed due to conflict " - "with '{1}' (short letter: '{2}')", - c.long, - dup_choices[0].long, - c.short, + "Prompt choice '{0.long}' removed due to conflict " + "with '{1[0].long}' (short letter: '{0.short}')", + c, + dup_choices, ) extra_choices.remove(c) @@ -1317,7 +1308,8 @@ def import_files(lib, paths: list[bytes], query): loghandler = logging.FileHandler(logpath, encoding="utf-8") except OSError: raise ui.UserError( - f"Could not open log file for writing: {displayable_path(logpath)}" + "Could not open log file for writing:" + f" {displayable_path(logpath)}" ) else: loghandler = None @@ -1362,9 +1354,7 @@ def import_func(lib, opts, args: list[str]): for path in byte_paths: if not os.path.exists(syspath(normpath(path))): raise ui.UserError( - "no such file or directory: {}".format( - displayable_path(path) - ) + f"no such file or directory: {displayable_path(path)}" ) # Check the directories from the logfiles, but don't throw an error in @@ -1374,9 +1364,7 @@ def import_func(lib, opts, args: list[str]): for path in paths_from_logfiles: if not os.path.exists(syspath(normpath(path))): log.warning( - "No such file or directory: {}".format( - displayable_path(path) - ) + "No such file or directory: {}", displayable_path(path) ) continue @@ -1650,9 +1638,8 @@ def update_items(lib, query, album, move, pretend, fields, exclude_fields=None): # Did the item change since last checked? if item.current_mtime() <= item.mtime: log.debug( - "skipping {0} because mtime is up to date ({1})", - displayable_path(item.path), - item.mtime, + "skipping {0.filepath} because mtime is up to date ({0.mtime})", + item, ) continue @@ -1660,9 +1647,7 @@ def update_items(lib, query, album, move, pretend, fields, exclude_fields=None): try: item.read() except library.ReadError as exc: - log.error( - "error reading {0}: {1}", displayable_path(item.path), exc - ) + log.error("error reading {.filepath}: {}", item, exc) continue # Special-case album artist when it matches track artist. (Hacky @@ -1703,7 +1688,7 @@ def update_items(lib, query, album, move, pretend, fields, exclude_fields=None): continue album = lib.get_album(album_id) if not album: # Empty albums have already been removed. - log.debug("emptied album {0}", album_id) + log.debug("emptied album {}", album_id) continue first_item = album.items().get() @@ -1714,7 +1699,7 @@ def update_items(lib, query, album, move, pretend, fields, exclude_fields=None): # Move album art (and any inconsistent items). if move and lib.directory in ancestry(first_item.path): - log.debug("moving album {0}", album_id) + log.debug("moving album {}", album_id) # Manually moving and storing the album. items = list(album.items()) @@ -1808,7 +1793,7 @@ def remove_items(lib, query, album, delete, force): if not force: # Prepare confirmation with user. album_str = ( - " in {} album{}".format(len(albums), "s" if len(albums) > 1 else "") + f" in {len(albums)} album{'s' if len(albums) > 1 else ''}" if album else "" ) @@ -1816,14 +1801,17 @@ def remove_items(lib, query, album, delete, force): if delete: fmt = "$path - $title" prompt = "Really DELETE" - prompt_all = "Really DELETE {} file{}{}".format( - len(items), "s" if len(items) > 1 else "", album_str + prompt_all = ( + "Really DELETE" + f" {len(items)} file{'s' if len(items) > 1 else ''}{album_str}" ) else: fmt = "" prompt = "Really remove from the library?" - prompt_all = "Really remove {} item{}{} from the library?".format( - len(items), "s" if len(items) > 1 else "", album_str + prompt_all = ( + "Really remove" + f" {len(items)} item{'s' if len(items) > 1 else ''}{album_str}" + " from the library?" ) # Helpers for printing affected items @@ -1892,7 +1880,7 @@ def show_stats(lib, query, exact): try: total_size += os.path.getsize(syspath(item.path)) except OSError as exc: - log.info("could not get size of {}: {}", item.path, exc) + log.info("could not get size of {.path}: {}", item, exc) else: total_size += int(item.length * item.bitrate / 8) total_time += item.length @@ -1902,27 +1890,17 @@ def show_stats(lib, query, exact): if item.album_id: albums.add(item.album_id) - size_str = "" + human_bytes(total_size) + size_str = human_bytes(total_size) if exact: size_str += f" ({total_size} bytes)" - print_( - """Tracks: {} -Total time: {}{} -{}: {} -Artists: {} -Albums: {} -Album artists: {}""".format( - total_items, - human_seconds(total_time), - f" ({total_time:.2f} seconds)" if exact else "", - "Total size" if exact else "Approximate total size", - size_str, - len(artists), - len(albums), - len(album_artists), - ), - ) + print_(f"""Tracks: {total_items} +Total time: {human_seconds(total_time)} +{f" ({total_time:.2f} seconds)" if exact else ""} +{"Total size" if exact else "Approximate total size"}: {size_str} +Artists: {len(artists)} +Albums: {len(albums)} +Album artists: {len(album_artists)}""") def stats_func(lib, opts, args): @@ -1943,7 +1921,7 @@ def stats_func(lib, opts, args): def show_version(lib, opts, args): - print_("beets version %s" % beets.__version__) + print_(f"beets version {beets.__version__}") print_(f"Python version {python_version()}") # Show plugins. names = sorted(p.name for p in plugins.find_plugins()) @@ -1977,7 +1955,7 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm, inherit): # Apply changes *temporarily*, preview them, and collect modified # objects. - print_("Modifying {} {}s.".format(len(objs), "album" if album else "item")) + print_(f"Modifying {len(objs)} {'album' if album else 'item'}s.") changed = [] templates = { key: functemplate.template(value) for key, value in mods.items() @@ -2007,7 +1985,7 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm, inherit): extra = "" changed = ui.input_select_objects( - "Really modify%s" % extra, + f"Really modify{extra}", changed, lambda o: print_and_modify(o, mods, dels), ) @@ -2159,7 +2137,7 @@ def isalbummoved(album): act = "copy" if copy else "move" entity = "album" if album else "item" log.info( - "{0} {1} {2}{3}{4}.", + "{} {} {}{}{}.", action, len(objs), entity, @@ -2185,7 +2163,7 @@ def isalbummoved(album): else: if confirm: objs = ui.input_select_objects( - "Really %s" % act, + f"Really {act}", objs, lambda o: show_path_changes( [(o.path, o.destination(basedir=dest))] @@ -2193,7 +2171,7 @@ def isalbummoved(album): ) for obj in objs: - log.debug("moving: {0}", util.displayable_path(obj.path)) + log.debug("moving: {.filepath}", obj) if export: # Copy without affecting the database. @@ -2213,9 +2191,7 @@ def move_func(lib, opts, args): if dest is not None: dest = normpath(dest) if not os.path.isdir(syspath(dest)): - raise ui.UserError( - "no such directory: {}".format(displayable_path(dest)) - ) + raise ui.UserError(f"no such directory: {displayable_path(dest)}") move_items( lib, @@ -2278,16 +2254,14 @@ def write_items(lib, query, pretend, force): for item in items: # Item deleted? if not os.path.exists(syspath(item.path)): - log.info("missing file: {0}", util.displayable_path(item.path)) + log.info("missing file: {.filepath}", item) continue # Get an Item object reflecting the "clean" (on-disk) state. try: clean_item = library.Item.from_path(item.path) except library.ReadError as exc: - log.error( - "error reading {0}: {1}", displayable_path(item.path), exc - ) + log.error("error reading {.filepath}: {}", item, exc) continue # Check for and display changes. @@ -2480,30 +2454,27 @@ def completion_script(commands): yield "_beet() {\n" # Command names - yield " local commands='%s'\n" % " ".join(command_names) + yield f" local commands={' '.join(command_names)!r}\n" yield "\n" # Command aliases - yield " local aliases='%s'\n" % " ".join(aliases.keys()) + yield f" local aliases={' '.join(aliases.keys())!r}\n" for alias, cmd in aliases.items(): - yield " local alias__{}={}\n".format(alias.replace("-", "_"), cmd) + yield f" local alias__{alias.replace('-', '_')}={cmd}\n" yield "\n" # Fields - yield " fields='%s'\n" % " ".join( - set( - list(library.Item._fields.keys()) - + list(library.Album._fields.keys()) - ) - ) + fields = library.Item._fields.keys() | library.Album._fields.keys() + yield f" fields={' '.join(fields)!r}\n" # Command options for cmd, opts in options.items(): for option_type, option_list in opts.items(): if option_list: option_list = " ".join(option_list) - yield " local {}__{}='{}'\n".format( - option_type, cmd.replace("-", "_"), option_list + yield ( + " local" + f" {option_type}__{cmd.replace('-', '_')}='{option_list}'\n" ) yield " _beet_dispatch\n" diff --git a/beets/util/__init__.py b/beets/util/__init__.py index e2f7f46bda..f895a60ee8 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -112,7 +112,7 @@ def _reasonstr(self): elif hasattr(self.reason, "strerror"): # i.e., EnvironmentError return self.reason.strerror else: - return '"{}"'.format(str(self.reason)) + return f'"{self.reason}"' def get_message(self): """Create the human-readable description of the error, sans @@ -126,7 +126,7 @@ def log(self, logger): """ if self.tb: logger.debug(self.tb) - logger.error("{0}: {1}", self.error_kind, self.args[0]) + logger.error("{0.error_kind}: {0.args[0]}", self) class FilesystemError(HumanReadableError): @@ -142,18 +142,16 @@ def __init__(self, reason, verb, paths, tb=None): def get_message(self): # Use a nicer English phrasing for some specific verbs. if self.verb in ("move", "copy", "rename"): - clause = "while {} {} to {}".format( - self._gerund(), - displayable_path(self.paths[0]), - displayable_path(self.paths[1]), + clause = ( + f"while {self._gerund()} {displayable_path(self.paths[0])} to" + f" {displayable_path(self.paths[1])}" ) elif self.verb in ("delete", "write", "create", "read"): - clause = "while {} {}".format( - self._gerund(), displayable_path(self.paths[0]) - ) + clause = f"while {self._gerund()} {displayable_path(self.paths[0])}" else: - clause = "during {} of paths {}".format( - self.verb, ", ".join(displayable_path(p) for p in self.paths) + clause = ( + f"during {self.verb} of paths" + f" {', '.join(displayable_path(p) for p in self.paths)}" ) return f"{self._reasonstr()} {clause}" @@ -223,12 +221,12 @@ def sorted_walk( # Get all the directories and files at this level. try: contents = os.listdir(syspath(bytes_path)) - except OSError as exc: + except OSError: if logger: logger.warning( - "could not list directory {}: {}".format( - displayable_path(bytes_path), exc.strerror - ) + "could not list directory {}", + displayable_path(bytes_path), + exc_info=True, ) return dirs = [] @@ -436,8 +434,8 @@ def syspath(path: PathLike, prefix: bool = True) -> str: if prefix and not str_path.startswith(WINDOWS_MAGIC_PREFIX): if str_path.startswith("\\\\"): # UNC path. Final path should look like \\?\UNC\... - str_path = "UNC" + str_path[1:] - str_path = WINDOWS_MAGIC_PREFIX + str_path + str_path = f"UNC{str_path[1:]}" + str_path = f"{WINDOWS_MAGIC_PREFIX}{str_path}" return str_path @@ -509,8 +507,8 @@ def move(path: bytes, dest: bytes, replace: bool = False): basename = os.path.basename(bytestring_path(dest)) dirname = os.path.dirname(bytestring_path(dest)) tmp = tempfile.NamedTemporaryFile( - suffix=syspath(b".beets", prefix=False), - prefix=syspath(b"." + basename + b".", prefix=False), + suffix=".beets", + prefix=f".{os.fsdecode(basename)}.", dir=syspath(dirname), delete=False, ) @@ -719,7 +717,7 @@ def truncate_path(str_path: str) -> str: path = Path(str_path) parent_parts = [truncate_str(p, max_length) for p in path.parts[:-1]] stem = truncate_str(path.stem, max_length - len(path.suffix)) - return str(Path(*parent_parts, stem)) + path.suffix + return f"{Path(*parent_parts, stem)}{path.suffix}" def _legalize_stage( diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index fe67c506e0..5ecde51407 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -54,7 +54,7 @@ def resize_url(url: str, maxwidth: int, quality: int = 0) -> str: if quality > 0: params["q"] = quality - return "{}?{}".format(PROXY_URL, urlencode(params)) + return f"{PROXY_URL}?{urlencode(params)}" class LocalBackendNotAvailableError(Exception): @@ -255,7 +255,7 @@ def resize( path_out = get_temp_filename(__name__, "resize_IM_", path_in) log.debug( - "artresizer: ImageMagick resizing {0} to {1}", + "artresizer: ImageMagick resizing {} to {}", displayable_path(path_in), displayable_path(path_out), ) @@ -287,7 +287,7 @@ def resize( util.command_output(cmd) except subprocess.CalledProcessError: log.warning( - "artresizer: IM convert failed for {0}", + "artresizer: IM convert failed for {}", displayable_path(path_in), ) return path_in @@ -306,9 +306,9 @@ def get_size(self, path_in: bytes) -> tuple[int, int] | None: except subprocess.CalledProcessError as exc: log.warning("ImageMagick size query failed") log.debug( - "`convert` exited with (status {}) when " + "`convert` exited with (status {.returncode}) when " "getting size with command {}:\n{}", - exc.returncode, + exc, cmd, exc.output.strip(), ) @@ -441,8 +441,8 @@ def compare( convert_proc.wait() if convert_proc.returncode: log.debug( - "ImageMagick convert failed with status {}: {!r}", - convert_proc.returncode, + "ImageMagick convert failed with status {.returncode}: {!r}", + convert_proc, convert_stderr, ) return None @@ -452,7 +452,7 @@ def compare( if compare_proc.returncode: if compare_proc.returncode != 1: log.debug( - "ImageMagick compare failed: {0}, {1}", + "ImageMagick compare failed: {}, {}", displayable_path(im2), displayable_path(im1), ) @@ -472,7 +472,7 @@ def compare( log.debug("IM output is not a number: {0!r}", out_str) return None - log.debug("ImageMagick compare score: {0}", phash_diff) + log.debug("ImageMagick compare score: {}", phash_diff) return phash_diff <= compare_threshold @property @@ -523,7 +523,7 @@ def resize( from PIL import Image log.debug( - "artresizer: PIL resizing {0} to {1}", + "artresizer: PIL resizing {} to {}", displayable_path(path_in), displayable_path(path_out), ) @@ -552,7 +552,7 @@ def resize( for i in range(5): # 5 attempts is an arbitrary choice filesize = os.stat(syspath(path_out)).st_size - log.debug("PIL Pass {0} : Output size: {1}B", i, filesize) + log.debug("PIL Pass {} : Output size: {}B", i, filesize) if filesize <= max_filesize: return path_out # The relationship between filesize & quality will be @@ -569,7 +569,7 @@ def resize( progressive=False, ) log.warning( - "PIL Failed to resize file to below {0}B", max_filesize + "PIL Failed to resize file to below {}B", max_filesize ) return path_out @@ -577,7 +577,7 @@ def resize( return path_out except OSError: log.error( - "PIL cannot create thumbnail for '{0}'", + "PIL cannot create thumbnail for '{}'", displayable_path(path_in), ) return path_in @@ -696,7 +696,7 @@ def __init__(self) -> None: for backend_cls in BACKEND_CLASSES: try: self.local_method = backend_cls() - log.debug(f"artresizer: method is {self.local_method.NAME}") + log.debug("artresizer: method is {.local_method.NAME}", self) break except LocalBackendNotAvailableError: continue diff --git a/beets/util/bluelet.py b/beets/util/bluelet.py index b81b389e0d..3f3a88b1ee 100644 --- a/beets/util/bluelet.py +++ b/beets/util/bluelet.py @@ -559,7 +559,7 @@ def spawn(coro): and child coroutines run concurrently. """ if not isinstance(coro, types.GeneratorType): - raise ValueError("%s is not a coroutine" % coro) + raise ValueError(f"{coro} is not a coroutine") return SpawnEvent(coro) @@ -569,7 +569,7 @@ def call(coro): returns a value using end(), then this event returns that value. """ if not isinstance(coro, types.GeneratorType): - raise ValueError("%s is not a coroutine" % coro) + raise ValueError(f"{coro} is not a coroutine") return DelegationEvent(coro) diff --git a/beets/util/functemplate.py b/beets/util/functemplate.py index b0daefac28..5d85530a12 100644 --- a/beets/util/functemplate.py +++ b/beets/util/functemplate.py @@ -136,7 +136,7 @@ def __init__(self, ident, original): self.original = original def __repr__(self): - return "Symbol(%s)" % repr(self.ident) + return f"Symbol({self.ident!r})" def evaluate(self, env): """Evaluate the symbol in the environment, returning a Unicode @@ -152,7 +152,7 @@ def evaluate(self, env): def translate(self): """Compile the variable lookup.""" ident = self.ident - expr = ex_rvalue(VARIABLE_PREFIX + ident) + expr = ex_rvalue(f"{VARIABLE_PREFIX}{ident}") return [expr], {ident}, set() @@ -165,9 +165,7 @@ def __init__(self, ident, args, original): 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 @@ -180,7 +178,7 @@ def evaluate(self, env): except Exception as exc: # Function raised exception! Maybe inlining the name of # the exception will help debug. - return "<%s>" % str(exc) + return f"<{exc}>" return str(out) else: return self.original @@ -213,7 +211,7 @@ def translate(self): ) ) - subexpr_call = ex_call(FUNCTION_PREFIX + self.ident, arg_exprs) + subexpr_call = ex_call(f"{FUNCTION_PREFIX}{self.ident}", arg_exprs) return [subexpr_call], varnames, funcnames @@ -226,7 +224,7 @@ def __init__(self, parts): self.parts = parts def __repr__(self): - return "Expression(%s)" % (repr(self.parts)) + return f"Expression({self.parts!r})" def evaluate(self, env): """Evaluate the entire expression in the environment, returning @@ -298,9 +296,6 @@ def __init__(self, string, in_argument=False): GROUP_CLOSE, ESCAPE_CHAR, ) - special_char_re = re.compile( - r"[%s]|\Z" % "".join(re.escape(c) for c in special_chars) - ) escapable_chars = (SYMBOL_DELIM, FUNC_DELIM, GROUP_CLOSE, ARG_SEP) terminator_chars = (GROUP_CLOSE,) @@ -312,24 +307,18 @@ def parse_expression(self): """ # Append comma (ARG_SEP) to the list of special characters only when # parsing function arguments. - extra_special_chars = () - special_char_re = self.special_char_re - if self.in_argument: - extra_special_chars = (ARG_SEP,) - special_char_re = re.compile( - r"[%s]|\Z" - % "".join( - re.escape(c) - for c in self.special_chars + extra_special_chars - ) - ) + extra_special_chars = (ARG_SEP,) if self.in_argument else () + special_chars = (*self.special_chars, *extra_special_chars) + special_char_re = re.compile( + rf"[{''.join(map(re.escape, special_chars))}]|\Z" + ) text_parts = [] while self.pos < len(self.string): char = self.string[self.pos] - if char not in self.special_chars + extra_special_chars: + if char not in special_chars: # A non-special character. Skip to the next special # character, treating the interstice as literal text. next_pos = ( @@ -566,9 +555,9 @@ def translate(self): argnames = [] for varname in varnames: - argnames.append(VARIABLE_PREFIX + varname) + argnames.append(f"{VARIABLE_PREFIX}{varname}") for funcname in funcnames: - argnames.append(FUNCTION_PREFIX + funcname) + argnames.append(f"{FUNCTION_PREFIX}{funcname}") func = compile_func( argnames, @@ -578,9 +567,9 @@ def translate(self): def wrapper_func(values={}, functions={}): args = {} for varname in varnames: - args[VARIABLE_PREFIX + varname] = values[varname] + args[f"{VARIABLE_PREFIX}{varname}"] = values[varname] for funcname in funcnames: - args[FUNCTION_PREFIX + funcname] = functions[funcname] + args[f"{FUNCTION_PREFIX}{funcname}"] = functions[funcname] parts = func(**args) return "".join(parts) diff --git a/beets/util/id_extractors.py b/beets/util/id_extractors.py index 6cdb787d1e..f66f1690f6 100644 --- a/beets/util/id_extractors.py +++ b/beets/util/id_extractors.py @@ -58,7 +58,8 @@ def extract_release_id(source: str, id_: str) -> str | None: source_pattern = PATTERN_BY_SOURCE[source.lower()] except KeyError: log.debug( - f"Unknown source '{source}' for ID extraction. Returning id/url as-is." + "Unknown source '{}' for ID extraction. Returning id/url as-is.", + source, ) return id_ diff --git a/beets/util/units.py b/beets/util/units.py index d07d425462..f5fcb743b3 100644 --- a/beets/util/units.py +++ b/beets/util/units.py @@ -19,7 +19,7 @@ def human_seconds_short(interval): string. """ interval = int(interval) - return "%i:%02i" % (interval // 60, interval % 60) + return f"{interval // 60}:{interval % 60:02d}" def human_bytes(size): diff --git a/beetsplug/absubmit.py b/beetsplug/absubmit.py index c02a1c9236..62a2484823 100644 --- a/beetsplug/absubmit.py +++ b/beetsplug/absubmit.py @@ -42,9 +42,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): @@ -63,9 +61,7 @@ def __init__(self): # 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 @@ -101,8 +97,8 @@ def __init__(self): "with an HTTP scheme" ) elif base_url[-1] != "/": - base_url = base_url + "/" - self.url = base_url + "{mbid}/low-level" + base_url = f"{base_url}/" + self.url = f"{base_url}{{mbid}}/low-level" def commands(self): cmd = ui.Subcommand( @@ -122,8 +118,10 @@ def commands(self): dest="pretend_fetch", action="store_true", default=False, - help="pretend to perform action, but show \ -only files which would be processed", + help=( + "pretend to perform action, but show only files which would be" + " processed" + ), ) cmd.func = self.command return [cmd] diff --git a/beetsplug/acousticbrainz.py b/beetsplug/acousticbrainz.py index 56ac0f6c55..92a1976a18 100644 --- a/beetsplug/acousticbrainz.py +++ b/beetsplug/acousticbrainz.py @@ -97,7 +97,7 @@ def __init__(self): "with an HTTP scheme" ) elif self.base_url[-1] != "/": - self.base_url = self.base_url + "/" + self.base_url = f"{self.base_url}/" if self.config["auto"]: self.register_listener("import_task_files", self.import_task_files) @@ -153,7 +153,7 @@ def _get_data(self, mbid): try: data.update(res.json()) except ValueError: - self._log.debug("Invalid Response: {}", res.text) + self._log.debug("Invalid Response: {.text}", res) return {} return data @@ -300,4 +300,4 @@ def _data_to_scheme_child(self, subdata, subscheme, composites): def _generate_urls(base_url, mbid): """Generates AcousticBrainz end point urls for given `mbid`.""" for level in LEVELS: - yield base_url + mbid + level + yield f"{base_url}{mbid}{level}" diff --git a/beetsplug/aura.py b/beetsplug/aura.py index 53458d7ee8..7b75f31e57 100644 --- a/beetsplug/aura.py +++ b/beetsplug/aura.py @@ -236,14 +236,14 @@ def paginate(self, collection): # Not the last page so work out links.next url if not self.args: # No existing arguments, so current page is 0 - next_url = request.url + "?page=1" + next_url = f"{request.url}?page=1" elif not self.args.get("page", None): # No existing page argument, so add one to the end - next_url = request.url + "&page=1" + next_url = f"{request.url}&page=1" 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 = [ @@ -427,9 +427,7 @@ def single_resource(self, track_id): 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) @@ -513,9 +511,7 @@ def single_resource(self, album_id): 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) @@ -600,9 +596,7 @@ def single_resource(self, artist_id): 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) @@ -703,7 +697,7 @@ def get_resource_object(lib: Library, image_id): relationships = {} # Split id into [parent_type, parent_id, filename] id_split = image_id.split("-") - relationships[id_split[0] + "s"] = { + relationships[f"{id_split[0]}s"] = { "data": [{"type": id_split[0], "id": id_split[1]}] } @@ -727,9 +721,7 @@ def single_resource(self, image_id): 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) @@ -775,9 +767,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) @@ -785,9 +775,8 @@ 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] @@ -795,10 +784,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 @@ -810,10 +797,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( @@ -896,9 +881,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 a/beetsplug/badfiles.py b/beetsplug/badfiles.py index 0511d960d1..070008be80 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -110,9 +110,7 @@ def check_item(self, item): self._log.debug("checking path: {}", dpath) if not os.path.exists(item.path): ui.print_( - "{}: file does not exist".format( - ui.colorize("text_error", dpath) - ) + f"{ui.colorize('text_error', dpath)}: file does not exist" ) # Run the checker against the file if one is found @@ -129,37 +127,32 @@ def check_item(self, item): except CheckerCommandError as e: if e.errno == errno.ENOENT: self._log.error( - "command not found: {} when validating file: {}", - e.checker, - e.path, + "command not found: {0.checker} when validating file: {0.path}", + e, ) else: - self._log.error("error invoking {}: {}", e.checker, e.msg) + self._log.error("error invoking {0.checker}: {0.msg}", e) return [] error_lines = [] if status > 0: error_lines.append( - "{}: checker exited with status {}".format( - ui.colorize("text_error", dpath), status - ) + f"{ui.colorize('text_error', dpath)}: checker exited with" + f" status {status}" ) for line in output: error_lines.append(f" {line}") elif errors > 0: error_lines.append( - "{}: checker found {} errors or warnings".format( - ui.colorize("text_warning", dpath), errors - ) + f"{ui.colorize('text_warning', dpath)}: checker found" + f" {status} errors or warnings" ) for line in output: error_lines.append(f" {line}") elif self.verbose: - error_lines.append( - "{}: ok".format(ui.colorize("text_success", dpath)) - ) + error_lines.append(f"{ui.colorize('text_success', dpath)}: ok") return error_lines @@ -180,9 +173,8 @@ def on_import_task_start(self, task, session): def on_import_task_before_choice(self, task, session): if hasattr(task, "_badfiles_checks_failed"): ui.print_( - "{} one or more files failed checks:".format( - ui.colorize("text_warning", "BAD") - ) + f"{ui.colorize('text_warning', 'BAD')} one or more files failed" + " checks:" ) for error in task._badfiles_checks_failed: for error_line in error: diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index 16e0dc896e..fa681ce6a6 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -110,7 +110,7 @@ def get_access_token(self, auth_data: str) -> tuple[str, str]: :returns: OAuth resource owner key and secret as unicode """ self.api.parse_authorization_response( - "https://beets.io/auth?" + auth_data + f"https://beets.io/auth?{auth_data}" ) access_data = self.api.fetch_access_token( self._make_url("/identity/1/oauth/access-token") @@ -200,8 +200,8 @@ def get_track(self, beatport_id: str) -> BeatportTrack: def _make_url(self, endpoint: str) -> str: """Get complete URL for a given API endpoint.""" if not endpoint.startswith("/"): - endpoint = "/" + endpoint - return self._api_base + endpoint + endpoint = f"/{endpoint}" + return f"{self._api_base}{endpoint}" def _get(self, endpoint: str, **kwargs) -> list[JSONDict]: """Perform a GET request on a given API endpoint. @@ -212,14 +212,10 @@ def _get(self, endpoint: str, **kwargs) -> list[JSONDict]: 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"] @@ -275,15 +271,14 @@ def __init__(self, data: JSONDict): self.genre = data.get("genre") 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']}" ) def __str__(self) -> str: - return "".format( - self.artists_str(), - self.name, - self.catalog_number, + return ( + "" ) @@ -311,9 +306,7 @@ def __init__(self, data: JSONDict): 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")) @@ -373,7 +366,7 @@ def authenticate(self, c_key: str, c_secret: str) -> tuple[str, str]: try: url = auth_client.get_authorize_url() except AUTH_ERRORS as e: - self._log.debug("authentication error: {0}", e) + self._log.debug("authentication error: {}", e) raise beets.ui.UserError("communication with Beatport failed") beets.ui.print_("To authenticate with Beatport, visit:") @@ -384,11 +377,11 @@ def authenticate(self, c_key: str, c_secret: str) -> tuple[str, str]: try: token, secret = auth_client.get_access_token(data) except AUTH_ERRORS as e: - self._log.debug("authentication error: {0}", e) + self._log.debug("authentication error: {}", e) raise beets.ui.UserError("Beatport token request failed") # Save the token for later use. - self._log.debug("Beatport token {0}, secret {1}", token, secret) + self._log.debug("Beatport token {}, secret {}", token, secret) with open(self._tokenfile(), "w") as f: json.dump({"token": token, "secret": secret}, f) @@ -412,7 +405,7 @@ def candidates( try: yield from self._get_releases(query) except BeatportAPIError as e: - self._log.debug("API Error: {0} (query: {1})", e, query) + self._log.debug("API Error: {} (query: {})", e, query) return def item_candidates( @@ -422,14 +415,14 @@ def item_candidates( try: return self._get_tracks(query) except BeatportAPIError as e: - self._log.debug("API Error: {0} (query: {1})", e, query) + self._log.debug("API Error: {} (query: {})", e, query) return [] def album_for_id(self, album_id: str): """Fetches a release by its Beatport ID and returns an AlbumInfo object or None if the query is not a valid ID or release is not found. """ - self._log.debug("Searching for release {0}", album_id) + self._log.debug("Searching for release {}", album_id) if not (release_id := self._extract_id(album_id)): self._log.debug("Not a valid Beatport release ID.") @@ -444,7 +437,7 @@ def track_for_id(self, track_id: str): """Fetches a track by its Beatport ID and returns a TrackInfo object or None if the track is not a valid Beatport ID or track is not found. """ - self._log.debug("Searching for track {0}", track_id) + self._log.debug("Searching for track {}", track_id) # TODO: move to extractor match = re.search(r"(^|beatport\.com/track/.+/)(\d+)$", track_id) if not match: diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index a2ad2835ce..aa70131509 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -52,7 +52,7 @@ PROTOCOL_VERSION = "0.16.0" BUFSIZE = 1024 -HELLO = "OK MPD %s" % PROTOCOL_VERSION +HELLO = f"OK MPD {PROTOCOL_VERSION}" CLIST_BEGIN = "command_list_begin" CLIST_VERBOSE_BEGIN = "command_list_ok_begin" CLIST_END = "command_list_end" @@ -282,7 +282,7 @@ def _ctrl_send(self, message): if not self.ctrl_sock: self.ctrl_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.ctrl_sock.connect((self.ctrl_host, self.ctrl_port)) - self.ctrl_sock.sendall((message + "\n").encode("utf-8")) + self.ctrl_sock.sendall((f"{message}\n").encode("utf-8")) def _send_event(self, event): """Notify subscribed connections of an event.""" @@ -376,13 +376,13 @@ def cmd_commands(self, conn): if self.password and not conn.authenticated: # Not authenticated. Show limited list of commands. for cmd in SAFE_COMMANDS: - yield "command: " + cmd + yield f"command: {cmd}" else: # Authenticated. Show all commands. for func in dir(self): if func.startswith("cmd_"): - yield "command: " + func[4:] + yield f"command: {func[4:]}" def cmd_notcommands(self, conn): """Lists all unavailable commands.""" @@ -392,7 +392,7 @@ def cmd_notcommands(self, conn): if func.startswith("cmd_"): cmd = func[4:] if cmd not in SAFE_COMMANDS: - yield "command: " + cmd + yield f"command: {cmd}" else: # Authenticated. No commands are unavailable. @@ -406,22 +406,22 @@ def cmd_status(self, conn): playlist, playlistlength, and xfade. """ yield ( - "repeat: " + str(int(self.repeat)), - "random: " + str(int(self.random)), - "consume: " + str(int(self.consume)), - "single: " + str(int(self.single)), - "playlist: " + str(self.playlist_version), - "playlistlength: " + str(len(self.playlist)), - "mixrampdb: " + str(self.mixrampdb), + f"repeat: {int(self.repeat)}", + f"random: {int(self.random)}", + f"consume: {int(self.consume)}", + f"single: {int(self.single)}", + f"playlist: {self.playlist_version}", + f"playlistlength: {len(self.playlist)}", + f"mixrampdb: {self.mixrampdb}", ) if self.volume > 0: - yield "volume: " + str(self.volume) + yield f"volume: {self.volume}" if not math.isnan(self.mixrampdelay): - yield "mixrampdelay: " + str(self.mixrampdelay) + yield f"mixrampdelay: {self.mixrampdelay}" if self.crossfade > 0: - yield "xfade: " + str(self.crossfade) + yield f"xfade: {self.crossfade}" if self.current_index == -1: state = "stop" @@ -429,20 +429,20 @@ def cmd_status(self, conn): state = "pause" else: state = "play" - yield "state: " + state + yield f"state: {state}" if self.current_index != -1: # i.e., paused or playing current_id = self._item_id(self.playlist[self.current_index]) - yield "song: " + str(self.current_index) - yield "songid: " + str(current_id) + yield f"song: {self.current_index}" + yield f"songid: {current_id}" if len(self.playlist) > self.current_index + 1: # If there's a next song, report its index too. next_id = self._item_id(self.playlist[self.current_index + 1]) - yield "nextsong: " + str(self.current_index + 1) - yield "nextsongid: " + str(next_id) + yield f"nextsong: {self.current_index + 1}" + yield f"nextsongid: {next_id}" if self.error: - yield "error: " + self.error + yield f"error: {self.error}" def cmd_clearerror(self, conn): """Removes the persistent error state of the server. This @@ -522,7 +522,7 @@ def cmd_replay_gain_mode(self, conn, mode): def cmd_replay_gain_status(self, conn): """Get the replaygain mode.""" - yield "replay_gain_mode: " + str(self.replay_gain_mode) + yield f"replay_gain_mode: {self.replay_gain_mode}" def cmd_clear(self, conn): """Clear the playlist.""" @@ -643,8 +643,8 @@ def cmd_plchangesposid(self, conn, version): Also a dummy implementation. """ for idx, track in enumerate(self.playlist): - yield "cpos: " + str(idx) - yield "Id: " + str(track.id) + yield f"cpos: {idx}" + yield f"Id: {track.id}" def cmd_currentsong(self, conn): """Sends information about the currently-playing song.""" @@ -759,11 +759,11 @@ def __init__(self, server, sock): """Create a new connection for the accepted socket `client`.""" self.server = server self.sock = sock - self.address = "{}:{}".format(*sock.sock.getpeername()) + self.address = ":".join(map(str, sock.sock.getpeername())) def debug(self, message, kind=" "): """Log a debug message about this connection.""" - self.server._log.debug("{}[{}]: {}", kind, self.address, message) + self.server._log.debug("{}[{.address}]: {}", kind, self, message) def run(self): pass @@ -899,9 +899,7 @@ def run(self): return except BPDIdleError 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()) @@ -913,7 +911,7 @@ def __init__(self, server, sock): super().__init__(server, sock) def debug(self, message, kind=" "): - self.server._log.debug("CTRL {}[{}]: {}", kind, self.address, message) + self.server._log.debug("CTRL {}[{.address}]: {}", kind, self, message) def run(self): """Listen for control commands and delegate to `ctrl_*` methods.""" @@ -933,7 +931,7 @@ def run(self): 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()] @@ -992,7 +990,7 @@ def delegate(self, prefix, target, extra_args=0): of arguments. """ # Attempt to get correct command function. - func_name = prefix + self.name + func_name = f"{prefix}{self.name}" if not hasattr(target, func_name): raise AttributeError(f'unknown command "{self.name}"') func = getattr(target, func_name) @@ -1011,7 +1009,7 @@ def delegate(self, prefix, target, extra_args=0): # 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, ) @@ -1110,10 +1108,8 @@ def __init__(self, library, host, port, password, ctrl_port, log): 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("Server ready and listening on {}:{}", host, port) + log.debug("Listening for control signals on {}:{}", host, ctrl_port) def run(self): self.player.run() @@ -1128,23 +1124,21 @@ def play_finished(self): def _item_info(self, item): info_lines = [ - "file: " + as_string(item.destination(relative_to_libdir=True)), - "Time: " + str(int(item.length)), - "duration: " + f"{item.length:.3f}", - "Id: " + str(item.id), + f"file: {as_string(item.destination(relative_to_libdir=True))}", + f"Time: {int(item.length)}", + "duration: {item.length:.3f}", + f"Id: {item.id}", ] try: pos = self._id_to_index(item.id) - info_lines.append("Pos: " + str(pos)) + info_lines.append(f"Pos: {pos}") except ArgumentNotFoundError: # Don't include position if not in playlist. 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)}") return info_lines @@ -1207,7 +1201,7 @@ def _resolve_path(self, path): def _path_join(self, p1, p2): """Smashes together two BPD paths.""" - out = p1 + "/" + p2 + out = f"{p1}/{p2}" return out.replace("//", "/").replace("//", "/") def cmd_lsinfo(self, conn, path="/"): @@ -1225,7 +1219,7 @@ def cmd_lsinfo(self, conn, path="/"): if dirpath.startswith("/"): # Strip leading slash (libmpc rejects this). dirpath = dirpath[1:] - yield "directory: %s" % dirpath + yield f"directory: {dirpath}" def _listall(self, basepath, node, info=False): """Helper function for recursive listing. If info, show @@ -1237,7 +1231,7 @@ def _listall(self, basepath, node, info=False): item = self.lib.get_item(node) yield self._item_info(item) else: - yield "file: " + basepath + yield f"file: {basepath}" else: # List a directory. Recurse into both directories and files. for name, itemid in sorted(node.files.items()): @@ -1246,7 +1240,7 @@ def _listall(self, basepath, node, info=False): yield from self._listall(newpath, itemid, info) for name, subdir in sorted(node.dirs.items()): newpath = self._path_join(basepath, name) - yield "directory: " + newpath + yield f"directory: {newpath}" yield from self._listall(newpath, subdir, info) def cmd_listall(self, conn, path="/"): @@ -1280,7 +1274,7 @@ def _add(self, path, send_id=False): for item in self._all_items(self._resolve_path(path)): self.playlist.append(item) if send_id: - yield "Id: " + str(item.id) + yield f"Id: {item.id}" self.playlist_version += 1 self._send_event("playlist") @@ -1302,20 +1296,13 @@ def cmd_status(self, conn): 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}:{item.bitdepth}:{item.channels}", ) (pos, total) = self.player.time() yield ( - "time: {}:{}".format( - str(int(pos)), - str(int(total)), - ), + f"time: {int(pos)}:{int(total)}", "elapsed: " + f"{pos:.3f}", "duration: " + f"{total:.3f}", ) @@ -1335,13 +1322,13 @@ def cmd_stats(self, conn): artists, albums, songs, totaltime = tx.query(statement)[0] yield ( - "artists: " + str(artists), - "albums: " + str(albums), - "songs: " + str(songs), - "uptime: " + str(int(time.time() - self.startup_time)), - "playtime: " + "0", # Missing. - "db_playtime: " + str(int(totaltime)), - "db_update: " + str(int(self.updated_time)), + f"artists: {artists}", + f"albums: {albums}", + f"songs: {songs}", + f"uptime: {int(time.time() - self.startup_time)}", + "playtime: 0", # Missing. + f"db_playtime: {int(totaltime)}", + f"db_update: {int(self.updated_time)}", ) def cmd_decoders(self, conn): @@ -1383,7 +1370,7 @@ def cmd_tagtypes(self, conn): searching. """ for tag in self.tagtype_map: - yield "tagtype: " + tag + yield f"tagtype: {tag}" def _tagtype_lookup(self, tag): """Uses `tagtype_map` to look up the beets column name for an @@ -1458,12 +1445,9 @@ def cmd_list(self, conn, show_tag, *kv): clause, subvals = query.clause() statement = ( - "SELECT DISTINCT " - + show_key - + " FROM items WHERE " - + clause - + " ORDER BY " - + show_key + f"SELECT DISTINCT {show_key}" + f" FROM items WHERE {clause}" + f" ORDER BY {show_key}" ) self._log.debug(statement) with self.lib.transaction() as tx: @@ -1473,7 +1457,7 @@ def cmd_list(self, conn, show_tag, *kv): if not row[0]: # Skip any empty values of the field. continue - yield show_tag_canon + ": " + str(row[0]) + yield f"{show_tag_canon}: {row[0]}" def cmd_count(self, conn, tag, value): """Returns the number and total time of songs matching the @@ -1487,8 +1471,8 @@ def cmd_count(self, conn, tag, value): ): songs += 1 playtime += item.length - yield "songs: " + str(songs) - yield "playtime: " + str(int(playtime)) + yield f"songs: {songs}" + yield f"playtime: {int(playtime)}" # Persistent playlist manipulation. In MPD this is an optional feature so # these dummy implementations match MPD's behaviour with the feature off. diff --git a/beetsplug/bpd/gstplayer.py b/beetsplug/bpd/gstplayer.py index 03fb179aa2..fa23f2b0eb 100644 --- a/beetsplug/bpd/gstplayer.py +++ b/beetsplug/bpd/gstplayer.py @@ -129,7 +129,7 @@ def play_file(self, path): self.player.set_state(Gst.State.NULL) if isinstance(path, str): path = path.encode("utf-8") - uri = "file://" + urllib.parse.quote(path) + uri = f"file://{urllib.parse.quote(path)}" self.player.set_property("uri", uri) self.player.set_state(Gst.State.PLAYING) self.playing = True diff --git a/beetsplug/bpm.py b/beetsplug/bpm.py index 145986a957..d49963b728 100644 --- a/beetsplug/bpm.py +++ b/beetsplug/bpm.py @@ -73,12 +73,12 @@ def get_bpm(self, items, write=False): item = items[0] if item["bpm"]: - self._log.info("Found bpm {0}", item["bpm"]) + self._log.info("Found bpm {}", item["bpm"]) if not overwrite: return self._log.info( - "Press Enter {0} times to the rhythm or Ctrl-D to exit", + "Press Enter {} times to the rhythm or Ctrl-D to exit", self.config["max_strokes"].get(int), ) new_bpm = bpm(self.config["max_strokes"].get(int)) @@ -86,4 +86,4 @@ def get_bpm(self, items, write=False): if write: item.try_write() item.store() - self._log.info("Added new bpm {0}", item["bpm"]) + self._log.info("Added new bpm {}", item["bpm"]) diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py index ccd781b28b..9ae6d47d5f 100644 --- a/beetsplug/bpsync.py +++ b/beetsplug/bpsync.py @@ -82,8 +82,8 @@ def singletons(self, lib, query, move, pretend, write): if not self.is_beatport_track(item): self._log.info( - "Skipping non-{} singleton: {}", - self.beatport_plugin.data_source, + "Skipping non-{.beatport_plugin.data_source} singleton: {}", + self, item, ) continue @@ -107,8 +107,8 @@ def get_album_tracks(self, album): return False if not album.mb_albumid.isnumeric(): self._log.info( - "Skipping album with invalid {} ID: {}", - self.beatport_plugin.data_source, + "Skipping album with invalid {.beatport_plugin.data_source} ID: {}", + self, album, ) return False @@ -117,8 +117,8 @@ def get_album_tracks(self, album): return items if not all(self.is_beatport_track(item) for item in items): self._log.info( - "Skipping non-{} release: {}", - self.beatport_plugin.data_source, + "Skipping non-{.beatport_plugin.data_source} release: {}", + self, album, ) return False @@ -139,9 +139,7 @@ def albums(self, lib, query, move, pretend, write): albuminfo = self.beatport_plugin.album_for_id(album.mb_albumid) if not albuminfo: self._log.info( - "Release ID {} not found for album {}", - album.mb_albumid, - album, + "Release ID {0.mb_albumid} not found for album {0}", album ) continue diff --git a/beetsplug/bucket.py b/beetsplug/bucket.py index 9246539fcf..40369f74ab 100644 --- a/beetsplug/bucket.py +++ b/beetsplug/bucket.py @@ -41,7 +41,7 @@ def span_from_str(span_str): def normalize_year(d, yearfrom): """Convert string to a 4 digits year""" if yearfrom < 100: - raise BucketError("%d must be expressed on 4 digits" % yearfrom) + raise BucketError(f"{yearfrom} must be expressed on 4 digits") # if two digits only, pick closest year that ends by these two # digits starting from yearfrom @@ -55,14 +55,13 @@ def normalize_year(d, yearfrom): years = [int(x) for x in re.findall(r"\d+", span_str)] if not years: raise ui.UserError( - "invalid range defined for year bucket '%s': no " - "year found" % span_str + f"invalid range defined for year bucket {span_str!r}: no year found" ) try: years = [normalize_year(x, years[0]) for x in years] except BucketError as exc: raise ui.UserError( - "invalid range defined for year bucket '%s': %s" % (span_str, exc) + f"invalid range defined for year bucket {span_str!r}: {exc}" ) res = {"from": years[0], "str": span_str} @@ -125,22 +124,19 @@ def str2fmt(s): "fromnchars": len(m.group("fromyear")), "tonchars": len(m.group("toyear")), } - res["fmt"] = "{}%s{}{}{}".format( - m.group("bef"), - m.group("sep"), - "%s" if res["tonchars"] else "", - m.group("after"), + res["fmt"] = ( + f"{m['bef']}{{}}{m['sep']}{'{}' if res['tonchars'] else ''}{m['after']}" ) return res def format_span(fmt, yearfrom, yearto, fromnchars, tonchars): """Return a span string representation.""" - args = str(yearfrom)[-fromnchars:] + args = [str(yearfrom)[-fromnchars:]] if tonchars: - args = (str(yearfrom)[-fromnchars:], str(yearto)[-tonchars:]) + args.append(str(yearto)[-tonchars:]) - return fmt % args + return fmt.format(*args) def extract_modes(spans): @@ -169,14 +165,12 @@ def build_alpha_spans(alpha_spans_str, alpha_regexs): else: raise ui.UserError( "invalid range defined for alpha bucket " - "'%s': no alphanumeric character found" % elem + f"'{elem}': no alphanumeric character found" ) spans.append( re.compile( - "^[" - + ASCII_DIGITS[begin_index : end_index + 1] - + ASCII_DIGITS[begin_index : end_index + 1].upper() - + "]" + rf"^[{ASCII_DIGITS[begin_index : end_index + 1]}]", + re.IGNORECASE, ) ) return spans diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index 3b31382b7d..192310fb84 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -90,7 +90,7 @@ def acoustid_match(log, path): duration, fp = acoustid.fingerprint_file(util.syspath(path)) except acoustid.FingerprintGenerationError as exc: log.error( - "fingerprinting of {0} failed: {1}", + "fingerprinting of {} failed: {}", util.displayable_path(repr(path)), exc, ) @@ -103,12 +103,12 @@ def acoustid_match(log, path): ) except acoustid.AcoustidError as exc: log.debug( - "fingerprint matching {0} failed: {1}", + "fingerprint matching {} failed: {}", util.displayable_path(repr(path)), exc, ) return None - log.debug("chroma: fingerprinted {0}", util.displayable_path(repr(path))) + log.debug("chroma: fingerprinted {}", util.displayable_path(repr(path))) # Ensure the response is usable and parse it. if res["status"] != "ok" or not res.get("results"): @@ -146,7 +146,7 @@ def acoustid_match(log, path): release_ids = [rel["id"] for rel in releases] log.debug( - "matched recordings {0} on releases {1}", recording_ids, release_ids + "matched recordings {} on releases {}", recording_ids, release_ids ) _matches[path] = recording_ids, release_ids @@ -211,7 +211,7 @@ def candidates(self, items, artist, album, va_likely): if album: albums.append(album) - self._log.debug("acoustid album candidates: {0}", len(albums)) + self._log.debug("acoustid album candidates: {}", len(albums)) return albums def item_candidates(self, item, artist, title) -> Iterable[TrackInfo]: @@ -224,7 +224,7 @@ def item_candidates(self, item, artist, title) -> Iterable[TrackInfo]: track = self.mb.track_for_id(recording_id) if track: tracks.append(track) - self._log.debug("acoustid item candidates: {0}", len(tracks)) + self._log.debug("acoustid item candidates: {}", len(tracks)) return tracks def album_for_id(self, *args, **kwargs): @@ -292,11 +292,11 @@ def submit_items(log, userkey, items, chunksize=64): def submit_chunk(): """Submit the current accumulated fingerprint data.""" - log.info("submitting {0} fingerprints", len(data)) + log.info("submitting {} fingerprints", len(data)) try: acoustid.submit(API_KEY, userkey, data, timeout=10) except acoustid.AcoustidError as exc: - log.warning("acoustid submission error: {0}", exc) + log.warning("acoustid submission error: {}", exc) del data[:] for item in items: @@ -343,31 +343,23 @@ def fingerprint_item(log, item, write=False): """ # Get a fingerprint and length for this track. if not item.length: - log.info("{0}: no duration available", util.displayable_path(item.path)) + log.info("{.filepath}: no duration available", item) elif item.acoustid_fingerprint: if write: - log.info( - "{0}: fingerprint exists, skipping", - util.displayable_path(item.path), - ) + log.info("{.filepath}: fingerprint exists, skipping", item) else: - log.info( - "{0}: using existing fingerprint", - util.displayable_path(item.path), - ) + log.info("{.filepath}: using existing fingerprint", item) return item.acoustid_fingerprint else: - log.info("{0}: fingerprinting", util.displayable_path(item.path)) + log.info("{.filepath}: fingerprinting", item) try: _, fp = acoustid.fingerprint_file(util.syspath(item.path)) item.acoustid_fingerprint = fp.decode() if write: - log.info( - "{0}: writing fingerprint", util.displayable_path(item.path) - ) + log.info("{.filepath}: writing fingerprint", item) item.try_write() if item._db: item.store() return item.acoustid_fingerprint except acoustid.FingerprintGenerationError as exc: - log.info("fingerprint generation failed: {0}", exc) + log.info("fingerprint generation failed: {}", exc) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index c4df9ab574..e9db3592e1 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -64,9 +64,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 @@ -77,8 +75,8 @@ def get_format(fmt=None): command = config["convert"]["command"].as_str() elif "opts" in keys: # Undocumented option for backwards compatibility with < 1.3.1. - command = "ffmpeg -i $source -y {} $dest".format( - config["convert"]["opts"].as_str() + command = ( + f"ffmpeg -i $source -y {config['convert']['opts'].as_str()} $dest" ) if "extension" in keys: extension = config["convert"]["extension"].as_str() @@ -125,18 +123,25 @@ def __init__(self): "id3v23": "inherit", "formats": { "aac": { - "command": "ffmpeg -i $source -y -vn -acodec aac " - "-aq 1 $dest", + "command": ( + "ffmpeg -i $source -y -vn -acodec aac -aq 1 $dest" + ), "extension": "m4a", }, "alac": { - "command": "ffmpeg -i $source -y -vn -acodec alac $dest", + "command": ( + "ffmpeg -i $source -y -vn -acodec alac $dest" + ), "extension": "m4a", }, "flac": "ffmpeg -i $source -y -vn -acodec flac $dest", "mp3": "ffmpeg -i $source -y -vn -aq 2 $dest", - "opus": "ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest", - "ogg": "ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest", + "opus": ( + "ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest" + ), + "ogg": ( + "ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest" + ), "wma": "ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest", }, "max_bitrate": None, @@ -171,16 +176,17 @@ def commands(self): "--threads", action="store", type="int", - help="change the number of threads, \ - defaults to maximum available processors", + help=( + "change the number of threads, defaults to maximum available" + " processors" + ), ) cmd.parser.add_option( "-k", "--keep-new", action="store_true", dest="keep_new", - help="keep only the converted \ - and move the old files", + help="keep only the converted and move the old files", ) cmd.parser.add_option( "-d", "--dest", action="store", help="set the destination directory" @@ -204,16 +210,16 @@ def commands(self): "--link", action="store_true", dest="link", - help="symlink files that do not \ - need transcoding.", + help="symlink files that do not need transcoding.", ) cmd.parser.add_option( "-H", "--hardlink", action="store_true", dest="hardlink", - help="hardlink files that do not \ - need transcoding. Overrides --link.", + help=( + "hardlink files that do not need transcoding. Overrides --link." + ), ) cmd.parser.add_option( "-m", @@ -282,7 +288,7 @@ def encode(self, command, source, dest, pretend=False): quiet = self.config["quiet"].get(bool) if not quiet and not pretend: - self._log.info("Encoding {0}", util.displayable_path(source)) + self._log.info("Encoding {}", util.displayable_path(source)) command = os.fsdecode(command) source = os.fsdecode(source) @@ -301,7 +307,7 @@ def encode(self, command, source, dest, pretend=False): encode_cmd.append(os.fsdecode(args[i])) if pretend: - self._log.info("{0}", " ".join(args)) + self._log.info("{}", " ".join(args)) return try: @@ -309,26 +315,25 @@ def encode(self, command, source, dest, pretend=False): except subprocess.CalledProcessError as exc: # Something went wrong (probably Ctrl+C), remove temporary files self._log.info( - "Encoding {0} failed. Cleaning up...", + "Encoding {} failed. Cleaning up...", util.displayable_path(source), ) self._log.debug( - "Command {0} exited with status {1}: {2}", + "Command {0} exited with status {1.returncode}: {1.output}", args, - exc.returncode, - exc.output, + exc, ) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError( - "convert: couldn't invoke '{}': {}".format(" ".join(args), exc) + f"convert: couldn't invoke {' '.join(args)!r}: {exc}" ) if not quiet and not pretend: self._log.info( - "Finished encoding {0}", util.displayable_path(source) + "Finished encoding {}", util.displayable_path(source) ) def convert_item( @@ -356,7 +361,7 @@ def convert_item( try: mediafile.MediaFile(util.syspath(item.path)) except mediafile.UnreadableFileError as exc: - self._log.error("Could not open file to convert: {0}", exc) + self._log.error("Could not open file to convert: {}", exc) continue # When keeping the new file in the library, we first move the @@ -382,21 +387,20 @@ def convert_item( if os.path.exists(util.syspath(dest)): self._log.info( - "Skipping {0} (target file exists)", - util.displayable_path(item.path), + "Skipping {.filepath} (target file exists)", item ) continue if keep_new: if pretend: self._log.info( - "mv {0} {1}", - util.displayable_path(item.path), + "mv {.filepath} {}", + item, util.displayable_path(original), ) else: self._log.info( - "Moving to {0}", util.displayable_path(original) + "Moving to {}", util.displayable_path(original) ) util.move(item.path, original) @@ -412,10 +416,10 @@ def convert_item( msg = "ln" if hardlink else ("ln -s" if link else "cp") self._log.info( - "{2} {0} {1}", + "{} {} {}", + msg, util.displayable_path(original), util.displayable_path(converted), - msg, ) else: # No transcoding necessary. @@ -425,9 +429,7 @@ def convert_item( else ("Linking" if link else "Copying") ) - self._log.info( - "{1} {0}", util.displayable_path(item.path), msg - ) + self._log.info("{} {.filepath}", msg, item) if hardlink: util.hardlink(original, converted) @@ -458,8 +460,7 @@ def convert_item( if album and album.artpath: maxwidth = self._get_art_resize(album.artpath) self._log.debug( - "embedding album art from {}", - util.displayable_path(album.artpath), + "embedding album art from {.art_filepath}", album ) art.embed_item( self._log, @@ -517,8 +518,7 @@ def copy_album_art( if os.path.exists(util.syspath(dest)): self._log.info( - "Skipping {0} (target file exists)", - util.displayable_path(album.artpath), + "Skipping {.art_filepath} (target file exists)", album ) return @@ -528,8 +528,8 @@ def copy_album_art( # Either copy or resize (while copying) the image. if maxwidth is not None: self._log.info( - "Resizing cover art from {0} to {1}", - util.displayable_path(album.artpath), + "Resizing cover art from {.art_filepath} to {}", + album, util.displayable_path(dest), ) if not pretend: @@ -539,10 +539,10 @@ def copy_album_art( msg = "ln" if hardlink else ("ln -s" if link else "cp") self._log.info( - "{2} {0} {1}", - util.displayable_path(album.artpath), - util.displayable_path(dest), + "{} {.art_filepath} {}", msg, + album, + util.displayable_path(dest), ) else: msg = ( @@ -552,10 +552,10 @@ def copy_album_art( ) self._log.info( - "{2} cover art from {0} to {1}", - util.displayable_path(album.artpath), - util.displayable_path(dest), + "{} cover art from {.art_filepath} to {}", msg, + album, + util.displayable_path(dest), ) if hardlink: util.hardlink(album.artpath, dest) @@ -616,7 +616,7 @@ def convert_func(self, lib, opts, args): # Playlist paths are understood as relative to the dest directory. pl_normpath = util.normpath(playlist) pl_dir = os.path.dirname(pl_normpath) - self._log.info("Creating playlist file {0}", pl_normpath) + self._log.info("Creating playlist file {}", pl_normpath) # Generates a list of paths to media files, ensures the paths are # relative to the playlist's location and translates the unicode # strings we get from item.destination to bytes. @@ -644,7 +644,7 @@ def convert_on_import(self, lib, item): tmpdir = self.config["tmpdir"].get() if tmpdir: tmpdir = os.fsdecode(util.bytestring_path(tmpdir)) - fd, dest = tempfile.mkstemp(os.fsdecode(b"." + ext), dir=tmpdir) + fd, dest = tempfile.mkstemp(f".{os.fsdecode(ext)}", dir=tmpdir) os.close(fd) dest = util.bytestring_path(dest) _temp_files.append(dest) # Delete the transcode later. @@ -666,7 +666,7 @@ def convert_on_import(self, lib, item): if self.config["delete_originals"]: self._log.log( logging.DEBUG if self.config["quiet"] else logging.INFO, - "Removing original file {0}", + "Removing original file {}", source_path, ) util.remove(source_path, False) diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py index c8602b5e89..e427b08b1c 100644 --- a/beetsplug/deezer.py +++ b/beetsplug/deezer.py @@ -96,7 +96,7 @@ def album_for_id(self, album_id: str) -> AlbumInfo | None: f"Invalid `release_date` returned by {self.data_source} API: " f"{release_date!r}" ) - tracks_obj = self.fetch_data(self.album_url + deezer_id + "/tracks") + tracks_obj = self.fetch_data(f"{self.album_url}{deezer_id}/tracks") if tracks_obj is None: return None try: @@ -169,7 +169,7 @@ def track_for_id(self, track_id: str) -> None | TrackInfo: # the track's disc). if not ( album_tracks_obj := self.fetch_data( - self.album_url + str(track_data["album"]["id"]) + "/tracks" + f"{self.album_url}{track_data['album']['id']}/tracks" ) ): return None @@ -241,26 +241,26 @@ def _search_api( query = self._construct_search_query( query_string=query_string, filters=filters ) - self._log.debug(f"Searching {self.data_source} for '{query}'") + self._log.debug("Searching {.data_source} for '{}'", self, query) try: response = requests.get( - self.search_url + query_type, + f"{self.search_url}{query_type}", params={"q": query}, timeout=10, ) response.raise_for_status() except requests.exceptions.RequestException as e: self._log.error( - "Error fetching data from {} API\n Error: {}", - self.data_source, + "Error fetching data from {.data_source} API\n Error: {}", + self, e, ) return () response_data: Sequence[IDResponse] = response.json().get("data", []) self._log.debug( - "Found {} result(s) from {} for '{}'", + "Found {} result(s) from {.data_source} for '{}'", len(response_data), - self.data_source, + self, query, ) return response_data diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index ac7421c5ff..bf41cf38d8 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -145,7 +145,7 @@ def authenticate(self, c_key, c_secret): try: _, _, url = auth_client.get_authorize_url() except CONNECTION_ERRORS as e: - self._log.debug("connection error: {0}", e) + self._log.debug("connection error: {}", e) raise beets.ui.UserError("communication with Discogs failed") beets.ui.print_("To authenticate with Discogs, visit:") @@ -158,11 +158,11 @@ def authenticate(self, c_key, c_secret): except DiscogsAPIError: raise beets.ui.UserError("Discogs authorization failed") except CONNECTION_ERRORS as e: - self._log.debug("connection error: {0}", e) + self._log.debug("connection error: {}", e) raise beets.ui.UserError("Discogs token request failed") # Save the token for later use. - self._log.debug("Discogs token {0}, secret {1}", token, secret) + self._log.debug("Discogs token {}, secret {}", token, secret) with open(self._tokenfile(), "w") as f: json.dump({"token": token, "secret": secret}, f) @@ -202,7 +202,7 @@ def album_for_id(self, album_id: str) -> AlbumInfo | None: """Fetches an album by its Discogs ID and returns an AlbumInfo object or None if the album is not found. """ - self._log.debug("Searching for release {0}", album_id) + self._log.debug("Searching for release {}", album_id) discogs_id = self._extract_id(album_id) @@ -216,7 +216,7 @@ def album_for_id(self, album_id: str) -> AlbumInfo | None: except DiscogsAPIError as e: if e.status_code != 404: self._log.debug( - "API Error: {0} (query: {1})", + "API Error: {} (query: {})", e, result.data["resource_url"], ) @@ -266,7 +266,7 @@ def get_master_year(self, master_id: str) -> int | None: """Fetches a master release given its Discogs ID and returns its year or None if the master release is not found. """ - self._log.debug("Getting master release {0}", master_id) + self._log.debug("Getting master release {}", master_id) result = Master(self.discogs_client, {"id": master_id}) try: @@ -274,7 +274,7 @@ def get_master_year(self, master_id: str) -> int | None: except DiscogsAPIError as e: if e.status_code != 404: self._log.debug( - "API Error: {0} (query: {1})", + "API Error: {} (query: {})", e, result.data["resource_url"], ) @@ -385,7 +385,7 @@ def get_album_info(self, result): track.artist_id = artist_id # Discogs does not have track IDs. Invent our own IDs as proposed # in #2336. - track.track_id = str(album_id) + "-" + track.track_alt + track.track_id = f"{album_id}-{track.track_alt}" track.data_url = data_url track.data_source = "Discogs" @@ -552,7 +552,7 @@ def add_merged_subtracks(tracklist, subtracks): 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. @@ -574,8 +574,8 @@ def add_merged_subtracks(tracklist, subtracks): # 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 a/beetsplug/duplicates.py b/beetsplug/duplicates.py index ea7abaaff9..904e192628 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -150,7 +150,7 @@ def _dup(lib, opts, args): count = self.config["count"].get(bool) delete = self.config["delete"].get(bool) remove = self.config["remove"].get(bool) - fmt = self.config["format"].get(str) + fmt_tmpl = self.config["format"].get(str) full = self.config["full"].get(bool) keys = self.config["keys"].as_str_seq() merge = self.config["merge"].get(bool) @@ -175,15 +175,14 @@ def _dup(lib, opts, args): return if path: - fmt = "$path" + fmt_tmpl = "$path" # Default format string for count mode. - if count and not fmt: + if count and not fmt_tmpl: if album: - fmt = "$albumartist - $album" + fmt_tmpl = "$albumartist - $album" else: - fmt = "$albumartist - $album - $title" - fmt += ": {0}" + fmt_tmpl = "$albumartist - $album - $title" if checksum: for i in items: @@ -207,7 +206,7 @@ def _dup(lib, opts, args): delete=delete, remove=remove, tag=tag, - fmt=fmt.format(obj_count), + fmt=f"{fmt_tmpl}: {obj_count}", ) self._command.func = _dup @@ -255,28 +254,24 @@ def _checksum(self, item, prog): checksum = getattr(item, key, False) if not checksum: self._log.debug( - "key {0} on item {1} not cached:computing checksum", + "key {} on item {.filepath} not cached:computing checksum", key, - displayable_path(item.path), + item, ) try: checksum = command_output(args).stdout setattr(item, key, checksum) item.store() self._log.debug( - "computed checksum for {0} using {1}", item.title, key + "computed checksum for {.title} using {}", item, key ) except subprocess.CalledProcessError as e: - self._log.debug( - "failed to checksum {0}: {1}", - displayable_path(item.path), - e, - ) + self._log.debug("failed to checksum {.filepath}: {}", item, e) else: self._log.debug( - "key {0} on item {1} cached:not computing checksum", + "key {} on item {.filepath} cached:not computing checksum", key, - displayable_path(item.path), + item, ) return key, checksum @@ -294,15 +289,15 @@ def _group_by(self, objs, keys, strict): values = [v for v in values if v not in (None, "")] if strict and len(values) < len(keys): self._log.debug( - "some keys {0} on item {1} are null or empty: skipping", + "some keys {} on item {.filepath} are null or empty: skipping", keys, - displayable_path(obj.path), + obj, ) elif not strict and not len(values): self._log.debug( - "all keys {0} on item {1} are null or empty: skipping", + "all keys {} on item {.filepath} are null or empty: skipping", keys, - displayable_path(obj.path), + obj, ) else: key = tuple(values) @@ -360,11 +355,11 @@ def _merge_items(self, objs): value = getattr(o, f, None) if value: self._log.debug( - "key {0} on item {1} is null " - "or empty: setting from item {2}", + "key {} on item {} is null " + "or empty: setting from item {.filepath}", f, displayable_path(objs[0].path), - displayable_path(o.path), + o, ) setattr(objs[0], f, value) objs[0].store() @@ -384,11 +379,11 @@ def _merge_albums(self, objs): missing.album_id = objs[0].id missing.add(i._db) self._log.debug( - "item {0} missing from album {1}:" - " merging from {2} into {3}", + "item {} missing from album {}:" + " merging from {.filepath} into {}", missing, objs[0], - displayable_path(o.path), + o, displayable_path(missing.destination()), ) missing.move(operation=MoveOperation.COPY) diff --git a/beetsplug/edit.py b/beetsplug/edit.py index 52387c3140..f6fadefd0c 100644 --- a/beetsplug/edit.py +++ b/beetsplug/edit.py @@ -46,9 +46,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): @@ -71,9 +69,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 a/beetsplug/embedart.py b/beetsplug/embedart.py index 8df3c3c056..1a59e4f9c2 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -35,8 +35,9 @@ def _confirm(objs, album): to items). """ noun = "album" if album else "file" - prompt = "Modify artwork for {} {}{} (Y/n)?".format( - len(objs), noun, "s" if len(objs) > 1 else "" + prompt = ( + "Modify artwork for" + f" {len(objs)} {noun}{'s' if len(objs) > 1 else ''} (Y/n)?" ) # Show all the items or albums. @@ -110,9 +111,7 @@ def embed_func(lib, opts, args): imagepath = normpath(opts.file) if not os.path.isfile(syspath(imagepath)): raise ui.UserError( - "image file {} not found".format( - displayable_path(imagepath) - ) + f"image file {displayable_path(imagepath)} not found" ) items = lib.items(args) @@ -137,7 +136,7 @@ def embed_func(lib, opts, args): 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("{}", e) return extension = guess_extension(response.headers["Content-Type"]) if extension is None: @@ -149,7 +148,7 @@ def embed_func(lib, opts, args): with open(tempimg, "wb") as f: f.write(response.content) except Exception as e: - self._log.error("Unable to save image: {}".format(e)) + self._log.error("Unable to save image: {}", e) return items = lib.items(args) # Confirm with user. @@ -274,7 +273,7 @@ def remove_artfile(self, album): """ if self.config["remove_art_file"] and album.artpath: if os.path.isfile(syspath(album.artpath)): - self._log.debug("Removing album art file for {0}", album) + self._log.debug("Removing album art file for {}", album) os.remove(syspath(album.artpath)) album.artpath = None album.store() diff --git a/beetsplug/embyupdate.py b/beetsplug/embyupdate.py index c696f39f38..25f3ed8b3d 100644 --- a/beetsplug/embyupdate.py +++ b/beetsplug/embyupdate.py @@ -38,9 +38,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) @@ -81,12 +79,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 @@ -186,7 +184,7 @@ def update(self, lib): # Get user information from the Emby API. user = get_user(host, port, username) if not user: - self._log.warning(f"User {username} could not be found.") + self._log.warning("User {} could not be found.", username) return userid = user[0]["Id"] @@ -198,7 +196,7 @@ def update(self, lib): # Get authentication token. token = get_token(host, port, headers, auth_data) if not token: - self._log.warning("Could not get token for user {0}", username) + self._log.warning("Could not get token for user {}", username) return # Recreate headers with a token. diff --git a/beetsplug/export.py b/beetsplug/export.py index 05ca3f24ad..e6c2b88c7a 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -150,7 +150,7 @@ def run(self, lib, opts, args): try: data, item = data_emitter(included_keys or "*") except (mediafile.UnreadableFileError, OSError) as ex: - self._log.error("cannot read file: {0}", ex) + self._log.error("cannot read file: {}", ex) continue for key, value in data.items(): diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index e1ec5aa09a..54c065da40 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -133,7 +133,7 @@ def _validate( # get_size returns None if no local imaging backend is available if not self.size: self.size = ArtResizer.shared.get_size(self.path) - self._log.debug("image size: {}", self.size) + self._log.debug("image size: {.size}", self) if not self.size: self._log.warning( @@ -151,7 +151,7 @@ def _validate( # Check minimum dimension. if plugin.minwidth and self.size[0] < plugin.minwidth: self._log.debug( - "image too small ({} < {})", self.size[0], plugin.minwidth + "image too small ({} < {.minwidth})", self.size[0], plugin ) return ImageAction.BAD @@ -162,10 +162,10 @@ def _validate( if edge_diff > plugin.margin_px: self._log.debug( "image is not close enough to being " - "square, ({} - {} > {})", + "square, ({} - {} > {.margin_px})", long_edge, short_edge, - plugin.margin_px, + plugin, ) return ImageAction.BAD elif plugin.margin_percent: @@ -190,7 +190,7 @@ def _validate( downscale = False if plugin.maxwidth and self.size[0] > plugin.maxwidth: self._log.debug( - "image needs rescaling ({} > {})", self.size[0], plugin.maxwidth + "image needs rescaling ({} > {.maxwidth})", self.size[0], plugin ) downscale = True @@ -200,9 +200,9 @@ def _validate( filesize = os.stat(syspath(self.path)).st_size if filesize > plugin.max_filesize: self._log.debug( - "image needs resizing ({}B > {}B)", + "image needs resizing ({}B > {.max_filesize}B)", filesize, - plugin.max_filesize, + plugin, ) downsize = True @@ -213,9 +213,9 @@ def _validate( reformat = fmt != plugin.cover_format if reformat: self._log.debug( - "image needs reformatting: {} -> {}", + "image needs reformatting: {} -> {.cover_format}", fmt, - plugin.cover_format, + plugin, ) skip_check_for = skip_check_for or [] @@ -329,7 +329,7 @@ def _logged_get(log: Logger, *args, **kwargs) -> requests.Response: prepped.url, {}, None, None, None ) send_kwargs.update(settings) - log.debug("{}: {}", message, prepped.url) + log.debug("{}: {.url}", message, prepped) return s.send(prepped, **send_kwargs) @@ -542,14 +542,14 @@ def get_image_urls( try: response = self.request(url) except requests.RequestException: - self._log.debug("{}: error receiving response", self.NAME) + self._log.debug("{.NAME}: error receiving response", self) return try: data = response.json() except ValueError: self._log.debug( - "{}: error loading response: {}", self.NAME, response.text + "{.NAME}: error loading response: {.text}", self, response ) return @@ -593,7 +593,7 @@ def get_image_urls( class Amazon(RemoteArtSource): NAME = "Amazon" ID = "amazon" - URL = "https://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg" + URL = "https://images.amazon.com/images/P/{}.{:02d}.LZZZZZZZ.jpg" INDICES = (1, 2) def get( @@ -606,7 +606,7 @@ def get( if album.asin: for index in self.INDICES: yield self._candidate( - url=self.URL % (album.asin, index), + url=self.URL.format(album.asin, index), match=MetadataMatch.EXACT, ) @@ -629,7 +629,7 @@ def get( # Get the page from albumart.org. try: resp = self.request(self.URL, params={"asin": album.asin}) - self._log.debug("scraped art URL: {}", resp.url) + self._log.debug("scraped art URL: {.url}", resp) except requests.RequestException: self._log.debug("error scraping art page") return @@ -682,7 +682,7 @@ def get( """ if not (album.albumartist and album.album): return - search_string = (album.albumartist + "," + album.album).encode("utf-8") + search_string = f"{album.albumartist},{album.album}".encode("utf-8") try: response = self.request( @@ -702,7 +702,7 @@ def get( try: data = response.json() except ValueError: - self._log.debug("google: error loading response: {}", response.text) + self._log.debug("google: error loading response: {.text}", response) return if "error" in data: @@ -723,7 +723,7 @@ class FanartTV(RemoteArtSource): NAME = "fanart.tv" ID = "fanarttv" API_URL = "https://webservice.fanart.tv/v3/" - API_ALBUMS = API_URL + "music/albums/" + API_ALBUMS = f"{API_URL}music/albums/" PROJECT_KEY = "61a7d0ab4e67162b7a0c7c35915cd48e" def __init__(self, *args, **kwargs): @@ -750,7 +750,7 @@ def get( try: response = self.request( - self.API_ALBUMS + album.mb_releasegroupid, + f"{self.API_ALBUMS}{album.mb_releasegroupid}", headers={ "api-key": self.PROJECT_KEY, "client-key": self.client_key, @@ -764,7 +764,7 @@ def get( data = response.json() except ValueError: self._log.debug( - "fanart.tv: error loading response: {}", response.text + "fanart.tv: error loading response: {.text}", response ) return @@ -820,7 +820,7 @@ def get( return payload = { - "term": album.albumartist + " " + album.album, + "term": f"{album.albumartist} {album.album}", "entity": "album", "media": "music", "limit": 200, @@ -947,14 +947,14 @@ def get( data = dbpedia_response.json() results = data["results"]["bindings"] if results: - cover_filename = "File:" + results[0]["coverFilename"]["value"] + cover_filename = f"File:{results[0]['coverFilename']['value']}" page_id = results[0]["pageId"]["value"] else: self._log.debug("wikipedia: album not found on dbpedia") except (ValueError, KeyError, IndexError): self._log.debug( - "wikipedia: error scraping dbpedia response: {}", - dbpedia_response.text, + "wikipedia: error scraping dbpedia response: {.text}", + dbpedia_response, ) # Ensure we have a filename before attempting to query wikipedia @@ -996,7 +996,7 @@ def get( results = data["query"]["pages"][page_id]["images"] for result in results: if re.match( - re.escape(lpart) + r".*?\." + re.escape(rpart), + rf"{re.escape(lpart)}.*?\.{re.escape(rpart)}", result["title"], ): cover_filename = result["title"] @@ -1179,7 +1179,7 @@ def get( if "error" in data: if data["error"] == 6: self._log.debug( - "lastfm: no results for {}", album.mb_albumid + "lastfm: no results for {.mb_albumid}", album ) else: self._log.error( @@ -1200,7 +1200,7 @@ def get( url=images[size], size=self.SIZES[size] ) except ValueError: - self._log.debug("lastfm: error loading response: {}", response.text) + self._log.debug("lastfm: error loading response: {.text}", response) return @@ -1227,7 +1227,7 @@ def get( paths: None | Sequence[bytes], ) -> Iterator[Candidate]: try: - url = self.SPOTIFY_ALBUM_URL + album.items().get().spotify_album_id + url = f"{self.SPOTIFY_ALBUM_URL}{album.items().get().spotify_album_id}" except AttributeError: self._log.debug("Fetchart: no Spotify album ID found") return @@ -1244,7 +1244,7 @@ def get( soup = BeautifulSoup(html, "html.parser") except ValueError: self._log.debug( - "Spotify: error loading response: {}", response.text + "Spotify: error loading response: {.text}", response ) return @@ -1541,9 +1541,7 @@ def art_for_album( out = candidate assert out.path is not None # help mypy self._log.debug( - "using {0.LOC} image {1}", - source, - util.displayable_path(out.path), + "using {.LOC} image {.path}", source, out ) break # Remove temporary files for invalid candidates. @@ -1576,7 +1574,7 @@ def batch_fetch_art( message = ui.colorize( "text_highlight_minor", "has album art" ) - self._log.info("{0}: {1}", album, message) + self._log.info("{}: {}", album, message) else: # In ordinary invocations, look for images on the # filesystem. When forcing, however, always go to the Web @@ -1589,4 +1587,4 @@ def batch_fetch_art( message = ui.colorize("text_success", "found album art") else: message = ui.colorize("text_error", "no art found") - self._log.info("{0}: {1}", album, message) + self._log.info("{}: {}", album, message) diff --git a/beetsplug/fish.py b/beetsplug/fish.py index 4cf9b60a16..b1518f1c40 100644 --- a/beetsplug/fish.py +++ b/beetsplug/fish.py @@ -89,8 +89,9 @@ def commands(self): "-o", "--output", default="~/.config/fish/completions/beet.fish", - help="where to save the script. default: " - "~/.config/fish/completions", + help=( + "where to save the script. default: ~/.config/fish/completions" + ), ) return [cmd] @@ -122,23 +123,13 @@ def run(self, lib, opts, args): for name in names: cmd_names_help.append((name, cmd.help)) # Concatenate the string - totstring = HEAD + "\n" + totstring = f"{HEAD}\n" 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" - ) + totstring += "\n# ====== setup field completion for subcommands =====\n" totstring += get_subcommands(cmd_names_help, nobasicfields, extravalues) # Set up completion for all the command options totstring += get_all_commands(beetcmds) @@ -150,23 +141,19 @@ def run(self, lib, opts, args): def _escape(name): # Escape ? in fish if name == "?": - name = "\\" + name + name = f"\\{name}" return name def get_cmds_list(cmds_names): # Make a list of all Beets core & plugin commands - substr = "" - substr += "set CMDS " + " ".join(cmds_names) + ("\n" * 2) - return substr + return f"set CMDS {' '.join(cmds_names)}\n\n" def get_standard_fields(fields): # Make a list of album/track fields and append with ':' - fields = (field + ":" for field in fields) - substr = "" - substr += "set FIELDS " + " ".join(fields) + ("\n" * 2) - return substr + fields = (f"{field}:" for field in fields) + return f"set FIELDS {' '.join(fields)}\n\n" def get_extravalues(lib, extravalues): @@ -175,14 +162,8 @@ def get_extravalues(lib, extravalues): word = "" values_set = get_set_of_values_for_field(lib, extravalues) for fld in extravalues: - extraname = fld.upper() + "S" - word += ( - "set " - + extraname - + " " - + " ".join(sorted(values_set[fld])) - + ("\n" * 2) - ) + extraname = f"{fld.upper()}S" + word += f"set {extraname} {' '.join(sorted(values_set[fld]))}\n\n" return word @@ -226,35 +207,29 @@ 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 += f"\n# ------ fieldsetups for {cmdname} -------\n" word += BL_NEED2.format( - ("-a " + cmdname), ("-f " + "-d " + wrap(clean_whitespace(cmdhelp))) + f"-a {cmdname}", f"-f -d {wrap(clean_whitespace(cmdhelp))}" ) if nobasicfields is False: word += BL_USE3.format( cmdname, - ("-a " + wrap("$FIELDS")), - ("-f " + "-d " + wrap("fieldname")), + f"-a {wrap('$FIELDS')}", + f"-f -d {wrap('fieldname')}", ) if extravalues: for f in extravalues: - setvar = wrap("$" + f.upper() + "S") - word += ( - " ".join( - BL_EXTRA3.format( - (cmdname + " " + f + ":"), - ("-f " + "-A " + "-a " + setvar), - ("-d " + wrap(f)), - ).split() - ) - + "\n" + setvar = wrap(f"${f.upper()}S") + word += " ".join( + BL_EXTRA3.format( + f"{cmdname} {f}:", + f"-f -A -a {setvar}", + f"-d {wrap(f)}", + ).split() ) + word += "\n" return word @@ -267,59 +242,44 @@ def get_all_commands(beetcmds): for name in names: name = _escape(name) - word += "\n" - word += ( - ("\n" * 2) - + "# ====== {} =====".format("completions for " + name) - + "\n" - ) + word += f"\n\n\n# ====== completions for {name} =====\n" for option in cmd.parser._get_all_options()[1:]: cmd_l = ( - (" -l " + option._long_opts[0].replace("--", "")) + f" -l {option._long_opts[0].replace('--', '')}" if option._long_opts else "" ) cmd_s = ( - (" -s " + option._short_opts[0].replace("-", "")) + f" -s {option._short_opts[0].replace('-', '')}" if option._short_opts else "" ) cmd_need_arg = " -r " if option.nargs in [1] else "" cmd_helpstr = ( - (" -d " + wrap(" ".join(option.help.split()))) + f" -d {wrap(' '.join(option.help.split()))}" if option.help else "" ) cmd_arglist = ( - (" -a " + wrap(" ".join(option.choices))) + f" -a {wrap(' '.join(option.choices))}" if option.choices else "" ) - word += ( - " ".join( - BL_USE3.format( - name, - ( - cmd_need_arg - + cmd_s - + cmd_l - + " -f " - + cmd_arglist - ), - cmd_helpstr, - ).split() - ) - + "\n" + word += " ".join( + BL_USE3.format( + name, + f"{cmd_need_arg}{cmd_s}{cmd_l} -f {cmd_arglist}", + cmd_helpstr, + ).split() ) + word += "\n" - word = word + " ".join( - BL_USE3.format( - name, - ("-s " + "h " + "-l " + "help" + " -f "), - ("-d " + wrap("print help") + "\n"), - ).split() + word = word + BL_USE3.format( + name, + "-s h -l help -f", + f"-d {wrap('print help')}", ) return word @@ -332,9 +292,9 @@ def clean_whitespace(word): def wrap(word): # Need " or ' around strings but watch out if they're in the string sptoken = '"' - if ('"') in word and ("'") in word: + if '"' in word and ("'") in word: word.replace('"', sptoken) - return '"' + word + '"' + return f'"{word}"' tok = '"' if "'" in word else "'" - return tok + word + tok + return f"{tok}{word}{tok}" diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index 103e829016..5e8b338c7b 100644 --- a/beetsplug/fromfilename.py +++ b/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("Artist replaced with: {.artist}", item) # 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("Title replaced with: {.title}", item) 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("Track replaced with: {.track}", item) # Plugin structure and hook into import process. diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index 150f230aad..f0f0880999 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -20,7 +20,6 @@ from typing import TYPE_CHECKING from beets import plugins, ui -from beets.util import displayable_path if TYPE_CHECKING: from beets.importer import ImportSession, ImportTask @@ -90,7 +89,7 @@ def __init__(self) -> None: { "auto": True, "drop": False, - "format": "feat. {0}", + "format": "feat. {}", "keep_in_artist": False, } ) @@ -151,10 +150,10 @@ def update_metadata( # In case the artist is kept, do not update the artist fields. if keep_in_artist_field: self._log.info( - "artist: {0} (Not changing due to keep_in_artist)", item.artist + "artist: {.artist} (Not changing due to keep_in_artist)", item ) else: - self._log.info("artist: {0} -> {1}", item.artist, item.albumartist) + self._log.info("artist: {0.artist} -> {0.albumartist}", item) item.artist = item.albumartist if item.artist_sort: @@ -167,7 +166,7 @@ def update_metadata( feat_format = self.config["format"].as_str() new_format = feat_format.format(feat_part) new_title = f"{item.title} {new_format}" - self._log.info("title: {0} -> {1}", item.title, new_title) + self._log.info("title: {.title} -> {}", item, new_title) item.title = new_title def ft_in_title( @@ -195,7 +194,7 @@ def ft_in_title( if not featured: return False - self._log.info("{}", displayable_path(item.path)) + self._log.info("{.filepath}", item) # Attempt to find the featured artist. feat_part = find_feat_part(artist, albumartist) diff --git a/beetsplug/hook.py b/beetsplug/hook.py index 90d66553a6..b8869eca40 100644 --- a/beetsplug/hook.py +++ b/beetsplug/hook.py @@ -62,7 +62,7 @@ def __init__(self): def create_and_register_hook(self, event, command): def hook_function(**kwargs): if command is None or len(command) == 0: - self._log.error('invalid command "{0}"', command) + self._log.error('invalid command "{}"', command) return # For backwards compatibility, use a string formatter that decodes @@ -74,7 +74,7 @@ def hook_function(**kwargs): ] self._log.debug( - 'running command "{0}" for event {1}', + 'running command "{}" for event {}', " ".join(command_pieces), event, ) @@ -83,9 +83,9 @@ def hook_function(**kwargs): subprocess.check_call(command_pieces) except subprocess.CalledProcessError as exc: self._log.error( - "hook for {0} exited with status {1}", event, exc.returncode + "hook for {} exited with status {.returncode}", event, exc ) except OSError as exc: - self._log.error("hook for {0} failed: {1}", event, exc) + self._log.error("hook for {} failed: {}", event, exc) self.register_listener(event, hook_function) diff --git a/beetsplug/ihate.py b/beetsplug/ihate.py index d6357294db..54a61384ce 100644 --- a/beetsplug/ihate.py +++ b/beetsplug/ihate.py @@ -70,10 +70,10 @@ def import_task_choice_event(self, session, task): self._log.debug("processing your hate") if self.do_i_hate_this(task, skip_queries): task.choice_flag = Action.SKIP - self._log.info("skipped: {0}", summary(task)) + self._log.info("skipped: {}", summary(task)) return if self.do_i_hate_this(task, warn_queries): - self._log.info("you may hate this: {0}", summary(task)) + self._log.info("you may hate this: {}", summary(task)) else: self._log.debug("nothing to do") else: diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index 2564f26b28..f728a104f2 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -94,7 +94,7 @@ def record_import_mtime(self, item, source, destination): mtime = os.stat(util.syspath(source)).st_mtime self.item_mtime[destination] = mtime self._log.debug( - "Recorded mtime {0} for item '{1}' imported from '{2}'", + "Recorded mtime {} for item '{}' imported from '{}'", mtime, util.displayable_path(destination), util.displayable_path(source), @@ -103,9 +103,9 @@ def record_import_mtime(self, item, source, destination): def update_album_times(self, lib, album): if self.reimported_album(album): self._log.debug( - "Album '{0}' is reimported, skipping import of " + "Album '{.filepath}' is reimported, skipping import of " "added dates for the album and its items.", - util.displayable_path(album.path), + album, ) return @@ -119,18 +119,17 @@ def update_album_times(self, lib, album): item.store() album.added = min(album_mtimes) self._log.debug( - "Import of album '{0}', selected album.added={1} " + "Import of album '{0.album}', selected album.added={0.added} " "from item file mtimes.", - album.album, - album.added, + album, ) album.store() def update_item_times(self, lib, item): if self.reimported_item(item): self._log.debug( - "Item '{0}' is reimported, skipping import of added date.", - util.displayable_path(item.path), + "Item '{.filepath}' is reimported, skipping import of added date.", + item, ) return mtime = self.item_mtime.pop(item.path, None) @@ -139,9 +138,8 @@ def update_item_times(self, lib, item): if self.config["preserve_mtimes"].get(bool): self.write_item_mtime(item, mtime) self._log.debug( - "Import of item '{0}', selected item.added={1}", - util.displayable_path(item.path), - item.added, + "Import of item '{0.filepath}', selected item.added={0.added}", + item, ) item.store() @@ -153,7 +151,6 @@ def update_after_write_time(self, item, path): if self.config["preserve_write_mtimes"].get(bool): self.write_item_mtime(item, item.added) self._log.debug( - "Write of item '{0}', selected item.added={1}", - util.displayable_path(item.path), - item.added, + "Write of item '{0.filepath}', selected item.added={0.added}", + item, ) diff --git a/beetsplug/importfeeds.py b/beetsplug/importfeeds.py index 0a5a6afe46..a74746f8b1 100644 --- a/beetsplug/importfeeds.py +++ b/beetsplug/importfeeds.py @@ -50,7 +50,7 @@ def _build_m3u_filename(basename): path = normpath( os.path.join( config["importfeeds"]["dir"].as_filename(), - date + "_" + basename + ".m3u", + f"{date}_{basename}.m3u", ) ) return path @@ -136,7 +136,7 @@ def _record_items(self, lib, basename, items): if "echo" in formats: self._log.info("Location of imported music:") for path in paths: - self._log.info(" {0}", path) + self._log.info(" {}", path) def album_imported(self, lib, album): self._record_items(lib, album.album, album.items()) diff --git a/beetsplug/info.py b/beetsplug/info.py index c4d5aacbfd..cc78aaffef 100644 --- a/beetsplug/info.py +++ b/beetsplug/info.py @@ -117,7 +117,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)) @@ -126,7 +125,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): @@ -139,12 +138,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): @@ -221,7 +219,7 @@ def run(self, lib, opts, args): try: data, item = data_emitter(included_keys or "*") except (mediafile.UnreadableFileError, OSError) as ex: - self._log.error("cannot read file: {0}", ex) + self._log.error("cannot read file: {}", ex) continue if opts.summarize: diff --git a/beetsplug/inline.py b/beetsplug/inline.py index c4258fc838..e9a94ac380 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -28,8 +28,7 @@ class InlineError(Exception): def __init__(self, code, exc): super().__init__( - ("error in inline path field code:\n%s\n%s: %s") - % (code, type(exc).__name__, str(exc)) + f"error in inline path field code:\n{code}\n{type(exc).__name__}: {exc}" ) @@ -37,7 +36,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) @@ -60,14 +60,14 @@ def __init__(self): for key, view in itertools.chain( config["item_fields"].items(), config["pathfields"].items() ): - self._log.debug("adding item field {0}", key) + self._log.debug("adding item field {}", key) func = self.compile_inline(view.as_str(), False) if func is not None: self.template_fields[key] = func # Album fields. for key, view in config["album_fields"].items(): - self._log.debug("adding album field {0}", key) + self._log.debug("adding album field {}", key) func = self.compile_inline(view.as_str(), True) if func is not None: self.album_template_fields[key] = func @@ -87,7 +87,7 @@ def compile_inline(self, python_code, album): func = _compile_func(python_code) except SyntaxError: self._log.error( - "syntax error in inline field definition:\n{0}", + "syntax error in inline field definition:\n{}", traceback.format_exc(), ) return diff --git a/beetsplug/ipfs.py b/beetsplug/ipfs.py index 3c6425c06b..8b6d57fd31 100644 --- a/beetsplug/ipfs.py +++ b/beetsplug/ipfs.py @@ -77,7 +77,7 @@ def func(lib, opts, args): for album in lib.albums(args): if len(album.items()) == 0: self._log.info( - "{0} does not contain items, aborting", album + "{} does not contain items, aborting", album ) self.ipfs_add(album) @@ -122,13 +122,13 @@ def ipfs_add(self, album): return False try: if album.ipfs: - self._log.debug("{0} already added", album_dir) + self._log.debug("{} already added", album_dir) # Already added to ipfs return False except AttributeError: pass - self._log.info("Adding {0} to ipfs", album_dir) + self._log.info("Adding {} to ipfs", album_dir) if self.config["nocopy"]: cmd = "ipfs add --nocopy -q -r".split() @@ -138,7 +138,7 @@ def ipfs_add(self, album): try: output = util.command_output(cmd).stdout.split() except (OSError, subprocess.CalledProcessError) as exc: - self._log.error("Failed to add {0}, error: {1}", album_dir, exc) + self._log.error("Failed to add {}, error: {}", album_dir, exc) return False length = len(output) @@ -146,12 +146,12 @@ def ipfs_add(self, album): line = line.strip() if linenr == length - 1: # last printed line is the album hash - self._log.info("album: {0}", line) + self._log.info("album: {}", line) album.ipfs = line else: try: item = album.items()[linenr] - self._log.info("item: {0}", line) + self._log.info("item: {}", line) item.ipfs = line item.store() except IndexError: @@ -180,11 +180,11 @@ def ipfs_get_from_hash(self, lib, _hash): util.command_output(cmd) except (OSError, subprocess.CalledProcessError) as err: self._log.error( - "Failed to get {0} from ipfs.\n{1}", _hash, err.output + "Failed to get {} from ipfs.\n{.output}", _hash, err ) return False - self._log.info("Getting {0} from ipfs", _hash) + self._log.info("Getting {} from ipfs", _hash) imp = ui.commands.TerminalImportSession( lib, loghandler=None, query=None, paths=[_hash] ) @@ -208,7 +208,7 @@ def ipfs_publish(self, lib): msg = f"Failed to publish library. Error: {err}" self._log.error(msg) return False - self._log.info("hash of library: {0}", output) + self._log.info("hash of library: {}", output) def ipfs_import(self, lib, args): _hash = args[0] @@ -232,7 +232,7 @@ def ipfs_import(self, lib, args): try: util.command_output(cmd) except (OSError, subprocess.CalledProcessError): - self._log.error(f"Could not import {_hash}") + self._log.error("Could not import {}", _hash) return False # add all albums from remotes into a combined library @@ -306,7 +306,7 @@ def create_new_album(self, album, tmplib): items.append(item) if len(items) < 1: return False - self._log.info("Adding '{0}' to temporary library", album) + self._log.info("Adding '{}' to temporary library", album) new_album = tmplib.add_album(items) new_album.ipfs = album.ipfs new_album.store(inherit=False) diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index 00b688d4fe..e2aff24e59 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -65,7 +65,7 @@ def find_key(self, items, write=False): command + [util.syspath(item.path)] ).stdout except (subprocess.CalledProcessError, OSError) as exc: - self._log.error("execution failed: {0}", exc) + self._log.error("execution failed: {}", exc) continue try: @@ -73,7 +73,7 @@ def find_key(self, items, write=False): except IndexError: # Sometimes keyfinder-cli returns 0 but with no key, usually # when the file is silent or corrupt, so we log and skip. - self._log.error("no key returned for path: {0}", item.path) + self._log.error("no key returned for path: {.path}", item) continue try: @@ -84,9 +84,7 @@ def find_key(self, items, write=False): item["initial_key"] = key self._log.info( - "added computed initial key {0} for {1}", - key, - util.displayable_path(item.path), + "added computed initial key {} for {.filepath}", key, item ) if write: diff --git a/beetsplug/kodiupdate.py b/beetsplug/kodiupdate.py index 2f679c38be..890ab16c4d 100644 --- a/beetsplug/kodiupdate.py +++ b/beetsplug/kodiupdate.py @@ -96,10 +96,10 @@ def update(self, lib): continue self._log.info( - "Kodi update triggered for {0}:{1}", + "Kodi update triggered for {}:{}", instance["host"], instance["port"], ) except requests.exceptions.RequestException as e: - self._log.warning("Kodi update failed: {0}", str(e)) + self._log.warning("Kodi update failed: {}", str(e)) continue diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index b0808e4b97..dacd72f934 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -139,7 +139,7 @@ def setup(self): # Read the tree if c14n_filename: - self._log.debug("Loading canonicalization tree {0}", c14n_filename) + self._log.debug("Loading canonicalization tree {}", c14n_filename) c14n_filename = normpath(c14n_filename) with codecs.open(c14n_filename, "r", encoding="utf-8") as f: genres_tree = yaml.safe_load(f) @@ -277,7 +277,7 @@ def _last_lookup(self, entity, method, *args): genre = self._genre_cache[key] if self.config["extended_debug"]: - self._log.debug(f"last.fm (unfiltered) {entity} tags: {genre}") + self._log.debug("last.fm (unfiltered) {} tags: {}", entity, genre) return genre def fetch_album_genre(self, obj): @@ -327,8 +327,8 @@ def _combine_resolve_and_log( self, old: list[str], new: list[str] ) -> list[str]: """Combine old and new genres and process via _resolve_genres.""" - self._log.debug(f"raw last.fm tags: {new}") - self._log.debug(f"existing genres taken into account: {old}") + self._log.debug("raw last.fm tags: {}", new) + self._log.debug("existing genres taken into account: {}", old) combined = old + new return self._resolve_genres(combined) @@ -361,7 +361,7 @@ def _try_resolve_stage(stage_label: str, keep_genres, new_genres): ) if resolved_genres: suffix = "whitelist" if self.whitelist else "any" - label = stage_label + f", {suffix}" + label = f"{stage_label}, {suffix}" if keep_genres: label = f"keep + {label}" return self._format_and_stringify(resolved_genres), label @@ -583,9 +583,7 @@ def imported(self, session, task): item = task.item item.genre, src = self._get_genre(item) self._log.debug( - 'genre for track "{0.title}" ({1}): {0.genre}', - item, - src, + 'genre for track "{0.title}" ({1}): {0.genre}', item, src ) item.store() @@ -607,12 +605,12 @@ def _tags_for(self, obj, min_weight=None): try: res = obj.get_top_tags() except PYLAST_EXCEPTIONS as exc: - self._log.debug("last.fm error: {0}", exc) + self._log.debug("last.fm error: {}", exc) return [] except Exception as exc: # Isolate bugs in pylast. self._log.debug("{}", traceback.format_exc()) - self._log.error("error in pylast library: {0}", exc) + self._log.error("error in pylast library: {}", exc) return [] # Filter by weight (optionally). diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 122e5f9cd6..baa522d143 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -70,7 +70,7 @@ def _get_things( tuple with the total number of pages of results. Includes an MBID, if found. """ - doc = self._request(self.ws_prefix + "." + method, cacheable, params) + doc = self._request(f"{self.ws_prefix}.{method}", cacheable, params) toptracks_node = doc.getElementsByTagName("toptracks")[0] total_pages = int(toptracks_node.getAttribute("totalPages")) @@ -120,7 +120,7 @@ def import_lastfm(lib, log): if not user: raise ui.UserError("You must specify a user name for lastimport") - log.info("Fetching last.fm library for @{0}", user) + log.info("Fetching last.fm library for @{}", user) page_total = 1 page_current = 0 @@ -130,7 +130,7 @@ def import_lastfm(lib, log): # Iterate through a yet to be known page total count while page_current < page_total: log.info( - "Querying page #{0}{1}...", + "Querying page #{}{}...", page_current + 1, f"/{page_total}" if page_total > 1 else "", ) @@ -147,27 +147,27 @@ def import_lastfm(lib, log): unknown_total += unknown break else: - log.error("ERROR: unable to read page #{0}", page_current + 1) + log.error("ERROR: unable to read page #{}", page_current + 1) if retry < retry_limit: log.info( - "Retrying page #{0}... ({1}/{2} retry)", + "Retrying page #{}... ({}/{} retry)", page_current + 1, retry + 1, retry_limit, ) else: log.error( - "FAIL: unable to fetch page #{0}, ", - "tried {1} times", + "FAIL: unable to fetch page #{}, ", + "tried {} times", page_current, retry + 1, ) page_current += 1 log.info("... done!") - log.info("finished processing {0} song pages", page_total) - log.info("{0} unknown play-counts", unknown_total) - log.info("{0} play-counts imported", found_total) + log.info("finished processing {} song pages", page_total) + log.info("{} unknown play-counts", unknown_total) + log.info("{} play-counts imported", found_total) def fetch_tracks(user, page, limit): @@ -201,7 +201,7 @@ def process_tracks(lib, tracks, log): total = len(tracks) total_found = 0 total_fails = 0 - log.info("Received {0} tracks in this page, processing...", total) + log.info("Received {} tracks in this page, processing...", total) for num in range(0, total): song = None @@ -220,7 +220,7 @@ def process_tracks(lib, tracks, log): else None ) - log.debug("query: {0} - {1} ({2})", artist, title, album) + log.debug("query: {} - {} ({})", artist, title, album) # First try to query by musicbrainz's trackid if trackid: @@ -231,7 +231,7 @@ def process_tracks(lib, tracks, log): # If not, try just album/title if song is None: log.debug( - "no album match, trying by album/title: {0} - {1}", album, title + "no album match, trying by album/title: {} - {}", album, title ) query = dbcore.AndQuery( [ @@ -268,10 +268,9 @@ def process_tracks(lib, tracks, log): count = int(song.get("play_count", 0)) new_count = int(tracks[num].get("playcount", 1)) log.debug( - "match: {0} - {1} ({2}) updating: play_count {3} => {4}", - song.artist, - song.title, - song.album, + "match: {0.artist} - {0.title} ({0.album}) updating:" + " play_count {1} => {2}", + song, count, new_count, ) @@ -280,11 +279,11 @@ def process_tracks(lib, tracks, log): total_found += 1 else: total_fails += 1 - log.info(" - No match: {0} - {1} ({2})", artist, title, album) + log.info(" - No match: {} - {} ({})", artist, title, album) if total_fails > 0: log.info( - "Acquired {0}/{1} play-counts ({2} unknown)", + "Acquired {}/{} play-counts ({} unknown)", total_found, total, total_fails, diff --git a/beetsplug/listenbrainz.py b/beetsplug/listenbrainz.py index c579645db9..f7b1389ef7 100644 --- a/beetsplug/listenbrainz.py +++ b/beetsplug/listenbrainz.py @@ -42,14 +42,14 @@ def _lbupdate(self, lib, log): unknown_total = 0 ls = self.get_listens() tracks = self.get_tracks_from_listens(ls) - log.info(f"Found {len(ls)} listens") + log.info("Found {} listens", len(ls)) if tracks: found, unknown = process_tracks(lib, tracks, log) found_total += found unknown_total += unknown log.info("... done!") - log.info("{0} unknown play-counts", unknown_total) - log.info("{0} play-counts imported", found_total) + log.info("{} unknown play-counts", unknown_total) + log.info("{} play-counts imported", found_total) def _make_request(self, url, params=None): """Makes a request to the ListenBrainz API.""" @@ -63,7 +63,7 @@ def _make_request(self, url, params=None): response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: - self._log.debug(f"Invalid Search Error: {e}") + self._log.debug("Invalid Search Error: {}", e) return None def get_listens(self, min_ts=None, max_ts=None, count=None): @@ -156,7 +156,7 @@ def get_listenbrainz_playlists(self): playlist_info = playlist.get("playlist") if playlist_info.get("creator") == "listenbrainz": title = playlist_info.get("title") - self._log.debug(f"Playlist title: {title}") + self._log.debug("Playlist title: {}", title) playlist_type = ( "Exploration" if "Exploration" in title else "Jams" ) @@ -179,9 +179,7 @@ def get_listenbrainz_playlists(self): listenbrainz_playlists, key=lambda x: x["date"], reverse=True ) for playlist in listenbrainz_playlists: - self._log.debug( - f"Playlist: {playlist['type']} - {playlist['date']}" - ) + self._log.debug("Playlist: {0[type]} - {0[date]}", playlist) return listenbrainz_playlists def get_playlist(self, identifier): diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index f1c40ab24d..f492ab3ccd 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -154,7 +154,7 @@ def generate_alternatives(string, patterns): # 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*:.*", ] @@ -508,9 +508,9 @@ def check_match( # log out the candidate that did not make it but was close. # This may show a matching candidate with some noise in the name self.debug( - "({}, {}) does not match ({}, {}) but dist was close: {:.2f}", - result.artist, - result.title, + "({0.artist}, {0.title}) does not match ({1}, {2}) but dist" + " was close: {3:.2f}", + result, target_artist, target_title, max_dist, @@ -582,7 +582,7 @@ class Tekstowo(SearchBackend): """Fetch lyrics from Tekstowo.pl.""" BASE_URL = "https://www.tekstowo.pl" - SEARCH_URL = BASE_URL + "/szukaj,{}.html" + SEARCH_URL = f"{BASE_URL}/szukaj,{{}}.html" def build_url(self, artist, title): artistitle = f"{artist.title()} {title.title()}" @@ -644,7 +644,7 @@ class Google(SearchBackend): re.IGNORECASE | re.VERBOSE, ) #: Split cleaned up URL title into artist and title parts. - URL_TITLE_PARTS_RE = re.compile(r" +(?:[ :|-]+|par|by) +") + URL_TITLE_PARTS_RE = re.compile(r" +(?:[ :|-]+|par|by) +|, ") SOURCE_DIST_FACTOR = {"www.azlyrics.com": 0.5, "www.songlyrics.com": 0.6} @@ -702,8 +702,8 @@ def make_search_result( result_artist, result_title = "", parts[0] else: # sort parts by their similarity to the artist - parts.sort(key=lambda p: cls.get_part_dist(artist, title, p)) - result_artist, result_title = parts[0], " ".join(parts[1:]) + result_artist = min(parts, key=lambda p: string_dist(artist, p)) + result_title = min(parts, key=lambda p: string_dist(title, p)) return SearchResult(result_artist, result_title, item["link"]) @@ -838,15 +838,16 @@ def translate(self, new_lyrics: str, old_lyrics: str) -> str: lyrics_language = langdetect.detect(new_lyrics).upper() if lyrics_language == self.to_language: self.info( - "🔵 Lyrics are already in the target language {}", - self.to_language, + "🔵 Lyrics are already in the target language {.to_language}", + self, ) return new_lyrics if self.from_languages and lyrics_language not in self.from_languages: self.info( - "🔵 Configuration {} does not permit translating from {}", - self.from_languages, + "🔵 Configuration {.from_languages} does not permit translating" + " from {}", + self, lyrics_language, ) return new_lyrics @@ -854,7 +855,7 @@ def translate(self, new_lyrics: str, old_lyrics: str) -> str: lyrics, *url = new_lyrics.split("\n\nSource: ") with self.handle_request(): translated_lines = self.append_translations(lyrics.splitlines()) - self.info("🟢 Translated lyrics to {}", self.to_language) + self.info("🟢 Translated lyrics to {.to_language}", self) return "\n\nSource: ".join(["\n".join(translated_lines), *url]) @@ -1090,7 +1091,7 @@ def add_item_lyrics(self, item: Item, write: bool) -> None: return if lyrics := self.find_lyrics(item): - self.info("🟢 Found lyrics: {0}", item) + self.info("🟢 Found lyrics: {}", item) if translator := self.translator: lyrics = translator.translate(lyrics, item.lyrics) else: diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index 7a1289d1b5..2f9ef709e2 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -83,9 +83,7 @@ def _get_collection(self): 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 @@ -156,10 +154,10 @@ def update_album_list(self, lib, album_list, remove_missing=False): if re.match(UUID_REGEX, aid): album_ids.append(aid) else: - self._log.info("skipping invalid MBID: {0}", aid) + self._log.info("skipping invalid MBID: {}", aid) # Submit to MusicBrainz. - self._log.info("Updating MusicBrainz collection {0}...", collection_id) + self._log.info("Updating MusicBrainz collection {}...", collection_id) submit_albums(collection_id, album_ids) if remove_missing: self.remove_missing(collection_id, lib.albums()) diff --git a/beetsplug/mbsubmit.py b/beetsplug/mbsubmit.py index e23c0d6102..93e88dc9e9 100644 --- a/beetsplug/mbsubmit.py +++ b/beetsplug/mbsubmit.py @@ -73,7 +73,7 @@ def picard(self, session, task): subprocess.Popen([picard_path] + paths) self._log.info("launched picard from\n{}", picard_path) except OSError as exc: - self._log.error(f"Could not open picard, got error:\n{exc}") + self._log.error("Could not open picard, got error:\n{}", exc) def print_tracks(self, session, task): for i in sorted(task.items, key=lambda i: i.track): diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py index f99e820b59..d4e31851ef 100644 --- a/beetsplug/metasync/__init__.py +++ b/beetsplug/metasync/__init__.py @@ -49,7 +49,7 @@ def load_meta_sources(): meta_sources = {} for module_path, class_name in SOURCES.items(): - module = import_module(METASYNC_MODULE + "." + module_path) + module = import_module(f"{METASYNC_MODULE}.{module_path}") meta_sources[class_name.lower()] = getattr(module, class_name) return meta_sources @@ -117,13 +117,13 @@ def func(self, lib, opts, args): try: cls = META_SOURCES[player] except KeyError: - self._log.error("Unknown metadata source '{}'".format(player)) + self._log.error("Unknown metadata source '{}'", player) try: meta_source_instances[player] = cls(self.config, self._log) except (ImportError, ConfigValueError) as e: self._log.error( - f"Failed to instantiate metadata source {player!r}: {e}" + "Failed to instantiate metadata source {!r}: {}", player, e ) # Avoid needlessly iterating over items diff --git a/beetsplug/metasync/amarok.py b/beetsplug/metasync/amarok.py index 9afe6dbca8..47e6a1a65c 100644 --- a/beetsplug/metasync/amarok.py +++ b/beetsplug/metasync/amarok.py @@ -44,11 +44,12 @@ class Amarok(MetaSource): "amarok_lastplayed": types.DATE, } - query_xml = ' \ - \ - \ - \ - ' + query_xml = """ + + + + + """ def __init__(self, config, log): super().__init__(config, log) @@ -68,7 +69,7 @@ def sync_from_source(self, item): # of the result set. So query for the filename and then try to match # the correct item from the results we get back results = self.collection.Query( - self.query_xml % quoteattr(basename(path)) + self.query_xml.format(quoteattr(basename(path))) ) for result in results: if result["xesam:url"] != path: diff --git a/beetsplug/metasync/itunes.py b/beetsplug/metasync/itunes.py index f777d0d55a..6f441ef8b5 100644 --- a/beetsplug/metasync/itunes.py +++ b/beetsplug/metasync/itunes.py @@ -76,12 +76,12 @@ def __init__(self, config, log): library_path = config["itunes"]["library"].as_filename() try: - self._log.debug(f"loading iTunes library from {library_path}") + self._log.debug("loading iTunes library from {}", library_path) with create_temporary_copy(library_path) as library_copy: with open(library_copy, "rb") as library_copy_f: raw_library = plistlib.load(library_copy_f) except OSError as e: - raise ConfigValueError("invalid iTunes library: " + e.strerror) + raise ConfigValueError(f"invalid iTunes library: {e.strerror}") except Exception: # It's likely the user configured their '.itl' library (<> xml) if os.path.splitext(library_path)[1].lower() != ".xml": @@ -91,7 +91,7 @@ def __init__(self, config, log): ) else: hint = "" - raise ConfigValueError("invalid iTunes library" + hint) + raise ConfigValueError(f"invalid iTunes library{hint}") # Make the iTunes library queryable using the path self.collection = { @@ -104,7 +104,7 @@ def sync_from_source(self, item): result = self.collection.get(util.bytestring_path(item.path).lower()) if not result: - self._log.warning(f"no iTunes match found for {item}") + self._log.warning("no iTunes match found for {}", item) return item.itunes_rating = result.get("Rating") diff --git a/beetsplug/missing.py b/beetsplug/missing.py index d0e956930f..cbdda4599b 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -226,8 +226,8 @@ def _missing(self, album: Album) -> Iterator[Item]: for track_info in album_info.tracks: if track_info.track_id not in item_mbids: self._log.debug( - "track {0} in album {1}", - track_info.track_id, - album_info.album_id, + "track {.track_id} in album {.album_id}", + track_info, + album_info, ) yield _item(track_info, album_info, album.id) diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 52ae88e1f2..0a3e1de02a 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -51,8 +51,8 @@ def __init__(self, log): if not self.strip_path.endswith("/"): self.strip_path += "/" - self._log.debug("music_directory: {0}", self.music_directory) - self._log.debug("strip_path: {0}", self.strip_path) + self._log.debug("music_directory: {.music_directory}", self) + self._log.debug("strip_path: {.strip_path}", self) self.client = mpd.MPDClient() @@ -64,7 +64,7 @@ def connect(self): if host[0] in ["/", "~"]: host = os.path.expanduser(host) - self._log.info("connecting to {0}:{1}", host, port) + self._log.info("connecting to {}:{}", host, port) try: self.client.connect(host, port) except OSError as e: @@ -89,7 +89,7 @@ def get(self, command, retries=RETRIES): try: return getattr(self.client, command)() except (OSError, mpd.ConnectionError) as err: - self._log.error("{0}", err) + self._log.error("{}", err) if retries <= 0: # if we exited without breaking, we couldn't reconnect in time :( @@ -123,7 +123,7 @@ def currentsong(self): result = os.path.join(self.music_directory, file) else: result = entry["file"] - self._log.debug("returning: {0}", result) + self._log.debug("returning: {}", result) return result, entry.get("id") def status(self): @@ -169,7 +169,7 @@ def get_item(self, path): if item: return item else: - self._log.info("item not found: {0}", displayable_path(path)) + self._log.info("item not found: {}", displayable_path(path)) def update_item(self, item, attribute, value=None, increment=None): """Update the beets item. Set attribute to value or increment the value @@ -188,10 +188,10 @@ def update_item(self, item, attribute, value=None, increment=None): item.store() self._log.debug( - "updated: {0} = {1} [{2}]", + "updated: {} = {} [{.filepath}]", attribute, item[attribute], - displayable_path(item.path), + item, ) def update_rating(self, item, skipped): @@ -234,12 +234,12 @@ def handle_song_change(self, song): def handle_played(self, song): """Updates the play count of a song.""" self.update_item(song["beets_item"], "play_count", increment=1) - self._log.info("played {0}", displayable_path(song["path"])) + self._log.info("played {}", displayable_path(song["path"])) def handle_skipped(self, song): """Updates the skip count of a song.""" self.update_item(song["beets_item"], "skip_count", increment=1) - self._log.info("skipped {0}", displayable_path(song["path"])) + self._log.info("skipped {}", displayable_path(song["path"])) def on_stop(self, status): self._log.info("stop") @@ -278,11 +278,11 @@ def on_play(self, status): self.handle_song_change(self.now_playing) if is_url(path): - self._log.info("playing stream {0}", displayable_path(path)) + self._log.info("playing stream {}", displayable_path(path)) self.now_playing = None return - self._log.info("playing {0}", displayable_path(path)) + self._log.info("playing {}", displayable_path(path)) self.now_playing = { "started": time.time(), @@ -307,12 +307,12 @@ def run(self): if "player" in events: status = self.mpd.status() - handler = getattr(self, "on_" + status["state"], None) + handler = getattr(self, f"on_{status['state']}", None) if handler: handler(status) else: - self._log.debug('unhandled status "{0}"', status) + self._log.debug('unhandled status "{}"', status) events = self.mpd.events() diff --git a/beetsplug/mpdupdate.py b/beetsplug/mpdupdate.py index cb53afaa57..5d8fc598ba 100644 --- a/beetsplug/mpdupdate.py +++ b/beetsplug/mpdupdate.py @@ -101,8 +101,8 @@ def update_mpd(self, host="localhost", port=6600, password=None): try: s = BufferedSocket(host, port) - except OSError as e: - self._log.warning("MPD connection failed: {0}", str(e.strerror)) + except OSError: + self._log.warning("MPD connection failed", exc_info=True) return resp = s.readline() @@ -111,7 +111,7 @@ def update_mpd(self, host="localhost", port=6600, password=None): return if password: - s.send(b'password "%s"\n' % password.encode("utf8")) + s.send(f'password "{password}"\n'.encode()) resp = s.readline() if b"OK" not in resp: self._log.warning("Authentication failed: {0!r}", resp) diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index b52e44b239..524fb3c8cb 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -68,9 +68,7 @@ def __init__(self, reason, verb, query, tb=None): 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}" RELEASE_INCLUDES = list( @@ -203,7 +201,7 @@ def _multi_artist_credit( def track_url(trackid: str) -> str: - return urljoin(BASE_URL, "recording/" + trackid) + return urljoin(BASE_URL, f"recording/{trackid}") def _flatten_artist_credit(credit: list[JSONDict]) -> tuple[str, str, str]: @@ -248,7 +246,7 @@ def _get_related_artist_names(relations, relation_type): def album_url(albumid: str) -> str: - return urljoin(BASE_URL, "release/" + albumid) + return urljoin(BASE_URL, f"release/{albumid}") def _preferred_release_event( @@ -293,7 +291,7 @@ def _set_date_str( continue if original: - key = "original_" + key + key = f"original_{key}" setattr(info, key, date_num) @@ -838,7 +836,7 @@ def album_for_id( """ self._log.debug("Requesting MusicBrainz release {}", album_id) if not (albumid := self._extract_id(album_id)): - self._log.debug("Invalid MBID ({0}).", album_id) + self._log.debug("Invalid MBID ({}).", album_id) return None try: @@ -875,7 +873,7 @@ def track_for_id( or None if no track is found. May raise a MusicBrainzAPIError. """ if not (trackid := self._extract_id(track_id)): - self._log.debug("Invalid MBID ({0}).", track_id) + self._log.debug("Invalid MBID ({}).", track_id) return None try: diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index ab2d39b2b8..eb2fd8f117 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -179,10 +179,8 @@ def find_work(self, item, force, verbose): if not item.mb_workid: self._log.info( - "No work for {}, \ -add one at https://musicbrainz.org/recording/{}", + "No work for {0}, add one at https://musicbrainz.org/recording/{0.mb_trackid}", item, - item.mb_trackid, ) return diff --git a/beetsplug/play.py b/beetsplug/play.py index 3e7ba0a9e6..35b4b1f764 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -43,7 +43,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: @@ -154,7 +154,7 @@ def _command_str(self, args=None): return f"{command_str} {args}" else: # Don't include the marker in the command. - return command_str.replace(" " + ARGS_MARKER, "") + return command_str.replace(f" {ARGS_MARKER}", "") def _playlist_or_paths(self, paths): """Return either the raw paths of items or a playlist of the items.""" @@ -179,9 +179,7 @@ def _exceeds_threshold( 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 a/beetsplug/playlist.py b/beetsplug/playlist.py index 7a27b02a32..07c12e0e0c 100644 --- a/beetsplug/playlist.py +++ b/beetsplug/playlist.py @@ -123,7 +123,7 @@ def item_removed(self, item): def cli_exit(self, lib): for playlist in self.find_playlists(): - self._log.info(f"Updating playlist: {playlist}") + self._log.info("Updating playlist: {}", playlist) base_dir = beets.util.bytestring_path( self.relative_to if self.relative_to @@ -133,21 +133,16 @@ def cli_exit(self, lib): try: self.update_playlist(playlist, base_dir) except beets.util.FilesystemError: - self._log.error( - "Failed to update playlist: {}".format( - beets.util.displayable_path(playlist) - ) - ) + self._log.error("Failed to update playlist: {}", playlist) def find_playlists(self): """Find M3U playlists in the playlist directory.""" + playlist_dir = beets.util.syspath(self.playlist_dir) try: - dir_contents = os.listdir(beets.util.syspath(self.playlist_dir)) + dir_contents = os.listdir(playlist_dir) except OSError: self._log.warning( - "Unable to open playlist directory {}".format( - beets.util.displayable_path(self.playlist_dir) - ) + "Unable to open playlist directory {.playlist_dir}", self ) return @@ -195,9 +190,10 @@ def update_playlist(self, filename, base_dir): if changes or deletions: self._log.info( - "Updated playlist {} ({} changes, {} deletions)".format( - filename, changes, deletions - ) + "Updated playlist {} ({} changes, {} deletions)", + filename, + changes, + deletions, ) beets.util.copy(new_playlist, filename, replace=True) beets.util.remove(new_playlist) diff --git a/beetsplug/plexupdate.py b/beetsplug/plexupdate.py index 9b4419c715..5e255d45b7 100644 --- a/beetsplug/plexupdate.py +++ b/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( @@ -70,7 +66,7 @@ def update_plex(host, port, token, library_name, secure, ignore_cert_errors): def append_token(url, token): """Appends the Plex Home token to the api call if required.""" if token: - url += "?" + urlencode({"X-Plex-Token": token}) + url += f"?{urlencode({'X-Plex-Token': token})}" return url diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 96c8543147..3e777d9773 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -70,9 +70,7 @@ def call(args: list[str], 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}") def db_to_lufs(db: float) -> float: @@ -143,9 +141,8 @@ def _store_track_gain(self, item: Item, track_gain: Gain): item.rg_track_peak = track_gain.peak item.store() self._log.debug( - "applied track gain {0} LU, peak {1} of FS", - item.rg_track_gain, - item.rg_track_peak, + "applied track gain {0.rg_track_gain} LU, peak {0.rg_track_peak} of FS", + item, ) def _store_album_gain(self, item: Item, album_gain: Gain): @@ -157,9 +154,8 @@ def _store_album_gain(self, item: Item, album_gain: Gain): item.rg_album_peak = album_gain.peak item.store() self._log.debug( - "applied album gain {0} LU, peak {1} of FS", - item.rg_album_gain, - item.rg_album_peak, + "applied album gain {0.rg_album_gain} LU, peak {0.rg_album_peak} of FS", + item, ) def _store_track(self, write: bool): @@ -170,15 +166,14 @@ def _store_track(self, write: bool): # `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" + f" {item}" ) self._store_track_gain(item, self.track_gains[0]) if write: item.try_write() - self._log.debug("done analyzing {0}", item) + self._log.debug("done analyzing {}", item) def _store_album(self, write: bool): """Store track/album gains for all tracks of the task in the database.""" @@ -191,17 +186,15 @@ def _store_album(self, write: bool): # `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) self._store_album_gain(item, self.album_gain) if write: item.try_write() - self._log.debug("done analyzing {0}", item) + self._log.debug("done analyzing {}", item) def store(self, write: bool): """Store computed gains for the items of this task in the database.""" @@ -235,7 +228,7 @@ def __init__( def _store_track_gain(self, item: Item, track_gain: Gain): item.r128_track_gain = track_gain.gain item.store() - self._log.debug("applied r128 track gain {0} LU", item.r128_track_gain) + self._log.debug("applied r128 track gain {.r128_track_gain} LU", item) def _store_album_gain(self, item: Item, album_gain: Gain): """ @@ -244,7 +237,7 @@ def _store_album_gain(self, item: Item, album_gain: Gain): """ item.r128_album_gain = album_gain.gain item.store() - self._log.debug("applied r128 album gain {0} LU", item.r128_album_gain) + self._log.debug("applied r128 album gain {.r128_album_gain} LU", item) AnyRgTask = TypeVar("AnyRgTask", bound=RgTask) @@ -385,10 +378,7 @@ def sum_of_track_powers(track_gain: Gain, track_n_blocks: int): album_gain = target_level_lufs - album_gain self._log.debug( - "{}: gain {} LU, peak {}", - task.album, - album_gain, - album_peak, + "{.album}: gain {} LU, peak {}", task, album_gain, album_peak ) task.album_gain = Gain(album_gain, album_peak) @@ -431,9 +421,9 @@ def _analyse_item( target_level_lufs = db_to_lufs(target_level) # call ffmpeg - self._log.debug(f"analyzing {item}") + self._log.debug("analyzing {}", item) cmd = self._construct_cmd(item, peak_method) - self._log.debug("executing {0}", " ".join(map(displayable_path, cmd))) + self._log.debug("executing {}", " ".join(map(displayable_path, cmd))) output = call(cmd, self._log).stderr.splitlines() # parse output @@ -501,12 +491,10 @@ def _analyse_item( 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 - ) + "{}: {} blocks over {} LUFS", item, n_blocks, gating_threshold ) - self._log.debug("{}: gain {} LU, peak {}".format(item, gain, peak)) + self._log.debug("{}: gain {} LU, peak {}", item, gain, peak) return Gain(gain, peak), n_blocks @@ -526,9 +514,7 @@ def _find_line( 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: @@ -575,7 +561,7 @@ def __init__(self, config: ConfigView, log: Logger): # 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. @@ -663,8 +649,8 @@ def compute_gain( cmd = cmd + ["-d", str(int(target_level - 89))] cmd = cmd + [syspath(i.path) for i in items] - self._log.debug("analyzing {0} files", len(items)) - self._log.debug("executing {0}", " ".join(map(displayable_path, cmd))) + self._log.debug("analyzing {} files", len(items)) + self._log.debug("executing {}", " ".join(map(displayable_path, cmd))) output = call(cmd, self._log).stdout self._log.debug("analysis finished") return self.parse_tool_output( @@ -680,7 +666,7 @@ def parse_tool_output(self, text: bytes, num_lines: int) -> list[Gain]: for line in text.split(b"\n")[1 : num_lines + 1]: parts = line.split(b"\t") if len(parts) != 6 or parts[0] == b"File": - self._log.debug("bad tool output: {0}", text) + self._log.debug("bad tool output: {}", text) raise ReplayGainError("mp3gain failed") # _file = parts[0] @@ -1105,9 +1091,8 @@ def _compute_track_gain(self, item: Item, target_level: float): ) self._log.debug( - "ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}", - item.artist, - item.title, + "ReplayGain for track {0.artist} - {0.title}: {1:.2f}, {2:.2f}", + item, rg_track_gain, rg_track_peak, ) @@ -1132,7 +1117,7 @@ def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask: ) track_gains.append(Gain(gain=rg_track_gain, peak=rg_track_peak)) self._log.debug( - "ReplayGain for track {0}: {1:.2f}, {2:.2f}", + "ReplayGain for track {}: {.2f}, {.2f}", item, rg_track_gain, rg_track_peak, @@ -1145,8 +1130,8 @@ def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask: rg_album_gain, task.target_level ) self._log.debug( - "ReplayGain for album {0}: {1:.2f}, {2:.2f}", - task.items[0].album, + "ReplayGain for album {.items[0].album}: {.2f}, {.2f}", + task, rg_album_gain, rg_album_peak, ) @@ -1229,10 +1214,8 @@ def __init__(self) -> None: 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" + f" supported. Please select one of: {', '.join(BACKENDS)}" ) # FIXME: Consider renaming the configuration option to 'peak_method' @@ -1240,10 +1223,9 @@ def __init__(self) -> None: 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. Please select one of:" + f" {', '.join(PeakMethod.__members__)}" ) # This only applies to plain old rg tags, r128 doesn't store peak # values. @@ -1348,19 +1330,19 @@ def handle_album(self, album: Album, write: bool, force: bool = False): items, nothing is done. """ if not force and not self.album_requires_gain(album): - self._log.info("Skipping album {0}", album) + self._log.info("Skipping album {}", album) return items_iter = iter(album.items()) use_r128 = self.should_use_r128(next(items_iter)) if any(use_r128 != self.should_use_r128(i) for i in items_iter): self._log.error( - "Cannot calculate gain for album {0} (incompatible formats)", + "Cannot calculate gain for album {} (incompatible formats)", album, ) return - self._log.info("analyzing {0}", album) + self._log.info("analyzing {}", album) discs: dict[int, list[Item]] = {} if self.config["per_disc"].get(bool): @@ -1384,7 +1366,7 @@ def store_cb(task: RgTask): callback=store_cb, ) except ReplayGainError as e: - self._log.info("ReplayGain error: {0}", e) + self._log.info("ReplayGain error: {}", e) except FatalReplayGainError as e: raise ui.UserError(f"Fatal replay gain error: {e}") @@ -1396,7 +1378,7 @@ def handle_track(self, item: Item, write: bool, force: bool = False): in the item, nothing is done. """ if not force and not self.track_requires_gain(item): - self._log.info("Skipping track {0}", item) + self._log.info("Skipping track {}", item) return use_r128 = self.should_use_r128(item) @@ -1413,7 +1395,7 @@ def store_cb(task: RgTask): callback=store_cb, ) except ReplayGainError as e: - self._log.info("ReplayGain error: {0}", e) + self._log.info("ReplayGain error: {}", e) except FatalReplayGainError as e: raise ui.UserError(f"Fatal replay gain error: {e}") @@ -1526,18 +1508,16 @@ def command_func( if opts.album: albums = lib.albums(args) self._log.info( - "Analyzing {} albums ~ {} backend...".format( - len(albums), self.backend_name - ) + f"Analyzing {len(albums)} albums ~" + f" {self.backend_name} backend..." ) for album in albums: self.handle_album(album, write, force) else: items = lib.items(args) self._log.info( - "Analyzing {} tracks ~ {} backend...".format( - len(items), self.backend_name - ) + f"Analyzing {len(items)} tracks ~" + f" {self.backend_name} backend..." ) for item in items: self.handle_track(item, write, force) @@ -1556,8 +1536,10 @@ def commands(self) -> list[ui.Subcommand]: "--threads", dest="threads", type=int, - help="change the number of threads, \ - defaults to maximum available processors", + help=( + "change the number of threads, defaults to maximum available" + " processors" + ), ) cmd.parser.add_option( "-f", @@ -1565,8 +1547,10 @@ def commands(self) -> list[ui.Subcommand]: dest="force", action="store_true", default=False, - help="analyze all files, including those that " - "already have ReplayGain metadata", + help=( + "analyze all files, including those that already have" + " ReplayGain metadata" + ), ) cmd.parser.add_option( "-w", diff --git a/beetsplug/rewrite.py b/beetsplug/rewrite.py index 83829d6579..1cc21ad75e 100644 --- a/beetsplug/rewrite.py +++ b/beetsplug/rewrite.py @@ -57,9 +57,9 @@ def __init__(self): raise ui.UserError("invalid rewrite specification") if fieldname not in library.Item._fields: raise ui.UserError( - "invalid field name (%s) in rewriter" % fieldname + f"invalid field name ({fieldname}) in rewriter" ) - self._log.debug("adding template field {0}", key) + self._log.debug("adding template field {}", key) pattern = re.compile(pattern.lower()) rules[fieldname].append((pattern, value)) if fieldname == "artist": diff --git a/beetsplug/scrub.py b/beetsplug/scrub.py index 813effb5ff..c398941372 100644 --- a/beetsplug/scrub.py +++ b/beetsplug/scrub.py @@ -59,9 +59,7 @@ def commands(self): def scrub_func(lib, opts, args): # Walk through matching files and remove tags. for item in lib.items(args): - self._log.info( - "scrubbing: {0}", util.displayable_path(item.path) - ) + self._log.info("scrubbing: {.filepath}", item) self._scrub_item(item, opts.write) scrub_cmd = ui.Subcommand("scrub", help="clean audio tags") @@ -110,7 +108,7 @@ def _scrub(self, path): f.save() except (OSError, mutagen.MutagenError) as exc: self._log.error( - "could not scrub {0}: {1}", util.displayable_path(path), exc + "could not scrub {}: {}", util.displayable_path(path), exc ) def _scrub_item(self, item, restore): @@ -124,7 +122,7 @@ def _scrub_item(self, item, restore): util.syspath(item.path), config["id3v23"].get(bool) ) except mediafile.UnreadableFileError as exc: - self._log.error("could not open file to scrub: {0}", exc) + self._log.error("could not open file to scrub: {}", exc) return images = mf.images @@ -144,12 +142,10 @@ def _scrub_item(self, item, restore): mf.images = images mf.save() except mediafile.UnreadableFileError as exc: - self._log.error("could not write tags: {0}", exc) + self._log.error("could not write tags: {}", exc) def import_task_files(self, session, task): """Automatically scrub imported files.""" for item in task.imported_items(): - self._log.debug( - "auto-scrubbing {0}", util.displayable_path(item.path) - ) + self._log.debug("auto-scrubbing {.filepath}", item) self._scrub_item(item, ui.should_write()) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index e65d596499..8203ce4efd 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -138,10 +138,9 @@ def update_cmd(self, lib, opts, args): if name in args } if not playlists: + unmatched = [name for name, _, _ in self._unmatched_playlists] raise ui.UserError( - "No playlist matching any of {} found".format( - [name for name, _, _ in self._unmatched_playlists] - ) + f"No playlist matching any of {unmatched} found" ) self._matched_playlists = playlists @@ -235,7 +234,7 @@ def db_change(self, lib, model): for playlist in self._unmatched_playlists: n, (q, _), (a_q, _) = playlist if self.matches(model, q, a_q): - self._log.debug("{0} will be updated because of {1}", n, model) + self._log.debug("{} will be updated because of {}", n, model) self._matched_playlists.add(playlist) self.register_listener("cli_exit", self.update_playlists) @@ -244,12 +243,12 @@ def db_change(self, lib, model): def update_playlists(self, lib, pretend=False): if pretend: self._log.info( - "Showing query results for {0} smart playlists...", + "Showing query results for {} smart playlists...", len(self._matched_playlists), ) else: self._log.info( - "Updating {0} smart playlists...", len(self._matched_playlists) + "Updating {} smart playlists...", len(self._matched_playlists) ) playlist_dir = self.config["playlist_dir"].as_filename() @@ -268,7 +267,7 @@ def update_playlists(self, lib, pretend=False): if pretend: self._log.info("Results for playlist {}:", name) else: - self._log.info("Creating playlist {0}", name) + self._log.info("Creating playlist {}", name) items = [] if query: @@ -331,8 +330,9 @@ def update_playlists(self, lib, pretend=False): for key, value in attr ] attrs = "".join(al) - comment = "#EXTINF:{}{},{} - {}\n".format( - int(item.length), attrs, item.artist, item.title + comment = ( + f"#EXTINF:{int(item.length)}{attrs}," + f"{item.artist} - {item.title}\n" ) f.write(comment.encode("utf-8") + entry.uri + b"\n") # Send an event when playlists were updated. @@ -340,13 +340,11 @@ def update_playlists(self, lib, pretend=False): if pretend: self._log.info( - "Displayed results for {0} playlists", + "Displayed results for {} playlists", len(self._matched_playlists), ) else: - self._log.info( - "{0} playlists updated", len(self._matched_playlists) - ) + self._log.info("{} playlists updated", len(self._matched_playlists)) class PlaylistItem: diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index f45ed158b7..d839273286 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -168,8 +168,9 @@ def _authenticate(self) -> None: c_secret: str = self.config["client_secret"].as_str() headers = { - "Authorization": "Basic {}".format( - base64.b64encode(f"{c_id}:{c_secret}".encode()).decode() + "Authorization": ( + "Basic" + f" {base64.b64encode(f'{c_id}:{c_secret}'.encode()).decode()}" ) } response = requests.post( @@ -182,14 +183,12 @@ def _authenticate(self) -> None: 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"] # Save the token for later use. - self._log.debug( - "{} access token: {}", self.data_source, self.access_token - ) + self._log.debug("{0.data_source} access token: {0.access_token}", self) with open(self._tokenfile(), "w") as f: json.dump({"access_token": self.access_token}, f) @@ -227,16 +226,16 @@ def _handle_response( self._log.error("ReadTimeout.") raise APIError("Request timed out.") except requests.exceptions.ConnectionError as e: - self._log.error(f"Network error: {e}") + self._log.error("Network error: {}", e) raise APIError("Network error.") except requests.exceptions.RequestException as e: if e.response is None: - self._log.error(f"Request failed: {e}") + self._log.error("Request failed: {}", e) raise APIError("Request failed.") if e.response.status_code == 401: self._log.debug( - f"{self.data_source} access token has expired. " - f"Reauthenticating." + "{.data_source} access token has expired. Reauthenticating.", + self, ) self._authenticate() return self._handle_response( @@ -255,7 +254,7 @@ def _handle_response( "Retry-After", DEFAULT_WAITING_TIME ) self._log.debug( - f"Too many API requests. Retrying after {seconds} seconds." + "Too many API requests. Retrying after {} seconds.", seconds ) time.sleep(int(seconds) + 1) return self._handle_response( @@ -276,7 +275,7 @@ def _handle_response( f"URL:\n{url}\nparams:\n{params}" ) else: - self._log.error(f"Request failed. Error: {e}") + self._log.error("Request failed. Error: {}", e) raise APIError("Request failed.") def album_for_id(self, album_id: str) -> AlbumInfo | None: @@ -291,7 +290,9 @@ def album_for_id(self, album_id: str) -> AlbumInfo | None: if not (spotify_id := self._extract_id(album_id)): return None - album_data = self._handle_response("get", self.album_url + spotify_id) + album_data = self._handle_response( + "get", f"{self.album_url}{spotify_id}" + ) if album_data["name"] == "": self._log.debug("Album removed from Spotify: {}", album_id) return None @@ -314,9 +315,7 @@ def album_for_id(self, album_id: str) -> AlbumInfo | None: 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"] @@ -409,7 +408,7 @@ def track_for_id(self, track_id: str) -> None | TrackInfo: # release) and `track.medium_total` (total number of tracks on # the track's disc). album_data = self._handle_response( - "get", self.album_url + track_data["album"]["id"] + "get", f"{self.album_url}{track_data['album']['id']}" ) medium_total = 0 for i, track_data in enumerate(album_data["tracks"]["items"], start=1): @@ -438,7 +437,7 @@ def _search_api( filters=filters, query_string=query_string ) - self._log.debug(f"Searching {self.data_source} for '{query}'") + self._log.debug("Searching {.data_source} for '{}'", self, query) try: response = self._handle_response( "get", @@ -448,11 +447,11 @@ def _search_api( except APIError as e: self._log.debug("Spotify API error: {}", e) return () - response_data = response.get(query_type + "s", {}).get("items", []) + response_data = response.get(f"{query_type}s", {}).get("items", []) self._log.debug( - "Found {} result(s) from {} for '{}'", + "Found {} result(s) from {.data_source} for '{}'", len(response_data), - self.data_source, + self, query, ) return response_data @@ -472,17 +471,17 @@ def queries(lib, opts, args): "-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 @@ -515,7 +514,7 @@ def _parse_opts(self, opts): if self.config["mode"].get() not in ["list", "open"]: self._log.warning( - "{0} is not a valid mode", self.config["mode"].get() + "{} is not a valid mode", self.config["mode"].get() ) return False @@ -538,8 +537,8 @@ def _match_library_tracks(self, library: Library, keywords: str): if not items: self._log.debug( - "Your beets query returned no items, skipping {}.", - self.data_source, + "Your beets query returned no items, skipping {.data_source}.", + self, ) return @@ -594,8 +593,8 @@ def _match_library_tracks(self, library: Library, keywords: str): or self.config["tiebreak"].get() == "first" ): self._log.debug( - "{} track(s) found, count: {}", - self.data_source, + "{.data_source} track(s) found, count: {}", + self, len(response_data_tracks), ) chosen_result = response_data_tracks[0] @@ -618,19 +617,19 @@ def _match_library_tracks(self, library: Library, keywords: str): if failure_count > 0: if self.config["show_failures"].get(): self._log.info( - "{} track(s) did not match a {} ID:", + "{} track(s) did not match a {.data_source} ID:", failure_count, - self.data_source, + self, ) for track in failures: self._log.info("track: {}", track) self._log.info("") else: self._log.warning( - "{} track(s) did not match a {} ID:\n" + "{} track(s) did not match a {.data_source} ID:\n" "use --show-failures to display", failure_count, - self.data_source, + self, ) return results @@ -647,20 +646,18 @@ def _output_match_results(self, results): 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 - ) + "Attempting to open {.data_source} with playlist", self ) - spotify_url = "spotify:trackset:Playlist:" + ",".join( - spotify_ids + spotify_url = ( + f"spotify:trackset:Playlist:{','.join(spotify_ids)}" ) webbrowser.open(spotify_url) else: for spotify_id in spotify_ids: - print(self.open_track_url + spotify_id) + print(f"{self.open_track_url}{spotify_id}") else: self._log.warning( - f"No {self.data_source} tracks found from beets query" + "No {.data_source} tracks found from beets query", self ) def _fetch_info(self, items, write, force): @@ -705,7 +702,7 @@ def _fetch_info(self, items, write, force): def track_info(self, track_id: str): """Fetch a track's popularity and external IDs using its Spotify ID.""" - track_data = self._handle_response("get", self.track_url + track_id) + track_data = self._handle_response("get", f"{self.track_url}{track_id}") external_ids = track_data.get("external_ids", {}) popularity = track_data.get("popularity") self._log.debug( @@ -724,7 +721,7 @@ def track_audio_features(self, track_id: str): """Fetch track audio features by its Spotify ID.""" try: return self._handle_response( - "get", self.audio_features_url + track_id + "get", f"{self.audio_features_url}{track_id}" ) except APIError as e: self._log.debug("Spotify API error: {}", e) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 9b4a7778cf..6c11ab9180 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -168,9 +168,7 @@ def send(self, endpoint, params=None): params["v"] = "1.12.0" params["c"] = "beets" resp = requests.get( - "{}/rest/{}?{}".format( - self.config["base_url"].get(), endpoint, urlencode(params) - ), + f"{self.config['base_url'].get()}/rest/{endpoint}?{urlencode(params)}", timeout=10, ) return resp @@ -182,5 +180,5 @@ def get_playlists(self, ids): for track in tracks: if track not in output: output[track] = ";" - output[track] += name + ";" + output[track] += f"{name};" return output diff --git a/beetsplug/subsonicupdate.py b/beetsplug/subsonicupdate.py index ce888cb769..673cc94a84 100644 --- a/beetsplug/subsonicupdate.py +++ b/beetsplug/subsonicupdate.py @@ -74,7 +74,7 @@ def __create_token(self): # Pick the random sequence and salt the password r = string.ascii_letters + string.digits salt = "".join([random.choice(r) for _ in range(6)]) - salted_password = password + salt + salted_password = f"{password}{salt}" token = hashlib.md5(salted_password.encode("utf-8")).hexdigest() # Put together the payload of the request to the server and the URL @@ -101,14 +101,14 @@ def __format_url(self, endpoint): context_path = "" url = f"http://{host}:{port}{context_path}" - return url + f"/rest/{endpoint}" + return f"{url}/rest/{endpoint}" def start_scan(self): user = self.config["user"].as_str() auth = self.config["auth"].as_str() url = self.__format_url("startScan") - self._log.debug("URL is {0}", url) - self._log.debug("auth type is {0}", self.config["auth"]) + self._log.debug("URL is {}", url) + self._log.debug("auth type is {.config[auth]}", self) if auth == "token": salt, token = self.__create_token() @@ -145,14 +145,15 @@ def start_scan(self): and json["subsonic-response"]["status"] == "ok" ): count = json["subsonic-response"]["scanStatus"]["count"] - self._log.info(f"Updating Subsonic; scanning {count} tracks") + self._log.info("Updating Subsonic; scanning {} tracks", count) elif ( response.status_code == 200 and json["subsonic-response"]["status"] == "failed" ): - error_message = json["subsonic-response"]["error"]["message"] - self._log.error(f"Error: {error_message}") + self._log.error( + "Error: {[subsonic-response][error][message]}", json + ) else: - self._log.error("Error: {0}", json) + self._log.error("Error: {}", json) except Exception as error: - self._log.error(f"Error: {error}") + self._log.error("Error: {}", error) diff --git a/beetsplug/the.py b/beetsplug/the.py index 802b0a3db0..664d4c01e3 100644 --- a/beetsplug/the.py +++ b/beetsplug/the.py @@ -23,7 +23,7 @@ PATTERN_THE = "^the\\s" PATTERN_A = "^[a][n]?\\s" -FORMAT = "{0}, {1}" +FORMAT = "{}, {}" class ThePlugin(BeetsPlugin): @@ -38,7 +38,7 @@ def __init__(self): { "the": True, "a": True, - "format": "{0}, {1}", + "format": "{}, {}", "strip": False, "patterns": [], } @@ -50,11 +50,11 @@ def __init__(self): try: re.compile(p) except re.error: - self._log.error("invalid pattern: {0}", p) + self._log.error("invalid pattern: {}", p) else: if not (p.startswith("^") or p.endswith("$")): self._log.warning( - 'warning: "{0}" will not match string start/end', + 'warning: "{}" will not match string start/end', p, ) if self.config["a"]: @@ -94,7 +94,7 @@ def the_template_func(self, text): for p in self.patterns: r = self.unthe(text, p) if r != text: - self._log.debug('"{0}" -> "{1}"', text, r) + self._log.debug('"{}" -> "{}"', text, r) break return r else: diff --git a/beetsplug/thumbnails.py b/beetsplug/thumbnails.py index 5460d3fec6..651eaf3ac7 100644 --- a/beetsplug/thumbnails.py +++ b/beetsplug/thumbnails.py @@ -104,21 +104,21 @@ def _check_local_ok(self): f"Thumbnails: ArtResizer backend {ArtResizer.shared.method}" f" unexpectedly cannot write image metadata." ) - self._log.debug(f"using {ArtResizer.shared.method} to write metadata") + self._log.debug("using {.shared.method} to write metadata", ArtResizer) uri_getter = GioURI() if not uri_getter.available: uri_getter = PathlibURI() - self._log.debug("using {0.name} to compute URIs", uri_getter) + self._log.debug("using {.name} to compute URIs", uri_getter) self.get_uri = uri_getter.uri return True def process_album(self, album): """Produce thumbnails for the album folder.""" - self._log.debug("generating thumbnail for {0}", album) + self._log.debug("generating thumbnail for {}", album) if not album.artpath: - self._log.info("album {0} has no art", album) + self._log.info("album {} has no art", album) return if self.config["dolphin"]: @@ -127,7 +127,7 @@ def process_album(self, album): size = ArtResizer.shared.get_size(album.artpath) if not size: self._log.warning( - "problem getting the picture size for {0}", album.artpath + "problem getting the picture size for {.artpath}", album ) return @@ -137,9 +137,9 @@ def process_album(self, album): wrote &= self.make_cover_thumbnail(album, 128, NORMAL_DIR) if wrote: - self._log.info("wrote thumbnail for {0}", album) + self._log.info("wrote thumbnail for {}", album) else: - self._log.info("nothing to do for {0}", album) + self._log.info("nothing to do for {}", album) def make_cover_thumbnail(self, album, size, target_dir): """Make a thumbnail of given size for `album` and put it in @@ -154,16 +154,16 @@ def make_cover_thumbnail(self, album, size, target_dir): ): if self.config["force"]: self._log.debug( - "found a suitable {1}x{1} thumbnail for {0}, " + "found a suitable {0}x{0} thumbnail for {1}, " "forcing regeneration", - album, size, + album, ) else: self._log.debug( - "{1}x{1} thumbnail for {0} exists and is recent enough", - album, + "{0}x{0} thumbnail for {1} exists and is recent enough", size, + album, ) return False resized = ArtResizer.shared.resize(size, album.artpath, target) @@ -192,7 +192,7 @@ def add_tags(self, album, image_path): ArtResizer.shared.write_metadata(image_path, metadata) except Exception: self._log.exception( - "could not write metadata to {0}", displayable_path(image_path) + "could not write metadata to {}", displayable_path(image_path) ) def make_dolphin_cover_thumbnail(self, album): @@ -202,9 +202,9 @@ def make_dolphin_cover_thumbnail(self, album): 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)) + self._log.debug("Wrote file {}", displayable_path(outfilename)) class URIGetter: @@ -230,8 +230,7 @@ def copy_c_string(c_string): # This is a pretty dumb way to get a string copy, but it seems to # work. A more surefire way would be to allocate a ctypes buffer and copy # the data with `memcpy` or somesuch. - s = ctypes.cast(c_string, ctypes.c_char_p).value - return b"" + s + return ctypes.cast(c_string, ctypes.c_char_p).value class GioURI(URIGetter): @@ -266,9 +265,7 @@ def uri(self, path): g_file_ptr = self.libgio.g_file_new_for_path(path) if not g_file_ptr: raise RuntimeError( - "No gfile pointer received for {}".format( - displayable_path(path) - ) + f"No gfile pointer received for {displayable_path(path)}" ) try: diff --git a/beetsplug/types.py b/beetsplug/types.py index 9bdfdecee4..561ce68284 100644 --- a/beetsplug/types.py +++ b/beetsplug/types.py @@ -44,6 +44,6 @@ def _types(self): mytypes[key] = types.DATE else: raise ConfigValueError( - "unknown type '{}' for the '{}' field".format(value, key) + f"unknown type '{value}' for the '{key}' field" ) return mytypes diff --git a/beetsplug/unimported.py b/beetsplug/unimported.py index b473a346a6..20ae195a7e 100644 --- a/beetsplug/unimported.py +++ b/beetsplug/unimported.py @@ -34,7 +34,7 @@ def __init__(self): def commands(self): def print_unimported(lib, opts, args): ignore_exts = [ - ("." + x).encode() + f".{x}".encode() for x in self.config["ignore_extensions"].as_str_seq() ] ignore_dirs = [ diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 559f0622c5..7b13cf0168 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -77,7 +77,7 @@ def json_generator(items, root, expand=False): representation :returns: generator that yields strings """ - yield '{"%s":[' % root + yield f'{{"{root}":[' first = True for item in items: if first: @@ -232,9 +232,7 @@ def _get_unique_table_field_values(model, field, sort_field): raise KeyError with g.lib.transaction() as tx: rows = tx.query( - "SELECT DISTINCT '{}' FROM '{}' ORDER BY '{}'".format( - field, model._table, sort_field - ) + f"SELECT DISTINCT '{field}' FROM '{model._table}' ORDER BY '{sort_field}'" ) return [row[0] for row in rows] @@ -476,7 +474,7 @@ def func(lib, opts, args): # Enable CORS if required. if self.config["cors"]: self._log.info( - "Enabling CORS with origin: {0}", self.config["cors"] + "Enabling CORS with origin: {}", self.config["cors"] ) from flask_cors import CORS diff --git a/beetsplug/zero.py b/beetsplug/zero.py index 05e55bfcde..bce3b1a727 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -90,10 +90,10 @@ def _set_pattern(self, field): Do some sanity checks then compile the regexes. """ if field not in MediaFile.fields(): - self._log.error("invalid field: {0}", field) + self._log.error("invalid field: {}", field) elif field in ("id", "path", "album_id"): self._log.warning( - "field '{0}' ignored, zeroing it would be dangerous", field + "field '{}' ignored, zeroing it would be dangerous", field ) else: try: @@ -137,7 +137,7 @@ def set_fields(self, item, tags): if match: fields_set = True - self._log.debug("{0}: {1} -> None", field, value) + self._log.debug("{}: {} -> None", field, value) tags[field] = None if self.config["update_database"]: item[field] = None diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 620e1caec0..5ee07347ff 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -384,9 +384,9 @@ Here's an example that adds a ``$disc_and_track`` field: number. """ if item.disctotal > 1: - return u'%02i.%02i' % (item.disc, item.track) + return f"{item.disc:02d}.{item.track:02d}" else: - return u'%02i' % (item.track) + return f"{item.track:02d}" With this plugin enabled, templates can reference ``$disc_and_track`` as they can any standard metadata field. diff --git a/docs/plugins/inline.rst b/docs/plugins/inline.rst index 46ee3d6346..d653b6d527 100644 --- a/docs/plugins/inline.rst +++ b/docs/plugins/inline.rst @@ -20,8 +20,7 @@ Here are a couple of examples of expressions: item_fields: initial: albumartist[0].upper() + u'.' - disc_and_track: u'%02i.%02i' % (disc, track) if - disctotal > 1 else u'%02i' % (track) + disc_and_track: f"{disc:02d}.{track:02d}" if disctotal > 1 else f"{track:02d}" Note that YAML syntax allows newlines in values if the subsequent lines are indented. diff --git a/pyproject.toml b/pyproject.toml index dbe8e568a7..3ba7b8b6ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -275,11 +275,15 @@ select = [ "E", # pycodestyle "F", # pyflakes # "B", # flake8-bugbear + "G", # flake8-logging-format "I", # isort + "ISC", # flake8-implicit-str-concat "N", # pep8-naming "PT", # flake8-pytest-style # "RUF", # ruff # "UP", # pyupgrade + "UP031", # do not use percent formatting + "UP032", # use f-string instead of format call "TCH", # flake8-type-checking "W", # pycodestyle ] diff --git a/test/plugins/lyrics_pages.py b/test/plugins/lyrics_pages.py index e1806b1679..15cb812a10 100644 --- a/test/plugins/lyrics_pages.py +++ b/test/plugins/lyrics_pages.py @@ -128,6 +128,7 @@ def backend(self) -> str: artist="Atlanta", track_title="Mergaitės Nori Mylėt", url_title="Mergaitės nori mylėt – Atlanta | Dainų Žodžiai", + marks=[xfail_on_ci("Expired SSL certificate")], ), LyricsPage.make( "https://genius.com/The-beatles-lady-madonna-lyrics", @@ -328,34 +329,40 @@ def backend(self) -> str: url_title="The Beatles - Lady Madonna Lyrics", ), LyricsPage.make( - "https://www.lyricsmode.com/lyrics/b/beatles/lady_madonna.html", + "https://www.lyricsmode.com/lyrics/b/beatles/mother_natures_son.html", """ - Lady Madonna, children at your feet. - Wonder how you manage to make ends meet. - Who finds the money? When you pay the rent? - Did you think that money was heaven sent? + Born a poor young country boy, Mother Nature's son + All day long I'm sitting singing songs for everyone - Friday night arrives without a suitcase. - Sunday morning creep in like a nun. - Mondays child has learned to tie his bootlace. - See how they run. + Sit beside a mountain stream, see her waters rise + Listen to the pretty sound of music as she flies - Lady Madonna, baby at your breast. - Wonder how you manage to feed the rest. + Doo doo doo doo doo doo doo doo doo doo doo + Doo doo doo doo doo doo doo doo doo + Doo doo doo - See how they run. - Lady Madonna, lying on the bed, - Listen to the music playing in your head. + Find me in my field of grass, Mother Nature's son + Swaying daises sing a lazy song beneath the sun - Tuesday afternoon is never ending. - Wednesday morning papers didn't come. - Thursday night you stockings needed mending. - See how they run. + Doo doo doo doo doo doo doo doo doo doo doo + Doo doo doo doo doo doo doo doo doo + Doo doo doo doo doo doo + Yeah yeah yeah - Lady Madonna, children at your feet. - Wonder how you manage to make ends meet. + Mm mm mm mm mm mm mm + Mm mm mm, ooh ooh ooh + Mm mm mm mm mm mm mm + Mm mm mm mm, wah wah wah + + Wah, Mother Nature's son """, - url_title="Lady Madonna lyrics by The Beatles - original song full text. Official Lady Madonna lyrics, 2024 version | LyricsMode.com", # noqa: E501 + artist="The Beatles", + track_title="Mother Nature's Son", + url_title=( + "Mother Nature's Son lyrics by The Beatles - original song full" + " text. Official Mother Nature's Son lyrics, 2025 version" + " | LyricsMode.com" + ), ), LyricsPage.make( "https://www.lyricsontop.com/amy-winehouse-songs/jazz-n-blues-lyrics.html", @@ -528,6 +535,7 @@ def backend(self) -> str: Wonder how you manage to make ends meet. """, url_title="The Beatles - Lady Madonna", + marks=[xfail_on_ci("Sweetslyrics also fails with 403 FORBIDDEN in CI")], ), LyricsPage.make( "https://www.tekstowo.pl/piosenka,the_beatles,lady_madonna.html", diff --git a/test/plugins/test_art.py b/test/plugins/test_art.py index 38f8c75597..285bb70e5a 100644 --- a/test/plugins/test_art.py +++ b/test/plugins/test_art.py @@ -89,11 +89,11 @@ class CAAHelper: MBID_RELASE = "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_RELASE}" + GROUP_URL = f"coverartarchive.org/release-group/{MBID_GROUP}" - RELEASE_URL = "https://" + RELEASE_URL - GROUP_URL = "https://" + GROUP_URL + RELEASE_URL = f"https://{RELEASE_URL}" + GROUP_URL = f"https://{GROUP_URL}" RESPONSE_RELEASE = """{ "images": [ @@ -305,10 +305,8 @@ def test_precedence_amongst_correct_files(self): 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() @@ -708,7 +706,7 @@ def mock_response(self, url, json): def test_fanarttv_finds_image(self): album = _common.Bag(mb_releasegroupid="thereleasegroupid") self.mock_response( - fetchart.FanartTV.API_ALBUMS + "thereleasegroupid", + f"{fetchart.FanartTV.API_ALBUMS}thereleasegroupid", self.RESPONSE_MULTIPLE, ) candidate = next(self.source.get(album, self.settings, [])) @@ -717,7 +715,7 @@ def test_fanarttv_finds_image(self): def test_fanarttv_returns_no_result_when_error_received(self): album = _common.Bag(mb_releasegroupid="thereleasegroupid") self.mock_response( - fetchart.FanartTV.API_ALBUMS + "thereleasegroupid", + f"{fetchart.FanartTV.API_ALBUMS}thereleasegroupid", self.RESPONSE_ERROR, ) with pytest.raises(StopIteration): @@ -726,7 +724,7 @@ def test_fanarttv_returns_no_result_when_error_received(self): def test_fanarttv_returns_no_result_with_malformed_response(self): album = _common.Bag(mb_releasegroupid="thereleasegroupid") self.mock_response( - fetchart.FanartTV.API_ALBUMS + "thereleasegroupid", + f"{fetchart.FanartTV.API_ALBUMS}thereleasegroupid", self.RESPONSE_MALFORMED, ) with pytest.raises(StopIteration): @@ -736,7 +734,7 @@ def test_fanarttv_only_other_images(self): # The source used to fail when there were images present, but no cover album = _common.Bag(mb_releasegroupid="thereleasegroupid") self.mock_response( - fetchart.FanartTV.API_ALBUMS + "thereleasegroupid", + f"{fetchart.FanartTV.API_ALBUMS}thereleasegroupid", self.RESPONSE_NO_ART, ) with pytest.raises(StopIteration): diff --git a/test/plugins/test_convert.py b/test/plugins/test_convert.py index dcf684ccc7..1452686a7d 100644 --- a/test/plugins/test_convert.py +++ b/test/plugins/test_convert.py @@ -49,14 +49,12 @@ def tagged_copy_cmd(self, tag): """ if re.search("[^a-zA-Z0-9]", tag): raise ValueError( - "tag '{}' must only contain letters and digits".format(tag) + f"tag '{tag}' must only contain letters and digits" ) # A Python script that copies the file and appends a tag. stub = os.path.join(_common.RSRC, b"convert_stub.py").decode("utf-8") - return "{} {} $source $dest {}".format( - shell_quote(sys.executable), shell_quote(stub), tag - ) + return f"{shell_quote(sys.executable)} {shell_quote(stub)} $source $dest {tag}" def file_endswith(self, path: Path, tag: str): """Check the path is a file and if its content ends with `tag`.""" diff --git a/test/plugins/test_discogs.py b/test/plugins/test_discogs.py index c31ac7511e..e3e51042c0 100644 --- a/test/plugins/test_discogs.py +++ b/test/plugins/test_discogs.py @@ -82,7 +82,7 @@ def _make_release_from_positions(self, positions): """Return a Bag that mimics a discogs_client.Release with a tracklist where tracks have the specified `positions`.""" tracks = [ - self._make_track("TITLE%s" % i, position) + self._make_track(f"TITLE{i}", position) for (i, position) in enumerate(positions, start=1) ] return self._make_release(tracks) diff --git a/test/plugins/test_embedart.py b/test/plugins/test_embedart.py index 62b2bb7d10..58d2a5f636 100644 --- a/test/plugins/test_embedart.py +++ b/test/plugins/test_embedart.py @@ -144,9 +144,7 @@ def test_embed_art_remove_art_file(self): if os.path.isfile(syspath(tmp_path)): os.remove(syspath(tmp_path)) self.fail( - "Artwork file {} was not deleted".format( - displayable_path(tmp_path) - ) + f"Artwork file {displayable_path(tmp_path)} was not deleted" ) def test_art_file_missing(self): diff --git a/test/plugins/test_ftintitle.py b/test/plugins/test_ftintitle.py index 1dbe4a7274..572431b45b 100644 --- a/test/plugins/test_ftintitle.py +++ b/test/plugins/test_ftintitle.py @@ -56,21 +56,21 @@ def test_functional_not_found(self): assert item["title"] == "Song 1" def test_functional_custom_format(self): - self._ft_set_config("feat. {0}") + self._ft_set_config("feat. {}") item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "Alice") self.run_command("ftintitle") item.load() assert item["artist"] == "Alice" assert item["title"] == "Song 1 feat. Bob" - self._ft_set_config("featuring {0}") + self._ft_set_config("featuring {}") item = self._ft_add_item("/", "Alice feat. Bob", "Song 1", "Alice") self.run_command("ftintitle") item.load() assert item["artist"] == "Alice" assert item["title"] == "Song 1 featuring Bob" - self._ft_set_config("with {0}") + self._ft_set_config("with {}") item = self._ft_add_item("/", "Alice feat Bob", "Song 1", "Alice") self.run_command("ftintitle") item.load() @@ -78,7 +78,7 @@ def test_functional_custom_format(self): assert item["title"] == "Song 1 with Bob" def test_functional_keep_in_artist(self): - self._ft_set_config("feat. {0}", keep_in_artist=True) + self._ft_set_config("feat. {}", keep_in_artist=True) item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "Alice") self.run_command("ftintitle") item.load() diff --git a/test/plugins/test_importadded.py b/test/plugins/test_importadded.py index 1b198b31d9..352471f9b6 100644 --- a/test/plugins/test_importadded.py +++ b/test/plugins/test_importadded.py @@ -65,7 +65,7 @@ def find_media_file(self, item): if m.title.replace("Tag", "Applied") == item.title: return m raise AssertionError( - "No MediaFile found for Item " + displayable_path(item.path) + f"No MediaFile found for Item {displayable_path(item.path)}" ) def test_import_album_with_added_dates(self): @@ -117,7 +117,7 @@ def test_reimported_album_skipped(self): for item_path, added_after in items_added_after.items(): assert items_added_before[item_path] == pytest.approx( added_after, rel=1e-4 - ), "reimport modified Item.added for " + displayable_path(item_path) + ), f"reimport modified Item.added for {displayable_path(item_path)}" def test_import_singletons_with_added_dates(self): self.config["import"]["singletons"] = True @@ -157,4 +157,4 @@ def test_reimported_singletons_skipped(self): for item_path, added_after in items_added_after.items(): assert items_added_before[item_path] == pytest.approx( added_after, rel=1e-4 - ), "reimport modified Item.added for " + displayable_path(item_path) + ), f"reimport modified Item.added for {displayable_path(item_path)}" diff --git a/test/plugins/test_ipfs.py b/test/plugins/test_ipfs.py index 096bc393bc..b94bd551bf 100644 --- a/test/plugins/test_ipfs.py +++ b/test/plugins/test_ipfs.py @@ -37,7 +37,7 @@ def test_stored_hashes(self): try: if check_item.get("ipfs", with_album=False): ipfs_item = os.fsdecode(os.path.basename(want_item.path)) - want_path = "/ipfs/{}/{}".format(test_album.ipfs, ipfs_item) + want_path = f"/ipfs/{test_album.ipfs}/{ipfs_item}" want_path = bytestring_path(want_path) assert check_item.path == want_path assert ( diff --git a/test/plugins/test_limit.py b/test/plugins/test_limit.py index 12700295e0..d77e47ca84 100644 --- a/test/plugins/test_limit.py +++ b/test/plugins/test_limit.py @@ -42,8 +42,8 @@ def setUp(self): # a subset of tests has only `num_limit` results, identified by a # range filter on the track number - self.track_head_range = "track:.." + str(self.num_limit) - self.track_tail_range = "track:" + str(self.num_limit + 1) + ".." + self.track_head_range = f"track:..{self.num_limit}" + self.track_tail_range = f"track:{self.num_limit + 1}{'..'}" def test_no_limit(self): """Returns all when there is no limit or filter.""" @@ -82,13 +82,13 @@ def test_prefix(self): def test_prefix_when_correctly_ordered(self): """Returns the expected number with the query prefix and filter when the prefix portion (correctly) appears last.""" - correct_order = self.track_tail_range + " " + self.num_limit_prefix + correct_order = f"{self.track_tail_range} {self.num_limit_prefix}" result = self.lib.items(correct_order) assert len(result) == self.num_limit def test_prefix_when_incorrectly_ordred(self): """Returns no results with the query prefix and filter when the prefix portion (incorrectly) appears first.""" - incorrect_order = self.num_limit_prefix + " " + self.track_tail_range + incorrect_order = f"{self.num_limit_prefix} {self.track_tail_range}" result = self.lib.items(incorrect_order) assert len(result) == 0 diff --git a/test/plugins/test_mpdstats.py b/test/plugins/test_mpdstats.py index dcaf196efa..6f5d3f3cee 100644 --- a/test/plugins/test_mpdstats.py +++ b/test/plugins/test_mpdstats.py @@ -77,7 +77,7 @@ def test_run_mpdstats(self, mpd_mock): except KeyboardInterrupt: pass - log.debug.assert_has_calls([call('unhandled status "{0}"', ANY)]) + log.debug.assert_has_calls([call('unhandled status "{}"', ANY)]) log.info.assert_has_calls( - [call("pause"), call("playing {0}", ANY), call("stop")] + [call("pause"), call("playing {}", ANY), call("stop")] ) diff --git a/test/plugins/test_musicbrainz.py b/test/plugins/test_musicbrainz.py index aea05bc209..844b2ad4ef 100644 --- a/test/plugins/test_musicbrainz.py +++ b/test/plugins/test_musicbrainz.py @@ -99,7 +99,7 @@ def _make_release( for recording in tracks: i += 1 track = { - "id": "RELEASE TRACK ID %d" % i, + "id": f"RELEASE TRACK ID {i}", "recording": recording, "position": i, "number": "A1", @@ -140,7 +140,7 @@ def _make_release( for recording in data_tracks: i += 1 data_track = { - "id": "RELEASE TRACK ID %d" % i, + "id": f"RELEASE TRACK ID {i}", "recording": recording, "position": i, "number": "A1", @@ -670,17 +670,17 @@ class ArtistFlatteningTest(unittest.TestCase): def _credit_dict(self, suffix=""): return { "artist": { - "name": "NAME" + suffix, - "sort-name": "SORT" + suffix, + "name": f"NAME{suffix}", + "sort-name": f"SORT{suffix}", }, - "name": "CREDIT" + suffix, + "name": f"CREDIT{suffix}", } def _add_alias(self, credit_dict, suffix="", locale="", primary=False): alias = { - "alias": "ALIAS" + suffix, + "alias": f"ALIAS{suffix}", "locale": locale, - "sort-name": "ALIASSORT" + suffix, + "sort-name": f"ALIASSORT{suffix}", } if primary: alias["primary"] = "primary" diff --git a/test/plugins/test_parentwork.py b/test/plugins/test_parentwork.py index 99267f6ffa..1abe257090 100644 --- a/test/plugins/test_parentwork.py +++ b/test/plugins/test_parentwork.py @@ -93,8 +93,7 @@ def test_normal_case_real(self): item = Item( path="/file", mb_workid="e27bda6e-531e-36d3-9cd7-b8ebc18e8c53", - parentwork_workid_current="e27bda6e-531e-36d3-9cd7-\ - b8ebc18e8c53", + parentwork_workid_current="e27bda6e-531e-36d3-9cd7-b8ebc18e8c53", ) item.add(self.lib) @@ -109,8 +108,7 @@ def test_force_real(self): path="/file", mb_workid="e27bda6e-531e-36d3-9cd7-b8ebc18e8c53", mb_parentworkid="XXX", - parentwork_workid_current="e27bda6e-531e-36d3-9cd7-\ - b8ebc18e8c53", + parentwork_workid_current="e27bda6e-531e-36d3-9cd7-b8ebc18e8c53", parentwork="whatever", ) item.add(self.lib) @@ -124,11 +122,9 @@ def test_no_force_real(self): self.config["parentwork"]["force"] = False item = Item( path="/file", - mb_workid="e27bda6e-531e-36d3-9cd7-\ - b8ebc18e8c53", + mb_workid="e27bda6e-531e-36d3-9cd7-b8ebc18e8c53", mb_parentworkid="XXX", - parentwork_workid_current="e27bda6e-531e-36d3-9cd7-\ - b8ebc18e8c53", + parentwork_workid_current="e27bda6e-531e-36d3-9cd7-b8ebc18e8c53", parentwork="whatever", ) item.add(self.lib) diff --git a/test/plugins/test_play.py b/test/plugins/test_play.py index 571af95dd8..293a50a20c 100644 --- a/test/plugins/test_play.py +++ b/test/plugins/test_play.py @@ -49,7 +49,7 @@ def run_and_assert( open_mock.assert_called_once_with(ANY, expected_cmd) expected_playlist = expected_playlist or self.item.path.decode("utf-8") - exp_playlist = expected_playlist + "\n" + exp_playlist = f"{expected_playlist}\n" with open(open_mock.call_args[0][0][0], "rb") as playlist: assert exp_playlist == playlist.read().decode("utf-8") @@ -96,9 +96,7 @@ def test_use_folders(self, open_mock): open_mock.assert_called_once_with(ANY, open_anything()) with open(open_mock.call_args[0][0][0], "rb") as f: playlist = f.read().decode("utf-8") - assert ( - f"{os.path.dirname(self.item.path.decode('utf-8'))}\n" == playlist - ) + assert f"{self.item.filepath.parent}\n" == playlist def test_raw(self, open_mock): self.config["play"]["raw"] = True @@ -125,9 +123,7 @@ def test_skip_warning_threshold_bypass(self, open_mock): self.config["play"]["warning_threshold"] = 1 self.other_item = self.add_item(title="another NiceTitle") - expected_playlist = "{}\n{}".format( - self.item.path.decode("utf-8"), self.other_item.path.decode("utf-8") - ) + expected_playlist = f"{self.item.filepath}\n{self.other_item.filepath}" with control_stdin("a"): self.run_and_assert( diff --git a/test/plugins/test_playlist.py b/test/plugins/test_playlist.py index 9d9ce03034..a8c145696a 100644 --- a/test/plugins/test_playlist.py +++ b/test/plugins/test_playlist.py @@ -91,14 +91,7 @@ def test_name_query_with_absolute_paths_in_playlist(self): assert {i.title for i in results} == {"some item", "another item"} def test_path_query_with_absolute_paths_in_playlist(self): - q = "playlist:{}".format( - quote( - os.path.join( - self.playlist_dir, - "absolute.m3u", - ) - ) - ) + q = f"playlist:{quote(os.path.join(self.playlist_dir, 'absolute.m3u'))}" results = self.lib.items(q) assert {i.title for i in results} == {"some item", "another item"} @@ -108,14 +101,7 @@ def test_name_query_with_relative_paths_in_playlist(self): assert {i.title for i in results} == {"some item", "another item"} def test_path_query_with_relative_paths_in_playlist(self): - q = "playlist:{}".format( - quote( - os.path.join( - self.playlist_dir, - "relative.m3u", - ) - ) - ) + q = f"playlist:{quote(os.path.join(self.playlist_dir, 'relative.m3u'))}" results = self.lib.items(q) assert {i.title for i in results} == {"some item", "another item"} @@ -125,15 +111,7 @@ def test_name_query_with_nonexisting_playlist(self): assert set(results) == set() def test_path_query_with_nonexisting_playlist(self): - q = "playlist:{}".format( - quote( - os.path.join( - self.playlist_dir, - self.playlist_dir, - "nonexisting.m3u", - ) - ) - ) + q = f"playlist:{os.path.join(self.playlist_dir, 'nonexisting.m3u')!r}" results = self.lib.items(q) assert set(results) == set() @@ -141,20 +119,22 @@ def test_path_query_with_nonexisting_playlist(self): class PlaylistTestRelativeToLib(PlaylistQueryTest, PlaylistTestCase): def setup_test(self): with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f: - f.write( - "{}\n".format(os.path.join(self.music_dir, "a", "b", "c.mp3")) - ) - f.write( - "{}\n".format(os.path.join(self.music_dir, "d", "e", "f.mp3")) - ) - f.write( - "{}\n".format(os.path.join(self.music_dir, "nonexisting.mp3")) + f.writelines( + [ + os.path.join(self.music_dir, "a", "b", "c.mp3") + "\n", + os.path.join(self.music_dir, "d", "e", "f.mp3") + "\n", + os.path.join(self.music_dir, "nonexisting.mp3") + "\n", + ] ) with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f: - f.write("{}\n".format(os.path.join("a", "b", "c.mp3"))) - f.write("{}\n".format(os.path.join("d", "e", "f.mp3"))) - f.write("{}\n".format("nonexisting.mp3")) + f.writelines( + [ + os.path.join("a", "b", "c.mp3") + "\n", + os.path.join("d", "e", "f.mp3") + "\n", + "nonexisting.mp3\n", + ] + ) self.config["playlist"]["relative_to"] = "library" @@ -162,20 +142,22 @@ def setup_test(self): class PlaylistTestRelativeToDir(PlaylistQueryTest, PlaylistTestCase): def setup_test(self): with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f: - f.write( - "{}\n".format(os.path.join(self.music_dir, "a", "b", "c.mp3")) - ) - f.write( - "{}\n".format(os.path.join(self.music_dir, "d", "e", "f.mp3")) - ) - f.write( - "{}\n".format(os.path.join(self.music_dir, "nonexisting.mp3")) + f.writelines( + [ + os.path.join(self.music_dir, "a", "b", "c.mp3") + "\n", + os.path.join(self.music_dir, "d", "e", "f.mp3") + "\n", + os.path.join(self.music_dir, "nonexisting.mp3") + "\n", + ] ) with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f: - f.write("{}\n".format(os.path.join("a", "b", "c.mp3"))) - f.write("{}\n".format(os.path.join("d", "e", "f.mp3"))) - f.write("{}\n".format("nonexisting.mp3")) + f.writelines( + [ + os.path.join("a", "b", "c.mp3") + "\n", + os.path.join("d", "e", "f.mp3") + "\n", + "nonexisting.mp3\n", + ] + ) self.config["playlist"]["relative_to"] = self.music_dir @@ -183,40 +165,33 @@ def setup_test(self): class PlaylistTestRelativeToPls(PlaylistQueryTest, PlaylistTestCase): def setup_test(self): with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f: - f.write( - "{}\n".format(os.path.join(self.music_dir, "a", "b", "c.mp3")) - ) - f.write( - "{}\n".format(os.path.join(self.music_dir, "d", "e", "f.mp3")) - ) - f.write( - "{}\n".format(os.path.join(self.music_dir, "nonexisting.mp3")) + f.writelines( + [ + os.path.join(self.music_dir, "a", "b", "c.mp3") + "\n", + os.path.join(self.music_dir, "d", "e", "f.mp3") + "\n", + os.path.join(self.music_dir, "nonexisting.mp3") + "\n", + ] ) with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f: - f.write( - "{}\n".format( + f.writelines( + [ os.path.relpath( os.path.join(self.music_dir, "a", "b", "c.mp3"), start=self.playlist_dir, ) - ) - ) - f.write( - "{}\n".format( + + "\n", os.path.relpath( os.path.join(self.music_dir, "d", "e", "f.mp3"), start=self.playlist_dir, ) - ) - ) - f.write( - "{}\n".format( + + "\n", os.path.relpath( os.path.join(self.music_dir, "nonexisting.mp3"), start=self.playlist_dir, ) - ) + + "\n", + ] ) self.config["playlist"]["relative_to"] = "playlist" @@ -226,20 +201,22 @@ def setup_test(self): class PlaylistUpdateTest: def setup_test(self): with open(os.path.join(self.playlist_dir, "absolute.m3u"), "w") as f: - f.write( - "{}\n".format(os.path.join(self.music_dir, "a", "b", "c.mp3")) - ) - f.write( - "{}\n".format(os.path.join(self.music_dir, "d", "e", "f.mp3")) - ) - f.write( - "{}\n".format(os.path.join(self.music_dir, "nonexisting.mp3")) + f.writelines( + [ + os.path.join(self.music_dir, "a", "b", "c.mp3") + "\n", + os.path.join(self.music_dir, "d", "e", "f.mp3") + "\n", + os.path.join(self.music_dir, "nonexisting.mp3") + "\n", + ] ) with open(os.path.join(self.playlist_dir, "relative.m3u"), "w") as f: - f.write("{}\n".format(os.path.join("a", "b", "c.mp3"))) - f.write("{}\n".format(os.path.join("d", "e", "f.mp3"))) - f.write("{}\n".format("nonexisting.mp3")) + f.writelines( + [ + os.path.join("a", "b", "c.mp3") + "\n", + os.path.join("d", "e", "f.mp3") + "\n", + "nonexisting.mp3\n", + ] + ) self.config["playlist"]["auto"] = True self.config["playlist"]["relative_to"] = "library" @@ -249,9 +226,7 @@ class PlaylistTestItemMoved(PlaylistUpdateTest, PlaylistTestCase): def test_item_moved(self): # Emit item_moved event for an item that is in a playlist results = self.lib.items( - "path:{}".format( - quote(os.path.join(self.music_dir, "d", "e", "f.mp3")) - ) + f"path:{quote(os.path.join(self.music_dir, 'd', 'e', 'f.mp3'))}" ) item = results[0] beets.plugins.send( @@ -265,9 +240,7 @@ def test_item_moved(self): # Emit item_moved event for an item that is not in a playlist results = self.lib.items( - "path:{}".format( - quote(os.path.join(self.music_dir, "x", "y", "z.mp3")) - ) + f"path:{quote(os.path.join(self.music_dir, 'x', 'y', 'z.mp3'))}" ) item = results[0] beets.plugins.send( @@ -309,18 +282,14 @@ class PlaylistTestItemRemoved(PlaylistUpdateTest, PlaylistTestCase): def test_item_removed(self): # Emit item_removed event for an item that is in a playlist results = self.lib.items( - "path:{}".format( - quote(os.path.join(self.music_dir, "d", "e", "f.mp3")) - ) + f"path:{quote(os.path.join(self.music_dir, 'd', 'e', 'f.mp3'))}" ) item = results[0] beets.plugins.send("item_removed", item=item) # Emit item_removed event for an item that is not in a playlist results = self.lib.items( - "path:{}".format( - quote(os.path.join(self.music_dir, "x", "y", "z.mp3")) - ) + f"path:{quote(os.path.join(self.music_dir, 'x', 'y', 'z.mp3'))}" ) item = results[0] beets.plugins.send("item_removed", item=item) diff --git a/test/plugins/test_plexupdate.py b/test/plugins/test_plexupdate.py index f319db6cee..ab53d8c2e7 100644 --- a/test/plugins/test_plexupdate.py +++ b/test/plugins/test_plexupdate.py @@ -29,7 +29,7 @@ def add_response_get_music_section(self, section_name="Music"): "" '""" response = self.client.get("/item/query/") res_json = json.loads(response.data.decode("utf-8")) @@ -165,7 +164,6 @@ def test_get_item_empty_query(self): assert len(res_json["items"]) == 3 def test_get_simple_item_query(self): - """testing item query: another""" response = self.client.get("/item/query/another") res_json = json.loads(response.data.decode("utf-8")) @@ -174,8 +172,7 @@ def test_get_simple_item_query(self): assert res_json["results"][0]["title"] == "another title" def test_query_item_string(self): - """testing item query: testattr:ABC""" - response = self.client.get("/item/query/testattr%3aABC") + response = self.client.get("/item/query/testattr%3aABC") # testattr:ABC res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 @@ -183,8 +180,9 @@ def test_query_item_string(self): assert res_json["results"][0]["title"] == "and a third" def test_query_item_regex(self): - """testing item query: testattr::[A-C]+""" - response = self.client.get("/item/query/testattr%3a%3a[A-C]%2b") + response = self.client.get( + "/item/query/testattr%3a%3a[A-C]%2b" + ) # testattr::[A-C]+ res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 @@ -192,8 +190,9 @@ def test_query_item_regex(self): assert res_json["results"][0]["title"] == "and a third" def test_query_item_regex_backslash(self): - # """ testing item query: testattr::\w+ """ - response = self.client.get("/item/query/testattr%3a%3a%5cw%2b") + response = self.client.get( + "/item/query/testattr%3a%3a%5cw%2b" + ) # testattr::\w+ res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 @@ -201,7 +200,6 @@ def test_query_item_regex_backslash(self): assert res_json["results"][0]["title"] == "and a third" def test_query_item_path(self): - # """ testing item query: path:\somewhere\a """ """Note: path queries are special: the query item must match the path from the root all the way to a directory, so this matches 1 item""" """ Note: filesystem separators in the query must be '\' """ @@ -267,8 +265,9 @@ def test_get_album_details(self): assert response_track_titles == {"title", "and a third"} def test_query_album_string(self): - """testing query: albumtest:xy""" - response = self.client.get("/album/query/albumtest%3axy") + response = self.client.get( + "/album/query/albumtest%3axy" + ) # albumtest:xy res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 @@ -276,8 +275,9 @@ def test_query_album_string(self): assert res_json["results"][0]["album"] == "album" def test_query_album_artpath_regex(self): - """testing query: artpath::art_""" - response = self.client.get("/album/query/artpath%3a%3aart_") + response = self.client.get( + "/album/query/artpath%3a%3aart_" + ) # artpath::art_ res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 @@ -285,8 +285,9 @@ def test_query_album_artpath_regex(self): assert res_json["results"][0]["album"] == "other album" def test_query_album_regex_backslash(self): - # """ testing query: albumtest::\w+ """ - response = self.client.get("/album/query/albumtest%3a%3a%5cw%2b") + response = self.client.get( + "/album/query/albumtest%3a%3a%5cw%2b" + ) # albumtest::\w+ res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 @@ -310,18 +311,18 @@ def test_delete_item_id(self): ) # Check we can find the temporary item we just created - response = self.client.get("/item/" + str(item_id)) + response = self.client.get(f"/item/{item_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 assert res_json["id"] == item_id # Delete item by id - response = self.client.delete("/item/" + str(item_id)) + response = self.client.delete(f"/item/{item_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 # Check the item has gone - response = self.client.get("/item/" + str(item_id)) + response = self.client.get(f"/item/{item_id}") assert response.status_code == 404 # Note: if this fails, the item may still be around # and may cause other tests to fail @@ -336,18 +337,18 @@ def test_delete_item_without_file(self): item_id = self.lib.add(Item.from_path(ipath)) # Check we can find the temporary item we just created - response = self.client.get("/item/" + str(item_id)) + response = self.client.get(f"/item/{item_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 assert res_json["id"] == item_id # Delete item by id, without deleting file - response = self.client.delete("/item/" + str(item_id)) + response = self.client.delete(f"/item/{item_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 # Check the item has gone - response = self.client.get("/item/" + str(item_id)) + response = self.client.get(f"/item/{item_id}") assert response.status_code == 404 # Check the file has not gone @@ -364,18 +365,18 @@ def test_delete_item_with_file(self): item_id = self.lib.add(Item.from_path(ipath)) # Check we can find the temporary item we just created - response = self.client.get("/item/" + str(item_id)) + response = self.client.get(f"/item/{item_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 assert res_json["id"] == item_id # Delete item by id, with file - response = self.client.delete("/item/" + str(item_id) + "?delete") + response = self.client.delete(f"/item/{item_id}?delete") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 # Check the item has gone - response = self.client.get("/item/" + str(item_id)) + response = self.client.get(f"/item/{item_id}") assert response.status_code == 404 # Check the file has gone @@ -427,17 +428,17 @@ def test_delete_item_id_readonly(self): ) # Check we can find the temporary item we just created - response = self.client.get("/item/" + str(item_id)) + response = self.client.get(f"/item/{item_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 assert res_json["id"] == item_id # Try to delete item by id - response = self.client.delete("/item/" + str(item_id)) + response = self.client.delete(f"/item/{item_id}") assert response.status_code == 405 # Check the item has not gone - response = self.client.get("/item/" + str(item_id)) + response = self.client.get(f"/item/{item_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 assert res_json["id"] == item_id @@ -481,18 +482,18 @@ def test_delete_album_id(self): ) # Check we can find the temporary album we just created - response = self.client.get("/album/" + str(album_id)) + response = self.client.get(f"/album/{album_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 assert res_json["id"] == album_id # Delete album by id - response = self.client.delete("/album/" + str(album_id)) + response = self.client.delete(f"/album/{album_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 # Check the album has gone - response = self.client.get("/album/" + str(album_id)) + response = self.client.get(f"/album/{album_id}") assert response.status_code == 404 # Note: if this fails, the album may still be around # and may cause other tests to fail @@ -543,17 +544,17 @@ def test_delete_album_id_readonly(self): ) # Check we can find the temporary album we just created - response = self.client.get("/album/" + str(album_id)) + response = self.client.get(f"/album/{album_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 assert res_json["id"] == album_id # Try to delete album by id - response = self.client.delete("/album/" + str(album_id)) + response = self.client.delete(f"/album/{album_id}") assert response.status_code == 405 # Check the item has not gone - response = self.client.get("/album/" + str(album_id)) + response = self.client.get(f"/album/{album_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 assert res_json["id"] == album_id @@ -603,7 +604,7 @@ def test_patch_item_id(self): ) # Check we can find the temporary item we just created - response = self.client.get("/item/" + str(item_id)) + response = self.client.get(f"/item/{item_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 assert res_json["id"] == item_id @@ -613,7 +614,7 @@ def test_patch_item_id(self): # Patch item by id # patch_json = json.JSONEncoder().encode({"test_patch_f2": "New"}]}) response = self.client.patch( - "/item/" + str(item_id), json={"test_patch_f2": "New"} + f"/item/{item_id}", json={"test_patch_f2": "New"} ) res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 @@ -622,7 +623,7 @@ def test_patch_item_id(self): assert res_json["test_patch_f2"] == "New" # Check the update has really worked - response = self.client.get("/item/" + str(item_id)) + response = self.client.get(f"/item/{item_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 assert res_json["id"] == item_id @@ -647,7 +648,7 @@ def test_patch_item_id_readonly(self): ) # Check we can find the temporary item we just created - response = self.client.get("/item/" + str(item_id)) + response = self.client.get(f"/item/{item_id}") res_json = json.loads(response.data.decode("utf-8")) assert response.status_code == 200 assert res_json["id"] == item_id @@ -657,7 +658,7 @@ def test_patch_item_id_readonly(self): # Patch item by id # patch_json = json.JSONEncoder().encode({"test_patch_f2": "New"}) response = self.client.patch( - "/item/" + str(item_id), json={"test_patch_f2": "New"} + f"/item/{item_id}", json={"test_patch_f2": "New"} ) assert response.status_code == 405 @@ -670,6 +671,6 @@ def test_get_item_file(self): assert os.path.exists(ipath) item_id = self.lib.add(Item.from_path(ipath)) - response = self.client.get("/item/" + str(item_id) + "/file") + response = self.client.get(f"/item/{item_id}/file") assert response.status_code == 200 diff --git a/test/test_art_resize.py b/test/test_art_resize.py index 34bf810b95..0ccbb0eae6 100644 --- a/test/test_art_resize.py +++ b/test/test_art_resize.py @@ -150,9 +150,5 @@ def test_write_metadata_im(self, mock_util): metadata = {"a": "A", "b": "B"} im = DummyIMBackend() im.write_metadata("foo", metadata) - try: - command = im.convert_cmd + "foo -set a A -set b B foo".split() - mock_util.command_output.assert_called_once_with(command) - except AssertionError: - command = im.convert_cmd + "foo -set b B -set a A foo".split() - mock_util.command_output.assert_called_once_with(command) + command = [*im.convert_cmd, *"foo -set a A -set b B foo".split()] + mock_util.command_output.assert_called_once_with(command) diff --git a/test/test_datequery.py b/test/test_datequery.py index 1063a62c1f..d73fca45f3 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -186,37 +186,37 @@ def setUp(self): def test_relative(self): for timespan in ["d", "w", "m", "y"]: - query = DateQuery("added", "-4" + timespan + "..+4" + timespan) + query = DateQuery("added", f"-4{timespan}..+4{timespan}") matched = self.lib.items(query) assert len(matched) == 1 def test_relative_fail(self): for timespan in ["d", "w", "m", "y"]: - query = DateQuery("added", "-2" + timespan + "..-1" + timespan) + query = DateQuery("added", f"-2{timespan}..-1{timespan}") matched = self.lib.items(query) assert len(matched) == 0 def test_start_relative(self): for timespan in ["d", "w", "m", "y"]: - query = DateQuery("added", "-4" + timespan + "..") + query = DateQuery("added", f"-4{timespan}..") matched = self.lib.items(query) assert len(matched) == 1 def test_start_relative_fail(self): for timespan in ["d", "w", "m", "y"]: - query = DateQuery("added", "4" + timespan + "..") + query = DateQuery("added", f"4{timespan}..") matched = self.lib.items(query) assert len(matched) == 0 def test_end_relative(self): for timespan in ["d", "w", "m", "y"]: - query = DateQuery("added", "..+4" + timespan) + query = DateQuery("added", f"..+4{timespan}") matched = self.lib.items(query) assert len(matched) == 1 def test_end_relative_fail(self): for timespan in ["d", "w", "m", "y"]: - query = DateQuery("added", "..-4" + timespan) + query = DateQuery("added", f"..-4{timespan}") matched = self.lib.items(query) assert len(matched) == 0 diff --git a/test/test_dbcore.py b/test/test_dbcore.py index 3f9a9d45e6..b2ec2e968d 100644 --- a/test/test_dbcore.py +++ b/test/test_dbcore.py @@ -256,7 +256,7 @@ def test_mutate_increase_revision(self): def test_query_no_increase_revision(self): old_rev = self.db.revision with self.db.transaction() as tx: - tx.query("PRAGMA table_info(%s)" % ModelFixture1._table) + tx.query(f"PRAGMA table_info({ModelFixture1._table})") assert self.db.revision == old_rev diff --git a/test/test_library.py b/test/test_library.py index 35791bad72..7c05290017 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -1033,7 +1033,7 @@ def setUp(self): def test_art_filename_respects_setting(self): art = self.ai.art_destination("something.jpg") - new_art = bytestring_path("%sartimage.jpg" % os.path.sep) + new_art = bytestring_path(f"{os.path.sep}artimage.jpg") assert new_art in art def test_art_path_in_item_dir(self): diff --git a/test/test_logging.py b/test/test_logging.py index 1859ea2dd5..aee0bd61bb 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -42,7 +42,7 @@ def test_str_format_logging(self): logger.addHandler(handler) logger.propagate = False - logger.warning("foo {0} {bar}", "oof", bar="baz") + logger.warning("foo {} {bar}", "oof", bar="baz") handler.flush() assert stream.getvalue(), "foo oof baz" @@ -58,9 +58,9 @@ def __init__(self): self.register_listener("dummy_event", self.listener) def log_all(self, name): - self._log.debug("debug " + name) - self._log.info("info " + name) - self._log.warning("warning " + name) + self._log.debug("debug {}", name) + self._log.info("info {}", name) + self._log.warning("warning {}", name) def commands(self): cmd = ui.Subcommand("dummy") @@ -172,9 +172,9 @@ def __init__(self, test_case): self.t1_step = self.t2_step = 0 def log_all(self, name): - self._log.debug("debug " + name) - self._log.info("info " + name) - self._log.warning("warning " + name) + self._log.debug("debug {}", name) + self._log.info("info {}", name) + self._log.warning("warning {}", name) def listener1(self): try: diff --git a/test/test_ui.py b/test/test_ui.py index 664323e2a1..534d0e4665 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -1020,7 +1020,7 @@ def test_command_line_option_relative_to_working_dir(self): def test_cli_config_file_loads_plugin_commands(self): with open(self.cli_config_path, "w") as file: - file.write("pluginpath: %s\n" % _common.PLUGINPATH) + file.write(f"pluginpath: {_common.PLUGINPATH}\n") file.write("plugins: test") self.run_command("--config", self.cli_config_path, "plugin", lib=None) @@ -1257,11 +1257,10 @@ def test_album_data_change_wrap_newline(self): with patch("beets.ui.commands.ui.term_width", return_value=30): # Test newline layout config["ui"]["import"]["layout"] = "newline" - long_name = "another artist with a" + (" very" * 10) + " long name" + long_name = f"another artist with a{' very' * 10} long name" msg = self._show_change( cur_artist=long_name, cur_album="another album" ) - # _common.log.info("Message:{}".format(msg)) assert "artist: another artist" in msg assert " -> the artist" in msg assert "another album -> the album" not in msg @@ -1271,7 +1270,7 @@ def test_item_data_change_wrap_column(self): with patch("beets.ui.commands.ui.term_width", return_value=54): # Test Column layout config["ui"]["import"]["layout"] = "column" - long_title = "a track with a" + (" very" * 10) + " long name" + long_title = f"a track with a{' very' * 10} long name" self.items[0].title = long_title msg = self._show_change() assert "(#1) a track (1:00) -> (#1) the title (0:00)" in msg @@ -1280,7 +1279,7 @@ def test_item_data_change_wrap_newline(self): # Patch ui.term_width to force wrapping with patch("beets.ui.commands.ui.term_width", return_value=30): config["ui"]["import"]["layout"] = "newline" - long_title = "a track with a" + (" very" * 10) + " long name" + long_title = f"a track with a{' very' * 10} long name" self.items[0].title = long_title msg = self._show_change() assert "(#1) a track with" in msg